Привет! Я Илья, мне 19 лет, и последние полтора года я занимаюсь продуктовой разработкой на языке Go.  В этой статье расскажу про процесс разработки кодогенератора, про проблемы, с которыми столкнулся и про их решения. Также в конце подведем итоги и поговорим про саму библиотеку VK-API-SCHEMA и про то, почему на основе такой схемы сделать что-то хорошее не получится. Как-то, путешествуя по всемирная информационной сети интернет, я наткнулся на репозиторий с JSON документацией API ВКонтакте. К ссылке на гитхаб было приложено следующее сообщение: Схема позволяет работать с кодогенераторами, такой подход дает вам возможность уделить больше времени внутренней логике вашего приложения, сэкономив ресурсы на изучении формата данных API. Существующие реализации SDK либо написаны вручную, либо с генераторами, но недоделаны, поэтому решил создать свою библиотеку. Посмотреть исходники можно здесь. Пару моментов перед реализацией: В Golang единственная возможность понять, что поле не просто с дефолтным значением, а пустое - это использовать указатели. Хотя с протобафом такое работает и никто не жалуется, в случае с API это может навредить, поэтому поля с is_required==false будем генерировать с указателем, и слайсы тоже. В схеме могут быть указаны дополнительные параметры к полям, например maxLength для строки, format (json, int64), minimum/maximum для чисел и другие. Такая информация пишется в комментарий для поля. Можно было бы использовать по другому, например, написать валидатор параметров запроса, но смысла в этом я особого не вижу. Каждый метод, помимо обычной error, возвращает возможную ApiError ошибку от API. Не стоит мешать ошибку клиента, обработки JSON или любую другую внутреннюю ошибку с ошибкой в ответе от ВК. Итак, определимся с основными сущностями. У нас есть: Объекты, они же структуры и поля. Бывают разного типа. Такие, как обычный объект, allOf или oneOf имеют поля и непростую логику генерации. В эту же категорию попадают структуры запросов/ответов. Также имеем enum и тип обычное поле (SimpleField), у которого есть просто название и тип.  Объект со ссылкой на другой объект будем трактовать как обычное поле с типом-ссылкой. Нужно преобразовать название ссылочного типа также, как преобразовывается название у внешних объектов, таким образом мы получим реальное название типа. Коды ошибок. Они хранятся в vk-api-schema с описанием и ссылками на subcode'ы. Методы. У каждого метода свои параметры запросов и несколько вариантов ответов, зависящих от входных данных. На этапе чтения объектов появилась проблема бесконечной вложенности типов. То есть, из-за того, что мы парсим JSON, объект любого типа может как поле находится в другом объекте любого типа. Здесь на помощь нам приходят интерфейсы. В нашем случае достаточно двух абстракций: что-то, что будет генерироваться на самом верхнем уровне, и что-то, что будет генерироваться внутри. Такой подход дает возможность независимо от типа данных генерировать объекты неограниченной вложенности: Во время чтения JSON'а в Golang данные сохраняются в map'у, а ее особенность в том, что эти данные хранятся там в случайном порядке. Поэтому для генерируемых структур, методов и всего, что сохраняется в map я добавил метод GetName для сортировки полученного массива по имени, вы же не хотите каждый раз, исполняя go run, получать код в случайном порядке, верно?P.S. Здесь нужно было читать JSON, как строку, например, используя fastjson. Но понял я это слишком поздно. У каждого типа объекта своя генерация на внешнем и внутреннем уровне. Например, если enum объект сам по себе, то для него достаточно сгенерировать новый тип и объявить константы со значениями: Но когда enum объявляется внутри объекта, нужно создать новый внешний enum тип и включить его как поле с только что созданным типом: Таким образом каждый объектный тип должен имплементировать как внутренний, так и внешний интерфейсы генерации.  Многие внутренние объекты повторяют друг друга в названиях, и когда нам требуется создать новый внутренний объект, чтобы сослаться на него, стреляет ошибка ’Something’ redeclared in this package. Поэтому такие внутренние объекты ObjNested в глобальном объекте ObjMain генерируются с названием ObjMain + ObjNested.  Надеюсь, что мои коллеги по цеху не съедят меня за использование модифицированного snake_case, потому как появляются такой методы, как appWidgets.getGroupImageUploadServer или appWidgets.getAppImageUploadServer, а также такие объекты, как newsfeed_item_holiday_recommendations_block_header или adsweb_getAdCategories_response_categories_category. Это я не придумал, а читать это слитно не очень хочется, поэтому в названиях будем склеивать все, кроме первого слова, подразумевающее раздел использования. А для генерации внутренних объектов, как глобальные будем склеивать названия через нижнее подчеркивание как тут: С генерацией простых структур с полями все понятно. С allOf типом тоже: поля генерируются стандартно, а сторонние структуры можем добавить с помощью включения в структуру: А вот с oneOf не все так просто. Да, можно использовать пустой интерфейс для хранения любого типа, но тогда для чтения API ответа придется type accert'ить в строку, а оттуда анмаршалить в структуру. Сделать switch/case, чтобы знать в какую структуру заполнить JSON тоже не получится, потому что анмаршалинг не будет жаловаться на лишнее JSON поле. Поэтому для хранения oneOf определим структуру со слайсом байт, чтобы у пользователя была возможность самому определять, по каким признакам и куда переложить JSON: Упомяну отдельно про JSON-Schema тип `array`. В нем в поле `items` описывается тип объекта для списка. Хоть на уровне схемы это является вложенностью, нам достаточно провалится до корневого объекта, сохранить для него уровень вложенности и сгенерировать насчитанное количество квадратных скобок. Тут все просто. Файл схемы представляет собой перечисление названий с кодом ошибки и описанием. Генерируем им отдельный числовой тип, а затем сами коды ошибок. На сайте с документацией ошибок я нашел почти для каждой ошибки способы их лечения и генерирую их вместе с описанием: Структуры ответов у методов API уже описаны и сгруппированы в отдельные объекты в файле responses.json, их мы сгенерировали как объекты с полями. Но поля запроса нам переданы отдельно. Изначально генератор писал их, как отдельный параметр функции, но позже оказалось, что существуют методы с 20+ полями, поэтому от такого решения я отказался.  Для входных данных нужен отдельный тип, который не только генерируется как структура, но и знает, как правильно записать тип объекта в мапу параметров запроса. Тут нам поможет отдельный интерфейс, который расширяет интерфейс вложенной генерации новым методом. Он возвращает информацию для сборки параметров запроса, такую как название параметра, его тип, логическое is_required значение и так далее. Хорошо, что во входных данных кроме `enum` типа и обычного поля больше ничего не используется, поэтому достаточно добавить новый метод для двух типов. Бывают конечно и структуры, но они указаны с полем format==json, и такое маршалиться в строку.  Не забываем опциональные поля во всех запросах, такие как язык возвращаемых данных, включение/выключение тестового режима, а также поля для прохождения каптчи. Для этого заведем отдельный тип Option. И конечно же контекст для переданного HTTP клиента. Получается такая красота: Мне удалось сгенерировать работающий удобный код, который в добавок быстро работает из-за отсутствия пакета reflect. Сверху еще докинул easyjson, и получилось очень быстро. Бенчмарки можно глянуть тут.  Кодогенерация сильно облегчает поддержку библиотеку, а также дает возможность пользователям получать быстрые обновления SDK. В процессе работы над генератором, попадаясь на неточности в схеме и вставляя костыли, шоб работало, часто хотелось забросить это дело, но мне хотелось доделать до конца, да и на работе пока не приходится писать генераторы, а опыт очень интересный. Я находил много мест, где у JSON-объекта типа object, то есть объект с полями, просто не указан тип. Или есть такие объекты, у которых кроме поля type больше ничего нет, и чтобы корректно сгенерировать такое, нужно отправлять запрос, чтобы посмотреть, что придет в ответе. Но это не дает тебе гарантии, что тип именно тот, что пришел: возможно при других параметрах запроса это поле поменяется со строки на массив объектов. Приходится вручную перебирать все варианты в надежде, что скрытых полей там нет. Или структура передается, как параметр запроса, но поля format со значением json, хотя должно быть.  А кодогенератор - штука чувствительная к входным данным. Хорошо, если твоя логика отловит не стыковку, но скорее всего твой код сгенерируется неправильно, и ты никогда не найдешь ошибочный тип в 60к сгенерированных строках. Множество мест с неточными структурами ответа у методов. Регулярно описания в схеме и в официальной документации расходятся. Иногда верна схема, иногда документация. Получается, что кроме как вызывать все методы подряд в реальном приложении такие ошибки не отловить. Мой PR висит без ответа в репозитории больше полугода, также, как с десяток заведенных мной Issue, а также 40 открытых проблем с авторством других разработчиков. Люди также пытаются писать генераторы, тщетно стараясь исправить проблемы. Тут не нужен отдельный билд с определенными условиями на определенной ОС, чтобы выявить неточность. На репозиторий тупо забили. Я понимаю, что большинство клиентов VK API написано на JS, и SDK для других языков просто не нужно, но люди сами хотят и уже пишут готовые решения, от ВК просто требуется поддерживать актуальную версию схемы. Из-за вышеперечисленных проблем я создал форк репозитория и исправил критические для генерации ошибки.  ВКонтакте классный продукт. Но, резюмируя сказанное, кажется, что пока рано делать любые телодвижения в сторону кодогенераторов для API ВКонтакте.