diff --git a/AGENTS.md b/AGENTS.md index af830438..65458f9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,7 @@ change to a published package ships with a release entry (and vice versa). - A new MDX component must be registered in **three** places: `apps/docs/components/mdx/mdx-components.tsx`, `apps/docs/components/api/markdown-content.tsx`, and a handler in `apps/docs/lib/mdx-expander.ts` (else raw tags leak into generated `.md`/`llms-full.txt`). - Next.js middleware lives in `apps/docs/proxy.ts` (Next 16 renamed `middleware.ts` → `proxy.ts`; both present fails the build). - After changing `typespec.tsp`, always `npx turbo build --filter=@pachca/spec --force` — otherwise `openapi.yaml` stays stale. +- Snapshot regeneration for `packages/generator` is a bulk snapshot rewrite. Use `cd packages/generator && npm run regen-snapshots` (backed by top-level `scripts/regen-generator-snapshots.ts`) instead of ad-hoc `bun -e` one-offs or the old `bin/` / `tests/` helpers. - Restart the docs dev server after changing component registrations or new TS/TSX files (Turbopack HMR misses them). ## Deep-dive docs (read before the relevant task) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b9285ab7..fa390f5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,7 @@ Targeted regeneration: | OpenAPI only (after editing `typespec.tsp`) | `npx turbo build --filter=@pachca/spec --force` | | llms / skills / postman / per-page md | `cd apps/docs && bun run generate-llms` | | CLI commands | `cd packages/cli && bun run generate-cli` | +| Generator test snapshots | `cd packages/generator && npm run regen-snapshots` | | Everything (before commit) | `npx turbo build` | If the generator step fails with `Cannot find module @pachca/openapi-parser`: @@ -65,6 +66,10 @@ After a build, review the diff of regenerated files; a stale version sitting in a generated file means regeneration didn't run — debug it, don't hand-patch. +For `packages/generator` snapshot fixtures, use `npm run regen-snapshots` +(backed by top-level `scripts/regen-generator-snapshots.ts`) instead of +ad-hoc `bun -e` commands or the old `bin/` / `tests/` helpers. + ## Workflow 1. Edit source. diff --git a/apps/docs/data/releases.json b/apps/docs/data/releases.json index a6fda816..5a2175d7 100644 --- a/apps/docs/data/releases.json +++ b/apps/docs/data/releases.json @@ -1,4 +1,26 @@ [ + { + "product": "sdk", + "version": "1.0.20", + "date": "2026-06-02", + "changes": [ + { + "type": "~", + "description": "Go SDK: явные пустые массивы и объекты в optional request-полях теперь сериализуются как `[]` / `{}` и не теряются из-за `omitempty`" + } + ] + }, + { + "product": "generator", + "version": "1.1.5", + "date": "2026-06-02", + "changes": [ + { + "type": "~", + "description": "Go generator: для optional non-null array/map полей генерируется `MarshalJSON`, чтобы сохранять явные пустые контейнеры в JSON" + } + ] + }, { "product": "cli", "version": "2026.5.6", diff --git a/apps/docs/public/updates.md b/apps/docs/public/updates.md index 53d1fb8e..929b17c9 100644 --- a/apps/docs/public/updates.md +++ b/apps/docs/public/updates.md @@ -4,6 +4,18 @@ История изменений API Пачки, CLI, SDK и расширения для n8n. +## ☀️ Лето 2026 + +### 02 июня 2026 + +### SDK v1.0.20 + +- Go SDK: явные пустые массивы и объекты в optional request-полях теперь сериализуются как `[]` / `{}` и не теряются из-за `omitempty` + +### Generator v1.1.5 + +- Go generator: для optional non-null array/map полей генерируется `MarshalJSON`, чтобы сохранять явные пустые контейнеры в JSON + ## 🌷 Весна 2026 ### 21 мая 2026 @@ -433,184 +445,11 @@ _03 марта 2026_ Появился раздел документации [CLI](/guides/cli) — все методы API доступны как команды в терминале. -## ❄️ Зима 2025–26 - -### Полнотекстовый поиск, сообщение о недоступности и новые поля сообщений - -_27 февраля 2026_ - -Были добавлены новые методы для полнотекстового поиска по сотрудникам, чатам и сообщениям: - -- [Поиск сотрудников](GET /search/users) -- [Поиск чатов](GET /search/chats) -- [Поиск сообщений](GET /search/messages) - -С помощью этих методов вы можете искать по текстовому запросу с поддержкой фильтрации и курсорной пагинации. - -В модель статуса добавлено новое поле `away_message` — сообщение при режиме «Нет на месте», которое отображается в профиле пользователя и при отправке ему личного сообщения или упоминании в чате. - -В модель сообщения добавлены поля `root_chat_id` (идентификатор корневого чата для сообщений в тредах), `changed_at` (дата редактирования) и `deleted_at` (дата удаления). - -Были обновлены следующие методы: - -- [Новый статус](PUT /profile/status) -- [Новый статус сотрудника](PUT /users/{user_id}/status) -- [Отправить сообщение](POST /messages) -- [Информация о сообщении](GET /messages/{id}) -- [Редактирование сообщения](PUT /messages/{id}) -- [Список сообщений чата](GET /messages) - -### Режим «Нет на месте» и управление статусами - -_26 февраля 2026_ - -В модель статуса добавлено новое поле `is_away` — режим «Нет на месте». Также добавлены новые методы для администраторов и владельцев, позволяющие просматривать, устанавливать и удалять статус любого сотрудника. - -Были добавлены новые методы: - -- [Статус сотрудника](GET /users/{user_id}/status) -- [Новый статус сотрудника](PUT /users/{user_id}/status) -- [Удаление статуса сотрудника](DELETE /users/{user_id}/status) - -Были обновлены следующие методы: - -- [Текущий статус](GET /profile/status) -- [Новый статус](PUT /profile/status) - -### Привязка напоминаний к чатам - -_25 февраля 2026_ - -Напоминания теперь можно привязывать к чатам, в которых вы состоите — при создании напоминания укажите `chat_id`. В ответе всех методов напоминаний добавлено новое поле `chat_id`. - -Были обновлены следующие методы: - -- [Новое напоминание](POST /tasks) -- [Список напоминаний](GET /tasks) -- [Информация о напоминании](GET /tasks/{id}) -- [Редактирование напоминания](PUT /tasks/{id}) - -### Авторизация и скоупы - -_24 февраля 2026_ - -Добавлена поддержка OAuth 2.0 скоупов — теперь каждый метод API указывает, какой скоуп токена необходим для его вызова. Скоупы отображаются на странице каждого метода в виде бейджа. - -Был добавлен новый метод: - -- [Информация о токене](GET /oauth/token/info) — получение информации о текущем OAuth токене, включая список скоупов, дату создания и последнего использования - -Появился новый раздел документации — [Авторизация](/api/authorization). В нём описаны типы токенов, способы их создания, скоупы для персональных токенов и фиксированный набор скоупов для токенов ботов, а также ошибки авторизации. - -### Инструменты для тестирования API - -_22 февраля 2026_ - -Добавлен онлайн-клиент **Scalar** — тестирование всех методов API прямо в браузере без установки. Также доступна готовая коллекция запросов для **Postman** и **Bruno** с примерами и настроенной авторизацией. - -Подробнее в разделе [Тестирование API](/api/requests-responses#testirovanie-api). - -### Ресурсы для AI-агентов - -_21 февраля 2026_ - -Появился новый раздел документации — [AI агенты](/guides/ai-agents). В нём описано, как Пачка работает с AI-агентами, и собраны все ресурсы для интеграции. - -Что нового: - -- **Agent Skills** — описание API в формате skill-файлов для подключения к 40+ AI-агентам (Claude Code, Cursor, Codex, Windsurf и др.) -- **Context7 MCP** — документация Пачки доступна через MCP-сервер Context7 для агентов с поддержкой MCP -- **OpenAPI-спецификация** — спецификация для кодогенерации и автоматического создания клиентов - -### Аудит-события сообщений, тредов и реакций - -_20 февраля 2026_ - -В журнал аудита событий добавлены новые типы событий для отслеживания создания сообщений, тредов и работы с реакциями: - -- `message_created` — сообщение создано -- `thread_created` — тред создан -- `reaction_created` — реакция добавлена -- `reaction_deleted` — реакция удалена - -Полный список типов событий доступен на странице [Журнал аудита событий](/guides/audit-events). - -Был обновлен следующий метод: - -- [Журнал аудита событий](GET /audit_events) - -### Аудит-события безопасности и токенов - -_19 февраля 2026_ - -В журнал аудита событий были добавлены новые типы событий: - -- `user_2fa_fail` — неудачная попытка двухфакторной аутентификации -- `user_2fa_success` — успешная двухфакторная аутентификация -- `access_token_created` — создан новый токен доступа -- `access_token_updated` — токен доступа обновлен -- `access_token_destroy` — токен доступа удален -- `kms_encrypt` — данные зашифрованы -- `kms_decrypt` — данные расшифрованы - -Полный список типов событий доступен на странице [Журнал аудита событий](/guides/audit-events). - -Был обновлен следующий метод: - -- [Журнал аудита событий](GET /audit_events) - -### Курсорная пагинация - -_16 февраля 2026_ - -Во все методы получения списков была добавлена поддержка курсорной пагинации. Теперь вы можете использовать параметры `cursor` и `limit` для постраничного получения данных. Прежние параметры `page` и `per` продолжают работать для обратной совместимости. - -Были обновлены следующие методы: - -- [Список тегов сотрудников](GET /group_tags) -- [Список сотрудников тега](GET /group_tags/{id}/users) -- [Список сообщений](GET /messages) -- [Список прочитавших сообщение](GET /messages/{id}/read_member_ids) -- [Список реакций на сообщение](GET /messages/{id}/reactions) -- [Список напоминаний](GET /tasks) - -### Добавление участников в тред - -_12 февраля 2026_ - -Метод [Добавление пользователей](POST /chats/{id}/members) теперь поддерживает работу с тредами. Вы можете передать `chat_id` треда в качестве идентификатора чата, чтобы добавить участников в тред. - -### Управление напоминаниями - -_09 февраля 2026_ - -Были добавлены новые методы: - -- [Список напоминаний](GET /tasks) -- [Информация о напоминании](GET /tasks/{id}) -- [Редактирование напоминания](PUT /tasks/{id}) -- [Удаление напоминания](DELETE /tasks/{id}) - -С помощью этих методов вы можете получать список напоминаний, просматривать, редактировать и удалять их. - -### Фильтрация тегов по названию - -_29 января 2026_ - -В запрос списка тегов было добавлено новое поле: - -- `names` - -С помощью этого поля вы можете отфильтровать список тегов по их названиям. - -Был обновлен следующий метод: - -- [Список тегов сотрудников](GET /group_tags) - ## Более ранние обновления Выше — последние 2 сезона. Остальные обновления доступны по сезонам: +- [❄️ Зима 2025–26](https://dev.pachca.com/updates/season/winter-2025-26.md) - [🍁 Осень 2025](https://dev.pachca.com/updates/season/autumn-2025.md) - [☀️ Лето 2025](https://dev.pachca.com/updates/season/summer-2025.md) - [🌷 Весна 2025](https://dev.pachca.com/updates/season/spring-2025.md) diff --git a/apps/docs/public/updates/2026-06-02.md b/apps/docs/public/updates/2026-06-02.md new file mode 100644 index 00000000..04eb82d3 --- /dev/null +++ b/apps/docs/public/updates/2026-06-02.md @@ -0,0 +1,13 @@ +> Это Markdown-версия конкретной страницы. Для контекста за её пределами (правила API, полный перечень методов, авторизация) ОБЯЗАТЕЛЬНО открой [llms.txt](https://dev.pachca.com/llms.txt) перед ответом — это сэкономит токены и предотвратит неполный ответ. + +# 02 июня 2026 + +_02 июня 2026_ + +### SDK v1.0.20 + +- Go SDK: явные пустые массивы и объекты в optional request-полях теперь сериализуются как `[]` / `{}` и не теряются из-за `omitempty` + +### Generator v1.1.5 + +- Go generator: для optional non-null array/map полей генерируется `MarshalJSON`, чтобы сохранять явные пустые контейнеры в JSON diff --git a/apps/docs/public/updates/season/summer-2026.md b/apps/docs/public/updates/season/summer-2026.md new file mode 100644 index 00000000..98c35e96 --- /dev/null +++ b/apps/docs/public/updates/season/summer-2026.md @@ -0,0 +1,13 @@ +> Это Markdown-версия конкретной страницы. Для контекста за её пределами (правила API, полный перечень методов, авторизация) ОБЯЗАТЕЛЬНО открой [llms.txt](https://dev.pachca.com/llms.txt) перед ответом — это сэкономит токены и предотвратит неполный ответ. + +# ☀️ Лето 2026 + +### 02 июня 2026 + +### SDK v1.0.20 + +- Go SDK: явные пустые массивы и объекты в optional request-полях теперь сериализуются как `[]` / `{}` и не теряются из-за `omitempty` + +### Generator v1.1.5 + +- Go generator: для optional non-null array/map полей генерируется `MarshalJSON`, чтобы сохранять явные пустые контейнеры в JSON diff --git a/apps/docs/scripts/skills/generate.ts b/apps/docs/scripts/skills/generate.ts index 11fecf86..31f3f6ff 100644 --- a/apps/docs/scripts/skills/generate.ts +++ b/apps/docs/scripts/skills/generate.ts @@ -813,6 +813,7 @@ change to a published package ships with a release entry (and vice versa). - A new MDX component must be registered in **three** places: \`apps/docs/components/mdx/mdx-components.tsx\`, \`apps/docs/components/api/markdown-content.tsx\`, and a handler in \`apps/docs/lib/mdx-expander.ts\` (else raw tags leak into generated \`.md\`/\`llms-full.txt\`). - Next.js middleware lives in \`apps/docs/proxy.ts\` (Next 16 renamed \`middleware.ts\` → \`proxy.ts\`; both present fails the build). - After changing \`typespec.tsp\`, always \`npx turbo build --filter=@pachca/spec --force\` — otherwise \`openapi.yaml\` stays stale. +- Snapshot regeneration for \`packages/generator\` is a bulk snapshot rewrite. Use \`cd packages/generator && npm run regen-snapshots\` (backed by top-level \`scripts/regen-generator-snapshots.ts\`) instead of ad-hoc \`bun -e\` one-offs or the old \`bin/\` / \`tests/\` helpers. - Restart the docs dev server after changing component registrations or new TS/TSX files (Turbopack HMR misses them). ## Deep-dive docs (read before the relevant task) diff --git a/packages/generator/README.md b/packages/generator/README.md index 5c309414..cb4c2ec9 100644 --- a/packages/generator/README.md +++ b/packages/generator/README.md @@ -92,5 +92,9 @@ Snapshot-тесты сравнивают сгенерированный код Для обновления snapshot'ов после изменения генераторов: ```bash -bun -e "import { generate } from './src/index.ts'; generate('tests/crud/fixture.yaml', 'tests/crud/snapshots', ['typescript', 'python', 'go', 'kotlin', 'swift', 'csharp']);" +npm run regen-snapshots ``` + +Команда вызывает верхнеуровневый `scripts/regen-generator-snapshots.ts`, поэтому не нужно использовать ad-hoc `bun -e` команды или старые `bin/` / `tests/` helper-скрипты напрямую. + +Для AI-агентов это считается массовой перезаписью snapshot'ов: агент должен попросить пользователя запустить команду самостоятельно, а не выполнять её сам. diff --git a/packages/generator/bin/regen-snapshots.ts b/packages/generator/bin/regen-snapshots.ts deleted file mode 100644 index d21f6e2c..00000000 --- a/packages/generator/bin/regen-snapshots.ts +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env node - -/** - * Regenerate all test snapshots by running the generator against each - * fixture and writing directly into the snapshots/ directory. - * - * Usage: tsx bin/regen-snapshots.ts - */ - -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { generate, SUPPORTED_LANGS } from '../src/index.js'; - -const TESTS_DIR = path.resolve(import.meta.dirname, '..', 'tests'); -const suites = fs - .readdirSync(TESTS_DIR) - .filter((d) => { - const full = path.join(TESTS_DIR, d); - return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'fixture.yaml')); - }) - .sort(); - -for (const suite of suites) { - const specPath = path.join(TESTS_DIR, suite, 'fixture.yaml'); - const snapshotsDir = path.join(TESTS_DIR, suite, 'snapshots'); - generate(specPath, snapshotsDir, SUPPORTED_LANGS, { examples: true }); - console.log(` ✓ ${suite}`); -} - -console.log(`\nRegenerated ${suites.length} suites.`); diff --git a/packages/generator/package.json b/packages/generator/package.json index 1552b3aa..a3414455 100644 --- a/packages/generator/package.json +++ b/packages/generator/package.json @@ -22,7 +22,8 @@ "scripts": { "build": "bun build src/index.ts bin/generator.ts --outdir dist --target node --format esm --splitting --packages=bundle", "test": "vitest run", - "update-snapshots": "bun tests/update-snapshots.ts", + "regen-snapshots": "bun ../../scripts/regen-generator-snapshots.ts", + "update-snapshots": "bun ../../scripts/regen-generator-snapshots.ts", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/generator/src/lang/go.ts b/packages/generator/src/lang/go.ts index 8529edaf..d372c77a 100644 --- a/packages/generator/src/lang/go.ts +++ b/packages/generator/src/lang/go.ts @@ -154,6 +154,31 @@ function emitModel(lines: string[], m: IRModel, allModels: IRModel[]): void { for (const line of goAligned(rows)) lines.push(line); lines.push('}'); + const optionalContainers = m.fields.filter( + (f) => isOptionalField(f) && (f.type.kind === 'array' || f.type.kind === 'record') && !f.nullable, + ); + if (optionalContainers.length > 0) { + lines.push(''); + lines.push(`func (m ${m.name}) MarshalJSON() ([]byte, error) {`); + lines.push(`\ttype Alias ${m.name}`); + lines.push('\tdata, err := json.Marshal(Alias(m))'); + lines.push('\tif err != nil {'); + lines.push('\t\treturn nil, err'); + lines.push('\t}'); + lines.push('\tvar raw map[string]any'); + lines.push('\tif err := json.Unmarshal(data, &raw); err != nil {'); + lines.push('\t\treturn nil, err'); + lines.push('\t}'); + for (const field of optionalContainers) { + const goName = goExportName(field.name); + lines.push(`\tif m.${goName} != nil {`); + lines.push(`\t\traw[${JSON.stringify(field.name)}] = m.${goName}`); + lines.push('\t}'); + } + lines.push('\treturn json.Marshal(raw)'); + lines.push('}'); + } + if (m.name === 'ApiError') { // Find the items model for the errors array to determine which field to use const errorsField = m.fields.find((f) => f.name === 'errors'); @@ -285,9 +310,12 @@ function generateTypes(ir: IR): string { const needFmtStrings = ir.models.some((m) => m.name === 'ApiError'); const needIO = ir.models.some((m) => m.fields.some((f) => f.type.kind === 'binary')); const hasUnions = ir.unions.length > 0; + const hasOptionalContainers = ir.models.some((m) => m.fields.some( + (f) => isOptionalField(f) && (f.type.kind === 'array' || f.type.kind === 'record') && !f.nullable, + )); const imports: string[] = []; - if (hasUnions) imports.push('"encoding/json"'); + if (hasUnions || hasOptionalContainers) imports.push('"encoding/json"'); if (needFmtStrings || hasUnions) imports.push('"fmt"'); if (needIO) imports.push('"io"'); if (needFmtStrings) imports.push('"strings"'); diff --git a/packages/generator/tests/additional-props-bool/snapshots/go/types.go b/packages/generator/tests/additional-props-bool/snapshots/go/types.go index 285ba83e..c260ec9f 100644 --- a/packages/generator/tests/additional-props-bool/snapshots/go/types.go +++ b/packages/generator/tests/additional-props-bool/snapshots/go/types.go @@ -1,5 +1,9 @@ package pachca +import ( + "encoding/json" +) + type Metadata struct { } @@ -8,3 +12,19 @@ type Event struct { Type string `json:"type"` Metadata map[string]any `json:"metadata,omitempty"` } + +func (m Event) MarshalJSON() ([]byte, error) { + type Alias Event + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Metadata != nil { + raw["metadata"] = m.Metadata + } + return json.Marshal(raw) +} diff --git a/packages/generator/tests/circular-ref/snapshots/go/types.go b/packages/generator/tests/circular-ref/snapshots/go/types.go index fadaff36..a23f761f 100644 --- a/packages/generator/tests/circular-ref/snapshots/go/types.go +++ b/packages/generator/tests/circular-ref/snapshots/go/types.go @@ -1,5 +1,9 @@ package pachca +import ( + "encoding/json" +) + type Category struct { ID int32 `json:"id"` Name string `json:"name"` @@ -7,6 +11,22 @@ type Category struct { Children []Category `json:"children,omitempty"` } +func (m Category) MarshalJSON() ([]byte, error) { + type Alias Category + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Children != nil { + raw["children"] = m.Children + } + return json.Marshal(raw) +} + type TreeNode struct { Value string `json:"value"` Left *TreeNode `json:"left,omitempty"` diff --git a/packages/generator/tests/crud/snapshots/go/types.go b/packages/generator/tests/crud/snapshots/go/types.go index ed6c5b51..d08f9d1d 100644 --- a/packages/generator/tests/crud/snapshots/go/types.go +++ b/packages/generator/tests/crud/snapshots/go/types.go @@ -1,6 +1,7 @@ package pachca import ( + "encoding/json" "fmt" "strings" "time" @@ -29,6 +30,22 @@ type Chat struct { MemberIDs []int32 `json:"member_ids,omitempty"` } +func (m Chat) MarshalJSON() ([]byte, error) { + type Alias Chat + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.MemberIDs != nil { + raw["member_ids"] = m.MemberIDs + } + return json.Marshal(raw) +} + type ChatCreateRequestChat struct { Name string `json:"name"` Channel *bool `json:"channel,omitempty"` @@ -36,6 +53,22 @@ type ChatCreateRequestChat struct { MemberIDs []int32 `json:"member_ids,omitempty"` } +func (m ChatCreateRequestChat) MarshalJSON() ([]byte, error) { + type Alias ChatCreateRequestChat + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.MemberIDs != nil { + raw["member_ids"] = m.MemberIDs + } + return json.Marshal(raw) +} + type ChatCreateRequest struct { Chat ChatCreateRequestChat `json:"chat"` } @@ -58,6 +91,22 @@ type ApiError struct { Errors []ApiErrorItem `json:"errors,omitempty"` } +func (m ApiError) MarshalJSON() ([]byte, error) { + type Alias ApiError + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Errors != nil { + raw["errors"] = m.Errors + } + return json.Marshal(raw) +} + func (e *ApiError) Error() string { if len(e.Errors) == 0 { return "api error" diff --git a/packages/generator/tests/models/snapshots/go/types.go b/packages/generator/tests/models/snapshots/go/types.go index 28b5e409..52041621 100644 --- a/packages/generator/tests/models/snapshots/go/types.go +++ b/packages/generator/tests/models/snapshots/go/types.go @@ -1,6 +1,7 @@ package pachca import ( + "encoding/json" "fmt" "strings" "time" @@ -31,6 +32,22 @@ type User struct { Status *UserStatus `json:"status,omitempty"` } +func (m User) MarshalJSON() ([]byte, error) { + type Alias User + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.CustomProperties != nil { + raw["custom_properties"] = m.CustomProperties + } + return json.Marshal(raw) +} + type UserStatus struct { Emoji *string `json:"emoji,omitempty"` Title *string `json:"title,omitempty"` @@ -87,6 +104,25 @@ type MessageCreateRequestMessage struct { Buttons [][]MessageCreateRequestButton `json:"buttons,omitempty"` } +func (m MessageCreateRequestMessage) MarshalJSON() ([]byte, error) { + type Alias MessageCreateRequestMessage + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Files != nil { + raw["files"] = m.Files + } + if m.Buttons != nil { + raw["buttons"] = m.Buttons + } + return json.Marshal(raw) +} + type MessageCreateRequest struct { Message MessageCreateRequestMessage `json:"message"` } @@ -100,6 +136,22 @@ type ApiError struct { Errors []ApiErrorItem `json:"errors,omitempty"` } +func (m ApiError) MarshalJSON() ([]byte, error) { + type Alias ApiError + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Errors != nil { + raw["errors"] = m.Errors + } + return json.Marshal(raw) +} + func (e *ApiError) Error() string { if len(e.Errors) == 0 { return "api error" diff --git a/packages/generator/tests/patch/snapshots/go/types.go b/packages/generator/tests/patch/snapshots/go/types.go index 9c897741..805c31c5 100644 --- a/packages/generator/tests/patch/snapshots/go/types.go +++ b/packages/generator/tests/patch/snapshots/go/types.go @@ -1,6 +1,7 @@ package pachca import ( + "encoding/json" "fmt" "strings" ) @@ -31,6 +32,22 @@ type ApiError struct { Errors []ApiErrorItem `json:"errors,omitempty"` } +func (m ApiError) MarshalJSON() ([]byte, error) { + type Alias ApiError + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Errors != nil { + raw["errors"] = m.Errors + } + return json.Marshal(raw) +} + func (e *ApiError) Error() string { if len(e.Errors) == 0 { return "api error" diff --git a/packages/generator/tests/record/snapshots/go/types.go b/packages/generator/tests/record/snapshots/go/types.go index 05f774b9..e2062a81 100644 --- a/packages/generator/tests/record/snapshots/go/types.go +++ b/packages/generator/tests/record/snapshots/go/types.go @@ -1,6 +1,7 @@ package pachca import ( + "encoding/json" "fmt" "strings" ) @@ -24,6 +25,22 @@ type ApiError struct { Errors []ApiErrorItem `json:"errors,omitempty"` } +func (m ApiError) MarshalJSON() ([]byte, error) { + type Alias ApiError + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Errors != nil { + raw["errors"] = m.Errors + } + return json.Marshal(raw) +} + func (e *ApiError) Error() string { if len(e.Errors) == 0 { return "api error" diff --git a/packages/generator/tests/redirect/snapshots/go/types.go b/packages/generator/tests/redirect/snapshots/go/types.go index ed2034cc..f77d173d 100644 --- a/packages/generator/tests/redirect/snapshots/go/types.go +++ b/packages/generator/tests/redirect/snapshots/go/types.go @@ -1,6 +1,7 @@ package pachca import ( + "encoding/json" "fmt" "strings" ) @@ -14,6 +15,22 @@ type ApiError struct { Errors []ApiErrorItem `json:"errors,omitempty"` } +func (m ApiError) MarshalJSON() ([]byte, error) { + type Alias ApiError + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Errors != nil { + raw["errors"] = m.Errors + } + return json.Marshal(raw) +} + func (e *ApiError) Error() string { if len(e.Errors) == 0 { return "api error" diff --git a/packages/generator/tests/unwrap/snapshots/go/types.go b/packages/generator/tests/unwrap/snapshots/go/types.go index 31d305f4..5ddbafcb 100644 --- a/packages/generator/tests/unwrap/snapshots/go/types.go +++ b/packages/generator/tests/unwrap/snapshots/go/types.go @@ -1,6 +1,7 @@ package pachca import ( + "encoding/json" "fmt" "strings" "time" @@ -17,6 +18,22 @@ type ChatCreateRequestChat struct { MemberIDs []int32 `json:"member_ids,omitempty"` } +func (m ChatCreateRequestChat) MarshalJSON() ([]byte, error) { + type Alias ChatCreateRequestChat + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.MemberIDs != nil { + raw["member_ids"] = m.MemberIDs + } + return json.Marshal(raw) +} + type ChatCreateRequest struct { Chat ChatCreateRequestChat `json:"chat"` } @@ -38,6 +55,22 @@ type ApiError struct { Errors []ApiErrorItem `json:"errors,omitempty"` } +func (m ApiError) MarshalJSON() ([]byte, error) { + type Alias ApiError + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Errors != nil { + raw["errors"] = m.Errors + } + return json.Marshal(raw) +} + func (e *ApiError) Error() string { if len(e.Errors) == 0 { return "api error" diff --git a/packages/generator/tests/update-snapshots.ts b/packages/generator/tests/update-snapshots.ts deleted file mode 100644 index d744f4c9..00000000 --- a/packages/generator/tests/update-snapshots.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { generate, SUPPORTED_LANGS } from '../src/index.js'; - -const testsDir = path.resolve(import.meta.dirname); -const suites = fs.readdirSync(testsDir).filter((d) => { - const full = path.join(testsDir, d); - return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'fixture.yaml')); -}); - -for (const suite of suites) { - const snapshotsDir = path.join(testsDir, suite, 'snapshots'); - fs.rmSync(snapshotsDir, { recursive: true, force: true }); - generate(path.join(testsDir, suite, 'fixture.yaml'), snapshotsDir, SUPPORTED_LANGS, { examples: true }); - console.log(`Updated: ${suite}`); -} diff --git a/scripts/regen-generator-snapshots.ts b/scripts/regen-generator-snapshots.ts new file mode 100644 index 00000000..eef41be1 --- /dev/null +++ b/scripts/regen-generator-snapshots.ts @@ -0,0 +1,19 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { generate, SUPPORTED_LANGS } from '../packages/generator/src/index.js'; + +const testsDir = path.resolve(import.meta.dirname, '..', 'packages', 'generator', 'tests'); +const suites = fs + .readdirSync(testsDir) + .filter((d) => { + const full = path.join(testsDir, d); + return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'fixture.yaml')); + }) + .sort(); + +for (const suite of suites) { + const snapshotsDir = path.join(testsDir, suite, 'snapshots'); + fs.rmSync(snapshotsDir, { recursive: true, force: true }); + generate(path.join(testsDir, suite, 'fixture.yaml'), snapshotsDir, SUPPORTED_LANGS, { examples: true }); + console.log(`Updated: ${suite}`); +} diff --git a/sdk/go/generated/types.go b/sdk/go/generated/types.go index 74049401..d1c23e3c 100644 --- a/sdk/go/generated/types.go +++ b/sdk/go/generated/types.go @@ -512,6 +512,25 @@ type ChatCreateRequestChat struct { Public *bool `json:"public,omitempty"` } +func (m ChatCreateRequestChat) MarshalJSON() ([]byte, error) { + type Alias ChatCreateRequestChat + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.MemberIDs != nil { + raw["member_ids"] = m.MemberIDs + } + if m.GroupTagIDs != nil { + raw["group_tag_ids"] = m.GroupTagIDs + } + return json.Marshal(raw) +} + type ChatCreateRequest struct { Chat ChatCreateRequestChat `json:"chat"` } @@ -564,6 +583,22 @@ type ExportRequest struct { SkipChatsFile *bool `json:"skip_chats_file,omitempty"` } +func (m ExportRequest) MarshalJSON() ([]byte, error) { + type Alias ExportRequest + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.ChatIDs != nil { + raw["chat_ids"] = m.ChatIDs + } + return json.Marshal(raw) +} + type File struct { ID int32 `json:"id"` Key string `json:"key"` @@ -685,6 +720,25 @@ type MessageCreateRequestMessage struct { SkipInviteMentions *bool `json:"skip_invite_mentions,omitempty"` } +func (m MessageCreateRequestMessage) MarshalJSON() ([]byte, error) { + type Alias MessageCreateRequestMessage + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Files != nil { + raw["files"] = m.Files + } + if m.Buttons != nil { + raw["buttons"] = m.Buttons + } + return json.Marshal(raw) +} + type MessageCreateRequest struct { Message MessageCreateRequestMessage `json:"message"` LinkPreview *bool `json:"link_preview,omitempty"` @@ -707,6 +761,25 @@ type MessageUpdateRequestMessage struct { DisplayName *string `json:"display_name,omitempty"` } +func (m MessageUpdateRequestMessage) MarshalJSON() ([]byte, error) { + type Alias MessageUpdateRequestMessage + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Files != nil { + raw["files"] = m.Files + } + if m.Buttons != nil { + raw["buttons"] = m.Buttons + } + return json.Marshal(raw) +} + type MessageUpdateRequest struct { Message MessageUpdateRequestMessage `json:"message"` } @@ -841,6 +914,25 @@ type TaskCreateRequestTask struct { CustomProperties []TaskCreateRequestCustomProperty `json:"custom_properties,omitempty"` } +func (m TaskCreateRequestTask) MarshalJSON() ([]byte, error) { + type Alias TaskCreateRequestTask + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.PerformerIDs != nil { + raw["performer_ids"] = m.PerformerIDs + } + if m.CustomProperties != nil { + raw["custom_properties"] = m.CustomProperties + } + return json.Marshal(raw) +} + type TaskCreateRequest struct { Task TaskCreateRequestTask `json:"task"` } @@ -862,6 +954,25 @@ type TaskUpdateRequestTask struct { CustomProperties []TaskUpdateRequestCustomProperty `json:"custom_properties,omitempty"` } +func (m TaskUpdateRequestTask) MarshalJSON() ([]byte, error) { + type Alias TaskUpdateRequestTask + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.PerformerIDs != nil { + raw["performer_ids"] = m.PerformerIDs + } + if m.CustomProperties != nil { + raw["custom_properties"] = m.CustomProperties + } + return json.Marshal(raw) +} + type TaskUpdateRequest struct { Task TaskUpdateRequestTask `json:"task"` } @@ -934,6 +1045,28 @@ type UserCreateRequestUser struct { CustomProperties []UserCreateRequestCustomProperty `json:"custom_properties,omitempty"` } +func (m UserCreateRequestUser) MarshalJSON() ([]byte, error) { + type Alias UserCreateRequestUser + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.ListTags != nil { + raw["list_tags"] = m.ListTags + } + if m.ChatIDs != nil { + raw["chat_ids"] = m.ChatIDs + } + if m.CustomProperties != nil { + raw["custom_properties"] = m.CustomProperties + } + return json.Marshal(raw) +} + type UserCreateRequest struct { User UserCreateRequestUser `json:"user"` SkipEmailNotify *bool `json:"skip_email_notify,omitempty"` @@ -970,6 +1103,25 @@ type UserUpdateRequestUser struct { CustomProperties []UserUpdateRequestCustomProperty `json:"custom_properties,omitempty"` } +func (m UserUpdateRequestUser) MarshalJSON() ([]byte, error) { + type Alias UserUpdateRequestUser + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.ListTags != nil { + raw["list_tags"] = m.ListTags + } + if m.CustomProperties != nil { + raw["custom_properties"] = m.CustomProperties + } + return json.Marshal(raw) +} + type UserUpdateRequest struct { User UserUpdateRequestUser `json:"user"` } @@ -991,6 +1143,22 @@ type ViewBlockCheckbox struct { Hint *string `json:"hint,omitempty"` } +func (m ViewBlockCheckbox) MarshalJSON() ([]byte, error) { + type Alias ViewBlockCheckbox + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Options != nil { + raw["options"] = m.Options + } + return json.Marshal(raw) +} + type ViewBlockCheckboxOption struct { Text string `json:"text"` Value string `json:"value"` @@ -1021,6 +1189,22 @@ type ViewBlockFileInput struct { Hint *string `json:"hint,omitempty"` } +func (m ViewBlockFileInput) MarshalJSON() ([]byte, error) { + type Alias ViewBlockFileInput + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Filetypes != nil { + raw["filetypes"] = m.Filetypes + } + return json.Marshal(raw) +} + type ViewBlockHeader struct { Type string `json:"type"` // always "header" Text string `json:"text"` @@ -1058,6 +1242,22 @@ type ViewBlockRadio struct { Hint *string `json:"hint,omitempty"` } +func (m ViewBlockRadio) MarshalJSON() ([]byte, error) { + type Alias ViewBlockRadio + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Options != nil { + raw["options"] = m.Options + } + return json.Marshal(raw) +} + type ViewBlockSelect struct { Type string `json:"type"` // always "select" Name string `json:"name"` @@ -1067,6 +1267,22 @@ type ViewBlockSelect struct { Hint *string `json:"hint,omitempty"` } +func (m ViewBlockSelect) MarshalJSON() ([]byte, error) { + type Alias ViewBlockSelect + data, err := json.Marshal(Alias(m)) + if err != nil { + return nil, err + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if m.Options != nil { + raw["options"] = m.Options + } + return json.Marshal(raw) +} + type ViewBlockSelectableOption struct { Text string `json:"text"` Value string `json:"value"`