# Генератор SDK

Генерирует части PHP и TypeScript SDK из спецификаций API (`docs/api/*.yaml`, ADR-0011):

```powershell
composer sdk:generate            # записать сгенерированные файлы
php sdk/generator/bin/generate --check   # только проверить, что они актуальны
```

Генератор владеет двумя каталогами, всё в них сгенерировано и правится только генератором:
`sdk/php/src/Generated` и `sdk/typescript/src/generated`. Тест `RealSpecsTest` падает, если спецификацию
изменили, а код не перегенерировали, если сгенерированный файл поправили руками или если остался лишний файл.

## Закрытый мир

Генератор принимает только то, что умеет превратить в код. Остальное он отвергает с JSON-указателем,
чтобы новая конструкция в спецификации не превратилась незаметно в нетипизированное значение.

**Поддерживается:**

- схемы: `type` (одно из `string`, `integer`, `number`, `boolean`, `object`, `array`, `null` или тип и `null`),
  `properties`, `required`, `additionalProperties` (`false` или схема значений при отсутствии `properties`), `items`,
  `enum` строк (и `null`, если тип его допускает), локальный `$ref`, `oneOf` только в виде `[схема, {type: null}]`,
  `allOf` объектов (свойства объединяются, обязательные — из любой части);
- ключевые слова только для проверки и документации игнорируются: `format`, `pattern`, `minimum`, `maximum`,
  `minLength`, `maxLength`, `minItems`, `maxItems`, `uniqueItems`, `minProperties`, `maxProperties`, `default`,
  `examples`, `description`, `title`;
- параметры: путь (строка), запрос (одно значение: строка, число, логическое, перечисление), заголовки только
  `Idempotency-Key` и `Loyal-Merchant` — их клиенты отправляют сами;
- тело запроса с `application/json` (форма допускается только рядом с JSON); успешный ответ — `application/json`
  или без содержимого, у всех 2xx одна схема;
- безопасность: http bearer и OAuth 2.0 client credentials; операция без аутентификации должна быть исключена
  (`exclude` в `config.php`) и написана в SDK руками;
- корневой `x-problem-codes` и `x-safe-to-retry` у операции.

**Отвергается:** `anyOf`, `discriminator`, `const`, `not`, `if`/`then`, `patternProperties`, `nullable`,
`readOnly`/`writeOnly`, внешние `$ref`, любые `x-` в схемах, cookie и массивы в запросе, другие заголовки,
параметры `body`, `options`, `args`, имена типов, совпадающие с глобальными типами TypeScript.

**Как расширить:** добавить конструкцию в `Spec\SpecValidator`, её перевод — в `Spec\ModelBuilder` и оба
эмиттера, пример — в `tests/fixtures/mini.yaml`, проверки — в `tests/EmitterTest.php` и `tests/ValidatorTest.php`.

## Имена

Без слоя переименований: метод — `operationId`, аргументы и ключи — имена параметров и свойств из спецификации,
типы — имена схем `components`. Тело или ответ, описанные на месте, получают имя `<OperationId>Body` и
`<OperationId>Response`, элемент страницы — `<OperationId>Item`. Для операции со страницей `{data, next_cursor}` и
параметром `cursor` генерируется ещё `<operationId>All()` — перебор всех элементов всех страниц.

## Что генерируется и что должно написать ядро SDK

**PHP** (`Loyal\Sdk\Generated`, PHP 8.2):

- `<Api>Types` — формы массивов PHPStan (`@phpstan-type`), их импортирует код интегратора;
- `<Api>Operations` — трейт с методами операций: `(путь..., $body, запрос..., ?RequestOptions $options)`;
- `<Api>Spec::OPERATIONS` — что нужно клиенту для отправки операции: `method`, `path` (шаблон), `body`
  (`none|required|optional`), `idempotency` (`none|required|optional`), `merchant` (отправлять `Loyal-Merchant`),
  `safe` (`x-safe-to-retry`), `statuses`, `content` (есть ли тело ответа), `paginated`;
- `ProblemCode` — константы кодов ошибок, `Version` — версии SDK и спецификаций.

Трейт вызывает методы `Loyal\Sdk\Internal\ClientBase`, который пишется руками:

```php
/** @param array<string, string> $path  @param array<string, scalar|null> $query (null — не отправлять) */
protected function call(string $operationId, array $path, array $query, ?array $body, ?RequestOptions $options): mixed;
/** @return \Generator<int, mixed, mixed, void> */
protected function paginate(string $operationId, array $path, array $query, ?RequestOptions $options): \Generator;
```

**TypeScript** (`src/generated`, только стираемый синтаксис — работает под удалением типов Node):

- `<api>-types.ts` — интерфейсы и типы;
- `<api>-operations.ts` — объект `<api>Operations` с теми же метаданными и класс `<Api>Operations extends
  ClientBase` с методами `(args, options?)`: в `args` — параметры пути и запроса по именам и `body`;
- `problem-codes.ts`, `version.ts`.

Класс вызывает методы `ClientBase` из `src/client-base.ts`, который пишется руками и экспортирует также
типы `OperationSpec` и `RequestOptions`:

```ts
protected call(operationId: string, operation: OperationSpec, path: Record<string, string>,
  query: Record<string, string | number | boolean | undefined>, body: unknown, options?: RequestOptions): Promise<unknown>;
protected paginate(operationId: string, operation: OperationSpec, path: Record<string, string>,
  query: Record<string, string | number | boolean | undefined>, options?: RequestOptions): AsyncGenerator<unknown, void, undefined>;
```
