Skip to content

Архитектура ​

Один экран о том, что реально лежит в основном репозитории, чтобы про сгенерированные клиенты ничего не казалось магией.

Пайплайн ​

download-specs.py          →  swaggers/*.yaml         (сырой апстрим, checksummed)
process-specs.py           →  swaggers/processed/     (13 проходов)
generate-clients.py <ver>  →  clients/<lang>/…        (openapi-generator-cli, 8 языков)
    ├── inject-secrets.py          (SecretString-обёртка на каждый язык)
    ├── {black|prettier|gofmt|spotless|php-cs-fixer|rustfmt}   (канонизация форматирования)
    └── generate-readmes.py        (README.md на каждый язык)
     ↓ коммитится тегом v<version>, один тег покрывает все 8 языков
publish.yml                →  PyPI / npm / Go tag / Maven Central / Packagist / hub.oscript.io / NuGet / crates.io
generate-docs.py           →  wb-api-client-docs      (справка по API: страницы операций,
                                                        сниппеты и структуры данных на 8 языках, RU/EN)

Каждый шаг — обычный Python-файл, ничего не скрыто за непрозрачным тулингом.

Зачем генерировать ​

WB выпускает OpenAPI-спецификации. Написать 13 клиентов × 8 языков вручную — уплывут за неделю. Плюс генерации из спецификации: каждое поле, каждый enum, каждая обёртка ответа — ровно то, что задокументировал WB. И повторная синхронизация стоит нуля усилий после сборки пайплайна.

Минус — выхлоп генератора остаётся выхлопом генератора: местами уродливым, местами со следами кривостей спецификации. Именно поэтому есть слой пост-обработки.

process-specs.py — 13 проходов ​

  1. inject_bearer_auth — удаляет схему HeaderApiKey (дублирует реальный bearer JWT) и ставит одну BearerAuth. Каждый сгенерированный клиент получает ровно один путь авторизации.
  2. fix_untyped_arrays — добавляет items: {type: string} массивам без указания внутреннего типа (иначе Go / TS сгенерируют []any).
  3. widen_integer_formats — ставит format: int64 целым полям без формата и с форматами uint32/uint64/Unix-timestamp: WB использует их для ID и миллисекундных меток времени, которые переполняют знаковый 32-битный int. Явные int32 и коды валют не трогаются.
  4. inline_top_level_arrays — инлайнит все $ref на компоненты с type: array и схлопывает обёртки allOf: [array, description-only] (openapi-generator плохо работает с массивом на корне). Гоняется до неподвижной точки, потому что заинлайненные поддеревья могут содержать другие ссылки.
  5. rename_digit_prefixed_schemas — 409SupplyDeliverError → Http409SupplyDeliverError (иначе невалидный Go-идентификатор).
  6. name_inline_response_schemas — поднимает анонимные inline-тела 4xx/5xx в схемы <OperationId>Response<code> (снова ради валидных Go-идентификаторов).
  7. drop_response_required — WB помечает required почти каждое поле ответа, но живой API опускает поля когда хочет, и тогда каждый язык падает на десериализации. Проход удаляет required у всех схем, достижимых из ответов; у тел запросов required сохраняется, так что валидация ввода не страдает.
  8. sanitize_non_ascii_enums — добавляет x-enum-varnames для русских значений вроде "Склад WB" (иначе — пустые идентификаторы в JS/Go/Java).
  9. rename_tags_to_module — теги WB русские (Финансовые отчёты), openapi-generator зачищает их до пустого имени класса, и все языки получают Api/DefaultApi. Проход переписывает теги каждой операции на имя модуля (Finances, Reports, …), поэтому у модуля один нормально названный класс (FinancesApi, …), а исходные теги сохраняются в x-original-tags для генератора документации.
  10. drop_date_formats — WB возвращает "" для неустановленных дат (fixTariffDateFrom, utdUcdDate), что не парсится ни одним строгим date-парсером. Проход снимает format: date / format: date-time со строковых схем, и клиенты типизируют их как обычные строки.
  11. htmlize_descriptions_to_markdown — каждое поле description прогоняется через markdownify, чтобы HTML в докстрингах рендерился как Markdown.
  12. absolutize_description_links — к относительным ссылкам […](/openapi/…) добавляется https://dev.wildberries.ru, чтобы ссылки в сгенерированных докстрингах открывались.
  13. add_docs_links — добавляет строку Library doc: <url> в description каждой операции со ссылкой на страницу операции на этом сайте. Генераторы копируют description в докстринги/Javadoc, поэтому каждый метод клиента ссылается на свою страницу документации. generate-docs.py вырезает эту строку обратно, так что сам сайт не затрагивается.

generate-clients.py ​

  • Гоняет openapi-generator-cli (закреплённый Docker-образ) для каждой спецификации и каждого языка во временные каталоги.
  • Склеивает пер-спецификационные SDK в единое дерево на язык — Python-пакеты под wb_api_client.<slug>, TypeScript subpath-экспорты, Go под-пакеты, Java под-пакеты, PHP под-пространства, OneScript — по префиксу имени класса (пространств имён в языке нет), C# — пространства имён, Rust — модули одного крейта (wb_api_client::<slug>).
  • Запускает inject-secrets.py, чтобы патчить каждый сгенерированный Configuration / ApiClient соответствующей языковой обёрткой секрета.
  • Запускает закреплённый форматтер: black, prettier, gofmt, spotless (google-java-format), php-cs-fixer, dotnet format, cargo fmt (rustfmt). Всё внутри Docker — без языковых рантаймов на хосте.
  • Подставляет __VERSION__ в топ-уровневые манифесты (pyproject.toml, package.json, go.mod, pom.xml, composer.json, Cargo.toml).
  • Запускает generate-readmes.py, чтобы выпустить README на каждый язык.

Итог: clients/{python,typescript,go,java,php,onescript,csharp,rust}/ — восемь пакетов, готовых к публикации.

Детерминизм ​

Ежедневная задача постоянно бы «дрожала», не будь генерация детерминированной. Что для этого сделано:

  • Docker-тег openapi-generator-cli закреплён.
  • В конфиге openapi-generator для каждого языка выставлено hideGenerationTimestamp: true.
  • Все форматтеры закреплены на конкретных версиях.
  • PR-check-workflow перегенерирует всё с нуля и падает, если git diff swaggers/ clients/ непустой. Это ловит любое изменение, из-за которого следующие прогоны разъедутся.

Публикация ​

daily-check.yml считает версию, регенерирует, коммитит, тегает. Затем шлёт 8 событий workflow_dispatch в publish.yml — по одному на язык — с тегом в качестве входа ref. Каждая языковая ветка publish.yml:

  • Python — OIDC → PyPI trusted publishing (pypa/gh-action-pypi-publish).
  • TypeScript — OIDC → npm trusted publishing (npm publish --provenance).
  • Go — тег пушится в wb-api-client-go (см. ниже), сама джоба — no-op: proxy.golang.org тянет прямо из тега.
  • Java — подписанный GPG-деплой в Maven Central через central-publishing-maven-plugin. Maven закреплён на 3.9.x (stCarolas/setup-maven): плагин собирает bundle через Maven-2 stager, и Resolver из Maven 3.10 роняет туда служебный maven-metadata-local.xml, из-за чего Central отклоняет bundle («Bundle has content that does NOT have a .pom file»).
  • PHP — пинг API packagist.org/api/update-package.
  • OneScript — opm build и opm push в hub.oscript.io.
  • C# — dotnet pack и dotnet nuget push в NuGet.
  • Rust — cargo publish в crates.io (GitHub-окружение crates-io, toolchain закреплён на 1.98).

Отдельно daily-check перегенерирует справку по API (generate-docs.py — страницы операций, сниппеты вызова и структуры входящих/исходящих данных на каждом языке, ссылки на спецификацию WB) и пушит её в wb-api-client-docs, откуда сайт деплоится на GitHub Pages.

Reusable-воркфлоу (workflow_call) был бы аккуратнее, но PyPI и npm trusted publishing их не поддерживают: и OIDC-claim job_workflow_ref (callee), и Sigstore-attestation workflow_ref (caller) должны указывать на один и тот же файл, что с workflow_call невозможно. Поэтому — workflow_dispatch.

Соседние репозитории ​

Четыре языка живут в своих репозиториях, подключённых сюда как git-submodule:

  • ValeryVerkhoturov/wb-api-client-php → clients/php. Причина: Packagist требует composer.json в КОРНЕ обходимого репозитория.
  • ValeryVerkhoturov/wb-api-client-1c → clients/onescript. Так пользователи 1С могут склонировать только клиент и поставить его из тега.
  • ValeryVerkhoturov/wb-api-client-go → clients/go. Причина: Go версионирует модули тегами репозитория, поэтому модуль обязан лежать в корне репо (go get github.com/ValeryVerkhoturov/wb-api-client-go) — модуль в подкаталоге потребовал бы тегов вида clients/go/v… и тянул бы весь монорепозиторий.
  • ValeryVerkhoturov/wb-api-client-rust → clients/rust. Причина: crates.io связывает крейт с выделенным репозиторием, а git-зависимости (wb-api-client = { git = "…", tag = "vX.Y.Z" }) резолвятся от корня репозитория.

Ежедневный прогон коммитит и тегает все четыре соседних репозитория ДО основного — иначе записанные указатели сабмодулей ссылались бы на коммиты, которых на их удалённых ещё нет, и свежий git clone --recurse-submodules падал бы.

Пятый сосед — ValeryVerkhoturov/wb-api-client-docs, этот сайт. Он не сабмодуль: daily-check клонирует его, запускает generate-docs.py и пушит результат обратно.

Генератор OneScript ​

В openapi-generator нет генератора OneScript, поэтому он вынесен в отдельный репозиторий — ValeryVerkhoturov/onescript-openapi-generator. Это обычный плагин: класс наследует DefaultCodegen, регистрируется через META-INF/services и подключается рядом с openapi-generator-cli.jar, так что -g onescript работает как встроенный генератор. В основном репозитории он закреплён по коммиту — это аналог закреплённого тега Docker-образа для остальных языков.

Что НЕ генерируется ​

Ручные части основного репозитория:

  • scripts/ — сам пайплайн, разложенный по доменным контекстам в scripts/pipeline/: core/ (общее ядро: пути репозитория, именование спецификаций/модулей/языков, YAML IO, запуск внешних инструментов (subprocess, docker, git, python, macOS codesign)), specs/ (download, postprocess), generation/ (generate, secrets), onescript/ (toolchain, manifests, verify), docs/ (reference, readmes). Плоские scripts/*.py — тонкие CLI-обёртки над этими модулями, названные по производимому результату: download-specs, process-specs, generate-clients, generate-readmes, generate-docs, generate-onescript-manifests, inject-secrets, verify-onescript. Домашнее правило: ни #-комментариев, ни докстрингов под scripts/ — проверяется в make scripts-check и в PR-check.
  • generator-configs/*.yaml — по одному конфигу openapi-generator на язык.
  • templates/* — топ-уровневые манифесты с плейсхолдерами __VERSION__.
  • .github/workflows/* — daily-check, per-language publish, PR drift check.
  • Makefile, README.md, CLAUDE.md.

Всё под clients/ — сгенерированный выхлоп, руками не редактируется. Перегенерируйте.

Полное описание пайплайна ​

Файл CLAUDE.md в основном репозитории — рабочая, машиночитаемая версия того же содержимого, поддерживается в актуальном состоянии.

wb-api-client mascot

Распространяется под лицензией Apache 2.0.