Генерация 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# код клиента в отдельном окне:

7eb5285e0cef4329bdc3e61eea8ffa4f.png

Исходный код можете скачать здесь. Если не хотите или нет возможности скачать проект и запустить его, то там же можете скачать пример сгенерированного кода клиента.


Исходники нижеприведенного примера можно забрать здесь.

У нас получился код который совместимый с 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(new RequestWotAccountList() 
{ 
    ApplicationId = "demo", 
    Search = SearchNickname 
}); 
GamerAccounts = accounts; 


При этом, благодаря сгенерированному описанию, у нас есть возможность получать подсказки прямо в студии при наборе кода:

0c330ff3af3d4c7dbdb33948fe2ce8eb.png

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

Теперь осталось отобразить список найденных Nickname:

3afe4d684b4545dba47bc238acf86469.png

К сожалению, у меня уже нет своего аккаунта, спасибо Шериеву Амиру (моему брату) за предоставленный игровой ник для растерзания в примерах.

По тапу из списка найденных открываем вторую форму, передав выбранный AccountId:

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


На второй странице тоже создается запрос к другому методу для получения более подробной информации:

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


2ac69bc61d2f42da8dbd1a1595b6cfe0.png

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


Необходимость ручного допиливания напильником


Пожалуй это самый основной недостаток — как и для всего ответа в целом так и для подтипов нет никакой информации, как будут возвращены данные (простой ответ, массив или словарь). Поэтому во многих местах придется делать правки.

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

Для уменьшения объема ответа отфильтруем ответ по технике одного уровня и одной нации. Вызов следующего кода вызовет ошибку:

var client = new WGClient.Client(); 
var response = await client.SendRequestDictionary(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 возвращается в виде массива:

093d709254d54fbab658e03637d4719d.png

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

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

/// 
///Экипаж 
/// 
[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(TRequest request) 


Есть масса способов сделать решение проще. Но максимально простой вариант API можно получить, если компания WG не полениться доделать описание своего собственного API (а именно как нибудь регламентировать что именно можно ожидать от запроса в качестве ответа: словарь, массив или единичный объект в корневом и дочерних узлах ответа).

Nuget или необходимость вручную добавлять библиотеку Json.NET


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

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

Покрытие тестами


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

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

Качество кода


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

Выбор проекта для генерации клиента


На текущий момент генерируется больше 40 тысяч строк для всех проектов сразу. Можно было бы добавить выбор каких проектов и каких методов этого проекта необходимо сгенерировать клиент.

На данный момент все проекты разделены namespace-ами и можно просто удалить проект и тем самым уменьшить конечный размер сборки.

cda3256d22ba4ac4a2a45030e3d12889.png

То же самое касается лишних полей.

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

Резюме


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

Как мы видели из этой статьи, WG мог бы легко генерировать клиентские библиотеки хотя бы для нескольких основных языков программирования, что могло бы сэкономить огромное количество времени и сил для разработчиков путем простого подключения готовой библиотеки. Будем надеяться что, увидим подобные решения от WG.

© Habrahabr.ru