Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Игра в прятки: кодогенерация вопреки JSON

Anna | 2.07.2014 | нет комментариев

Ужасно подумать, но ещё каких-то десять лет назад разработка системы самого заштатного RPC была целым праздником в жизни разработчика. Болезненным и долгим праздником, как свадьба для лошади: голова в цветах, зад в мыле. Это было жутко интересно и единовременно немыслимо запарно. Один выбор протокола чего стоил. Я уж не говорю о борьбе с могучими и Жуткими фреймворками, типа DCOM либо CORBA. Реализация транспортного яруса вообще была уделом людей с длинными бородами.

В наше радостное время жизнь программиста под iOS должна быть легка и славна. Транспорт давным-давно перестал быть задачей. А RPC? Легко: достаём из кобуры Apache Thrift либо на худой конец Google Protocol Buffers и пожалуйста, с минимальным напряжением головного мозга готов и протокол, и сервер, и заказчик. Подавляющему числу приложений в AppStore только это и необходимо: примитивный и внятный интерфейс к удаленным процедурам, желанно в славных обертках из нативных классов, и такая же простая и внятная обработка ошибок. Всё.

Но. К сожалению, и Thrift, и Protobuf заточены под одновременную разработку заказчика и сервера. А такая успех случается в карьере программиста не Зачастую. В основном доводится иметь дело с ворохом давным-давным-давно написанных и данных нам в ощущениях сетевых источников, всякий из которых желает общаться с внешним миром в своей собственной неповторимой повадке. Безусловно, всё не так нехорошо, как десять лет назад, и Зачастую все пожелания сервер-сайда сводятся к REST JSON в маленьких вариациях. Но даже на эту де-факто стандартизацию больно глядеть человеку, избалованному Thrift-ом. Никакой типизации, сплошное сопоставление строк, выливающееся в тонны однотипного кода с изнурительными однообразными проверками. Разумеется, задачу давным-давно поняли, и для многих языков, скажем, имеется целый зоопарк средств прозрачной конверсии JSON в нативные объекты. Для Objective-C, безусловно, есть комбайн RestKit, но он полагается на интроспекцию и мапит все конструкции в рантайме. Вновь же, настройка такого динамического маппинга далека от изящества на мой вкус. Остальные библиотеки и утилиты, которые я пробовал, для реальной жизни подходили еще дрянней. Скажем, какой-нибудь JsonPack с его формочками в браузере трудновато встроить в постоянную сборку.

Я выбирал инструмент для нового плана i-Free, в котором ожидался пяток разнородных REST-сходственных сервисов, и своих родных, и внешних, всякий с пучком способов, выводком разлапистых конструкций и другими увлекательными подробностями. Мысль о необходимости ручного разбора словарей была непереносима. Как и предвкушение рефакторинга позже неотвратимых изменений в протоколах. Жизнь слишком коротка, Дабы занимать её неинтересной работой, так что отменнее день утратить, потом за пять минут долететь. Через день три дня первая версия ifacegen теснее генерировала код, наивный, но много.

Что такое ifacegen?

Это бесхитростный консольный инструмент (python-скрипт), генерирующий классы Objective-C из простого IDL, максимально схожего на начальный протокольный JSON. Классы эти прозрачно перепаковывают своё состояние в JSON и обратно. Вдобавок ifacegen может генерировать классы-обёртки для прозрачного вызова способов удалённых сервисов.

Для чего он необходим?

ifacegen работает как Thrift напротив. Он разрешает взять присутствующий REST JSON API и натянуть на негосопоставить конструкциям JSON и способам REST соответствующие классы Objective-C. Вы манипулируете в программе только вызовами способов натуральных для языка типов, а сгенерированный код прячет все непотребные подробности.

Для чего он верно не сгодится?

ifacegen верно не подойдет для сериализации произвольных классов в JSON и создания классов из всякого JSON-а на лету. Никакого рантайма, только компайл-тайм, только хардкор. Никакого NSCoder. И никаких других языков, помимо Objective-C.

Насколько трудный протокол дозволено адаптировать?

Достаточно трудный. ifacegen переваривает вложенные словари, массивы, в том числе массивы словарей (словарей с массивами, ну вы осознали). Словари конвертируются в классы, элементы словарей конвертируются в поля (@property) классов. IDL поддерживает изложение атомарных типов элементов: int32, int64, double, string, bool, raw и rawstr. raw — это произвольный словарь, напрямую копируемый в поле типа NSDictionary, rawstr — тоже произвольный словарь, но закодированный в строку типа такой: "{\"weird\":42,\"str\":\"yes\"}".

Насколько это легко?

Cинтаксис IDL максимально следует за данными (и да, язык изложения — сам по себе полновесный JSON). За примером пойдём к Google Places API. Вот как в его документации описывается гео-точка в результатах обслуживания:

"geometry" : {
            "location" : {
              "lat" : -33.8583790,
              "lng" : 151.2100270
            }
        }

ifacegen IDL, описывающий эту конструкцию, должен буквально её повторить (имена полей и их вложенность обязаны совпадать), добавив немножко разметки для комфорта. Поля с именами “id” и “void” либо начинающиеся со строки «new», «alloc», «copy» и «mutableCopy» механически получат префикс “the”, чтоб не мешать объявлениям в коде. Все объявления размещаются в массиве с заголовком “iface”:

{"iface": [
{
    "struct":"GoogleGeometry",
    "typedef" : {
            "location" : {
                "lat" : "double",
                "lng" : "double"
            }
    }
}
}

Самое время для легкой кодогенерации. При условии, что мы разместили объявления IDL в файл GoogleClient.json, она будет выглядеть так:

$ python ifacegen.py GoogleClient.json

В итоге получим два файла: GoogleClient.h и GoogleClient.m. Заголовок будет содержать объявления интерфейсов сгенерированных конструкций:

@interface GoogleGeometryLocation: NSObject
@property (nonatomic) double_t lat;
@property (nonatomic) double_t lng;
@end;

@interface GoogleGeometry: NSObject
- (NSData*)dumpWithError:(NSError* __autoreleasing*)error;
- (id)initWithLocation:(GoogleGeometryLocation*)location;
- (id)initWithDictionary:(NSDictionary*)dictionary error:(NSError* __autoreleasing*)error;
- (id)initWithJSONData:(NSData*)jsonData error:(NSError* __autoreleasing*)error;
@property (nonatomic) GoogleGeometryLocation* location;
@end;

Для всех вложенных словарей тоже будут сгенерированы примитивные классы. Но личные способы сериализации/десериализации будут только у конструкций верхнего яруса, напротив дюже распухает генераторный код. Разумеется, вложенный словарь тоже дозволено описать отдельной конструкцией с произвольным именем. Сейчас всюду, где встречается данный JSON-фрагмент, мы можем легко вставить его имя:

{
"struct":"GoogleLocation",
"typedef" : {
      "lat" : "double",
      "lng" : "double"
	}
},

{
"struct":"GoogleGeometry",
"typedef" : {
    	"location" : "GoogleLocation"
	}
}

Поддерживается наследование объявленных таким образом конструкций. Скажем, если внезапно потребуется описать расширенный вариант GoogleLocation, не непременно будет повторять все её поля, довольно отнаследоваться:

{
"struct":"GoogleLocationEx",
"extends": "GoogleLocation", 
"typedef" : {
      "elv" : "double"
	}
} 

Дозволено даже импортировать типы, объявленные во внешнем модуле. Модули эти ищутся в том же каталоге, что и начальный. Генерации кода, кстати, при импорте не будет, легко засосутся типы.

{"iface": [
{ "import": "OtherModule.json" }
...

Ок, это конструкции. А что с вызовами?

Вновь идем к Google Places API. Возьмём примитивный вызов: запрос данных о локации. У него есть URL, типа такого:
https://maps.googleapis.com/maps/api/place/details/json?<parameters>
И, безусловно, оно может возвращать JSON. Конструкцию возвращаемого значения я назову GoogleDetailsResult и опущу для простоты, а изложение самого вызова будет таким:

{
"procedure": "placeDetails",
"prefix": "place/details/json",
"prerequest": {
	"key":"string",
	"reference": "string",
	"sensor": "string",
	"language": "string"
  },
"request" : {},
"response": "GoogleDetailsResult"
}

Поле prefix определяет определенную функцию обслуживания, которую мы будем вызывать в этом способе. Поле prerequest принимает строковые параметры, которые лягут в URL (примерно по RFC 3986), а request принимает изложение конструкции JSON, если параметры вызова нужно передавать через неё. В нашем примере оставляем его пустым. Всё. Вновь жмем кнопку «сгенерировать» и откидываемся в кресле (на самом деле нет):

- (GoogleDetailsResult*)placeDetailsWithKey:(NSString*)key
andReference:(NSString*)reference
andSensor:(NSString*)sensor
andLanguage:(NSString*)language
andError:(NSError* __autoreleasing*)error;

В клиентском коде вызов удалённого способа будет выглядеть приблизительно так:

GoogleDetailsResult* result = [self.google placeDetailsWithKey:key
                                                  andReference:ref
                                                     andSensor:@"true"
                                                   andLanguage:locale
                                                      andError:&error];

Для неленивых немножко механического Objective-C кода: GoogleClient.m

- (GoogleDetailsResult*)placeDetailsWithKey:(NSString*)key
		andReference:(NSString*)reference
		andSensor:(NSString*)sensor
		andLanguage:(NSString*)language
		andError:(NSError* __autoreleasing*)error {
	id tmp;
	[transport setRequestParams:@{
		@"key" : NULLABLE(key),
		@"reference" : NULLABLE(reference),
		@"sensor" : NULLABLE(sensor),
		@"language" : NULLABLE(language)
	}];
	if ( ![transport writeAll:nil prefix:@"/place/details/json" error:error] ) {
		return nil;
	}
	NSData* outputData = [transport readAll];
	if ( outputData == nil ) {
		return nil;
	}
	NSDictionary* output = [NSJSONSerialization JSONObjectWithData:outputData options:NSJSONReadingAllowFragments error:error];
	if ( *error != nil ) {
		return nil;
	}
	GoogleDetailsResult* GoogleDetailsResult235;
	GoogleDetailsResult235 = [GoogleDetailsResult new];
		GooglePlaceDetails* GooglePlaceDetails236;
		NSDictionary* outputResult237 = [output objectForKey:@"result"];
		if ( outputResult237 != nil && ![outputResult237 isEqual:[NSNull null]]) {
			GooglePlaceDetails236 = [GooglePlaceDetails new];
				NSArray* outputResult237Address_components238 = [outputResult237 objectForKey:@"address_components"];
				NSMutableArray* address_components4;
				if ( outputResult237Address_components238 != nil && ![outputResult237Address_components238 isEqual:[NSNull null]]) {
					address_components4 = [NSMutableArray arrayWithCapacity:[outputResult237Address_components238 count]];
					for ( id item in outputResult237Address_components238) {
						GoogleAddressComponent* GoogleAddressComponent239;
						GoogleAddressComponent239 = [GoogleAddressComponent new];
						GoogleAddressComponent239.longName = ( tmp = [item objectForKey:@"long_name"], [tmp isEqual:[NSNull null]] ? nil : (NSString*)tmp );
						GoogleAddressComponent239.shortName = ( tmp = [item objectForKey:@"short_name"], [tmp isEqual:[NSNull null]] ? nil : (NSString*)tmp );
							NSArray* itemTypes240 = [item objectForKey:@"types"];
							NSMutableArray* types7;
							if ( itemTypes240 != nil && ![itemTypes240 isEqual:[NSNull null]]) {
								types7 = [NSMutableArray arrayWithCapacity:[itemTypes240 count]];
								for ( id item in itemTypes240) {
									[types7 addObject:item];
								}
							}
						GoogleAddressComponent239.types = types7;
						[address_components4 addObject:GoogleAddressComponent239];
					}
				}
			GooglePlaceDetails236.addressComponents = address_components4;

// ... Еще дюже много такого же колоритного нескучного кода

	GoogleDetailsResult235.result = GooglePlaceDetails236;
	GoogleDetailsResult235.status = ( tmp = [output objectForKey:@"status"], [tmp isEqual:[NSNull null]] ? nil : (NSString*)tmp );
	return GoogleDetailsResult235;
}

Что там с транспортом?

С транспортом тоже всё легко. Сгенерированный код полагается в вызовах на объект, реализующий протокол IFTransport:

@protocol IFTransport
- (void)setRequestParams:(NSDictionary*)params;
- (BOOL)writeAll:(NSData*)data prefix:(NSString*)prefix error:(NSError* __autoreleasing*)error;
- (NSData*)readAll;
@end

Из коробки прилагается примитивная реализация транспорта IFHTTPTransport, использующая NSURLConnection и протокол HTTP(S). Если имеются входные JSON-параметры, механически применяется способ POST. В остальных случаях — GET. Ранги HTTP поменьше 200 и огромнее 202 интерпретируются как ошибки, заворачиваются в NSError и передаются наверх. Если есть надобность добавочно отмодифицировать реквест либо возвести URL-параметры по-своему, дозволено воспользоваться категорией IFHTTPTransport (Protected). В ней объявлены способы для преемников:

- (NSMutableURLRequest*)prepareRequestWithURL:(NSURL*)url data:(NSData*)data;
- (NSString*)buildRequestParamsString:(NSDictionary*)requestParams;
- (BOOL)shouldBreakOnError:(NSError*)error;

Все вызовы протокола IFTransport в сгенерированном коде подразумеваются синхронными. Никаких блоков либо делегатов. Предполагается, что клиентский код отменнее знает, какую тактику асинхронности нужно предпочесть для всякого семантического способа. Скажем, несколько последовательных вызовов удаленного обслуживания значительно внятнее упаковать в один асинхронный блок, чем строить реактивную матрешку из 3 вложенных коллбэков.

В итоге сборка каждой RPC-подсистемы для заказчика выглядит так:

NSURL* googleURL = [NSURL URLWithString:@"https://maps.googleapis.com/maps/api"];
id<IFTransport> transport = [[IFHTTPTransport alloc] initWithURL:googleURL];
self.google = [[google alloc] initWithTransport:transport];

В чём подвох?

Как неизменно, сроки поджимали, следственно пришлось срезать пару углов. На чём сэкономили: во-первых, код генерируется только для ARC, простите. Во-вторых, внутри для простоты всё равно применяется NSJSONSerialization, и имеется некоторое избыточное перепаковывание данных в промежуточный словарь. Память тоже не экономится, следственно для крупных комплектов данных ifacegen подходит нехорошо. Код первоначально не планировалось открывать, следственно python применяется в режиме продвинутого шелла, без особенных изысков.

Что касается ограничений системы типов: нет вероятности задать префикс для всех типов, а также нет опережающих объявлений, немыслимо рекурсивное определение типа. Атомарных типов маловато. Нет разных плюшек типа дат, перечислений и прочего. Система импорта типов из других объявлений слабовата и не рассчитана на хитроумные трюки типа циклов либо ромбов, в всеобщем, легко поведётся на всякий подлог.

Из косяков: тип параметров в prerequest принимается только «string» в вере, что автор ничего не напутает. Ну и как неизменно, сообщения об ошибках не блещут конкретностью. Если есть оплошность в синтаксисе IDL, скорее каждого парсер вывалится со стек-трейсом.

Где брать?

На Bitbucket. Там же кратенько инструкция по IDL. Пример прилагается.

$ git clone https://bitbucket.org/ifreefree/ifacegen.git

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB