Генерація C# клієнта для Wargaming API

WG API надає дуже докладний опис API, але при цьому не надає жодних бібліотек для доступу до API. На жаль API не використовує жодних із стандартів, які могли б автоматично згенерувати моделі та методи. Крім того, в JSON відповідях не вийшло згенерувати моделі із за особливостей структури відповіді. У підсумку виявилося, що простіше написати моделі (і тим більш методи) вручну, але це заняття виявилося дуже рутинною і нудною. У статті розглянемо автоматизацію створення моделі і методів запиту з опису HTML, а також отримані переваги і недоліки.

Розробка
В силу відсутності необхідності у мене немає додатків в бою з WG API, але при цьому мені подобається використовувати це API в якості прикладу в різних демонстраціях за простий і відкритий доступ до даних. На жаль я не знайшов жодної робочої бібліотеки під WG API .NET і я написав свою утиліту для генерації клієнта на C#, яка читає документацію з сайту WG API і переробляє в готові моделі запитів і відповідей

На превеликий жаль документація WG API не повна і в деяких моментах зрозуміти який саме відповідь повернеться можна тільки з уже отриманої відповіді з JSON.

Пізніше хаброжитель thunderspb підказав значно більш простий спосіб отримання схеми даних, із за чого я відклав публікацію статті (майже на два місяці за гострої нестачі часу). На жаль і ця схема даних має рівно тими ж недоліками — немає можливості зрозуміти формат відповіді для складених типів даних. В майбутньому я планую переробити утиліту для роботи з цією схемою даних, але так як на поточний момент робота з WG API для мене не приоритетна, я вирішив викласти те, що було реалізовано на поточний момент.

Реалізація:
В силу того що WG зовсім не документує структуру того, що буде віддавати запит на виході довелося зробити трохи незручний на практиці формат запитів і відповідей. На виході ми можемо отримати як просто одиничний відповідь, так само можна отримати масив або словник. Як було сказано вище, дізнатися що ж буде на виході можна буде лише відправивши запит і отримавши відповідь у вигляді JSON, де і побачимо в живих даних, як саме повертаються дані (добре що в документації є посилання на API Explorer в кожному методі, який дозволяє дуже легко формувати запит і бачити відповідь прямо в браузері).

Зрештою, у нас вийшло дуже просте додаток, де по черзі відкриваються сторінки з WG API і распарсиваются. Після цього генерується і виводиться C# код клієнта в окремому вікні:



Вихідний код можете завантажити тут. Якщо не хочете або немає можливості завантажити проект і запустити його, то там же можете завантажити приклад згенерованого коду клієнта.

Приклад використання:
Исходники наведеного нижче прикладу можна забрати тут.

У нас вийшов код який сумісний з PCL, тому ми можемо використовувати цей код як для клієнтських додатків, так і серверних веб додатків.

Для прикладу витратив 15 хвилин і створив тестове Xamarin Forms додаток без дизайну, де накидав, у першій формі пошук по ніку, а у другій формі інформацію про обраному ніке серед знайдених.

У створений проект додав cs файл, куди скопіював код , згенерований нашої утилітою.

Наступний крок — додавання бібліотеки Json.net (пошук в nuget бібліотеки Newtonsoft.Json або командою Install-Package Newtonsoft.Json c nuget консолі).

Код пошуку виходить досить простим:

var client = new WGClient.Client(); 
var accounts = await client.SendRequestArray<ResponseWgnAccountList>(new RequestWotAccountList() 
{ 
ApplicationId = "demo", 
Search = SearchNickname 
}); 
GamerAccounts = accounts; 

При цьому, завдяки згенеровані опису, у нас є можливість отримувати підказки прямо в студії при наборі коду:



Зверніть увагу, ApplicationId: «demo» можна використовувати тільки для тестування API. Для релізу необхідно створити свій ApplicationId особистому кабінеті

Тепер залишилося відобразити список знайдених Nickname:



На жаль, у мене вже немає свого аккаунта, спасибі Шериеву Світові (мого брата) за наданий ігровий нік для розтерзання в прикладах.

По тапу зі списку знайдених відкриваємо другу форму, передавши вибраний AccountId:

var item = e.SelectedItem as ResponseWgnAccountList; 
Navigation.PushAsync(new DetailsPageView(item.AccountId)); 

На другій сторінці теж створюється запит до іншого методу для одержання детальнішої інформації:

var client=new Client(); 
var response=await client.SendRequestDictionary<ResponseWotAccountInfo>(new RequestWotAccountInfo() 
{ 
ApplicationId = "demo", 
AccountId = accountId 
}); 



Таким чином, за допомогою нашого згенерованого клієнта у нас є можливість заощадити величезну кількість часу на автоматизацію рутини формування запиту і відповіді до WG API і зосередити час, сили і увагу на сам додаток.

Недоліки:
Необхідність ручного допиливания напилком
Мабуть це самий основний недолік — як і для всього відповіді в цілому так і для підтипів немає ніякої інформації, як будуть повернуті дані (простий відповідь, масив або словник). Тому в багатьох місцях доведеться робити правки.

Возмьем для прикладу метод Техніка (encyclopedia/vehicles).

Для зменшення обсягу відповіді відфільтруємо відповідь по техніці одного рівня і одного нації. Виклик наступного коду викличе помилку:

var client = new WGClient.Client(); 
var response = await client.SendRequestDictionary<ResponseWotEncyclopediaVehicles>(new RequestWotEncyclopediaVehicles() 
{ 
ApplicationId = "demo", 
Tier = "8", 
Nation = "ussr" 
}); 

Викинувши виняток:

Unhandled Exception: Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'WGClient.WorldOfTanks.WotEncyclopediaVehiclesCrew' because the type requires a JSON object (e.g. {«name»:«value»}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {«name»:«value»}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.

Звідси випливає, що не вдалося десериализовать підтип Crew, і якщо ми побудуємо запит API Explorer, то побачимо, що Crew повертається у вигляді масиву:



При цьому поле Engines коректно розпізналося завдяки тому, що для нього був вказаний тип поля «list of integers» на відміну від підтипу Crew, для якого немає ніякої інформації.

Виправити помилку можна, зробивши поле Crew масивом, замінивши поле:

/// < summary> 
///Екіпаж 
/// < /summary> 
[JsonProperty("crew")] 
public WotEncyclopediaVehiclesCrew Crew { get; set; } 

на масив:

public WotEncyclopediaVehiclesCrew[] Crew { get; set; } 

Аналогічну помилку отримуємо для default_profile.ammo, відповідно, там теж необхідно виправити зробивши масив.

На жаль, майже в кожному методі доведеться вносити такі зміни, але тим не менше навіть з урахуванням цих правок обсяг роботи істотно поліпшується.

Залежність від HTML
Так як WG API не використовує ніяких стандартів опису JSON, доводиться задовольнятися парсингом HTML опису. А HTML може змінюватися довільним чином, а опис одних і тих же типів можуть відрізнятися і зустрічаються навіть російською мовою, тобто немає жодних гарантій, що завтра не з'явиться нова назва використовуваного типу.

В наступній версії буду розбирати замість HTML опису з JSON І вплив цієї проблеми трохи знизитися.

Браузер
Поточне рішення побудоване на базі CefSharp що вже означає що рішення буде працювати тільки на платформі Win32. Можна переписати з використанням бібліотек CefSharp, щоб отримати кроссплатформенной рішення. Знову таки перехід на прсинг JSON дозволить позбутися від залежності CefSharp і зробити рішення яке буде працювати на Windows, Mac і Web.

Поганий інтернет
Парсер не враховує, що інтернет може пропасти з усіма витікаючими наслідками.

Неоптимальне API клієнта
В кінченому підсумку зупинився на досить багатослівному і не дуже зручному великоваговому варіанті:

SendRequest<TResponse>(TRequest request) 

Є маса способів зробити рішення простіше. Але максимально простий варіант API можна отримати, якщо компанія WG не полінуватися доробити опис свого власного API (а саме якось регламентувати що саме можна очікувати від запиту до відповіді: словник, масив або одиничний об'єкт в кореневому і дочірніх сайтах відповіді).

Nuget або необхідність вручну додавати бібліотеку Json.NET
На жаль, на даний момент немає можливості генерувати рішення у вигляді складання або nuget пакету так як немає можливості відразу отримати повноцінно працюючий код з проблем описаних вище (брак опису відповіді запиту).

Рішення з Nuget пакетом могло б позбавити від необхідності копіювати вихідний код і підключати вручну Json.NET але як вже було сказано вище, місцями доведеться допілівать напилком.

Покриття тестами
1-2 рази в місяць виходить нова версія API. Відповідно перегенерировав весь відповідь ми автоматично втратимо всі правки напилком, які у нас вже були зроблені.

Для того, щоб переконатися, що в новій версії все коректно допрацювали напилком, можна було б покрити тестами отримане рішення для того, щоб переконатися, що не забули допилити напилком що небудь. За великим рахунком, можна було б автоматично генерувати тести. В теорії це дозволить частково переконатися в тому, що нова версія не разломает геть.

Якість коду
Як і було сказано вище, код писався як прототип рішення, який потім не шкода буде викинути і переписати в робочий варіант. На всі рішення (включаючи приклад написаний за 15 хвилин) було витрачено сумарно пара вечорів.

Вибір проекту для генерації клієнта
На поточний момент генерується більше 40 тисяч рядків для всіх проектів відразу. Можна було б додати вибір яких проектів і яких методів цього проекту необхідно згенерувати клієнт.

На даний момент всі проекти поділені namespace-ами і можна просто видалити проект і тим самим зменшити кінцевий розмір збірки.



Те ж саме стосується зайвих полів.

Можна нескінченно займатися поліпшенням, але на цьому можна підвести резюме.

Резюме
Сам факт того що Wargaming намагається бути відкритим до розробників і намагається не просто відкривати API, але ще і детально описує значення полів дуже похвальна. Незважаючи на істотні недоліки в описі і в самій структурі відповіді (а саме те що не можна зрозуміти що буде повернений, одиночний відповідь, масив або словник для складних типів), це одне з кращих документацій API для подібних сервісів.

Як ми бачили з цієї статті, WG міг би легко генерувати клієнтські бібліотеки хоча б для декількох основних мов програмування, що могло б заощадити величезну кількість часу і сил для розробників шляхом простого підключення готової бібліотеки. Будемо сподіватися що, побачимо подібні рішення від WG.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.