From 26e13db80aa38b478a8c77f23d6f1035879396a8 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sat, 31 Oct 2026 00:47:08 -0400 Subject: [PATCH 1/6] feat(codegen): JSON Schema and OpenAPI contract emitters (FR-022) Add `contract-schema` (JSON Schema 2020-12, canonical and strict profiles) and `openapi` (OpenAPI 3.1) as ordinary neutral generators in the catalog, over one shared engine in codegen-ts/src/contract. Both carry an owned reference copy, so `meta eject` works for them. The `contract-conformance` corpus holds the golden trees and the error cases. Ajv and the OpenAPI schema validator check every emitted document, and oasdiff runs against the documents in a test. The generated TypeScript API lanes now check real traffic against the document, which found two defects: writes and filters send zoneless timestamps that `date-time` refused, and the report case was missing its `format` parameter. Other ports share the REST contract but are not run against the document. Add docs/features/contract-emitters.md and the recipe that checks a change against the last release with oasdiff. Settle the open questions in the FR-022 design and mark it accepted. Target 1.2.1 on the roadmap; protobuf stays under 1.3 with binary protocols. --- AGENTS.md | 2 +- CHANGELOG.md | 10 + README.md | 6 +- .../skills/metaobjects-codegen/SKILL.md | 9 +- .../references/typescript.md | 10 +- .../references/capability-sheet.md | 17 +- bun.lock | 31 +- docs/CONFORMANCE.md | 5 +- docs/README.md | 2 + docs/features/cli.md | 23 + docs/features/codegen-concepts.md | 4 +- docs/features/contract-emitters.md | 225 + docs/features/own-your-codegen.md | 6 +- docs/llms/llms-full.txt | 2 +- docs/llms/llms.txt | 2 +- .../recipes/check-api-against-last-release.md | 78 + .../generators/typescript/json-schema.ts | 5 +- docs/recipes/generators/typescript/openapi.ts | 5 +- docs/recipes/write-your-own-generator.md | 16 +- ...6-06-11-fr-022-contract-emitters-design.md | 137 +- examples/showcase/site-payload.json | 2 +- .../skills/metaobjects-codegen/SKILL.md | 9 +- .../skills/metaobjects-codegen/SKILL.md | 9 +- .../skills/metaobjects-codegen/SKILL.md | 9 +- .../skills/metaobjects-codegen/SKILL.md | 9 +- .../references/typescript.md | 10 +- .../skills/metaobjects-codegen/SKILL.md | 9 +- .../references/typescript.md | 10 +- fixtures/contract-conformance/README.md | 44 + .../all-fields-wire-string-emit/case.json | 1 + .../expected/openapi.json | 2346 ++++++ .../acme/contract/Address.create.schema.json | 29 + .../schemas/acme/contract/Address.schema.json | 32 + .../acme/contract/Address.strict.schema.json | 31 + .../contract/AllFields.create.schema.json | 132 + .../acme/contract/AllFields.schema.json | 247 + .../contract/AllFields.strict.schema.json | 244 + .../contract/AllFields.update.schema.json | 184 + .../acme/contract/Bag.create.schema.json | 25 + .../schemas/acme/contract/Bag.schema.json | 49 + .../acme/contract/Bag.update.schema.json | 38 + .../acme/contract/Tree.create.schema.json | 21 + .../schemas/acme/contract/Tree.schema.json | 24 + .../all-fields-wire-string-omit/case.json | 1 + .../expected/openapi.json | 2334 ++++++ .../acme/contract/Address.create.schema.json | 29 + .../schemas/acme/contract/Address.schema.json | 24 + .../acme/contract/Address.strict.schema.json | 31 + .../contract/AllFields.create.schema.json | 132 + .../acme/contract/AllFields.schema.json | 247 + .../contract/AllFields.strict.schema.json | 244 + .../contract/AllFields.update.schema.json | 184 + .../acme/contract/Bag.create.schema.json | 25 + .../schemas/acme/contract/Bag.schema.json | 49 + .../acme/contract/Bag.update.schema.json | 38 + .../acme/contract/Tree.create.schema.json | 21 + .../schemas/acme/contract/Tree.schema.json | 20 + .../contract-conformance/all-fields/case.json | 1 + .../all-fields/expected/openapi.json | 2345 ++++++ .../acme/contract/Address.create.schema.json | 29 + .../schemas/acme/contract/Address.schema.json | 31 + .../acme/contract/Address.strict.schema.json | 31 + .../contract/AllFields.create.schema.json | 132 + .../acme/contract/AllFields.schema.json | 248 + .../contract/AllFields.strict.schema.json | 244 + .../contract/AllFields.update.schema.json | 184 + .../acme/contract/Bag.create.schema.json | 25 + .../schemas/acme/contract/Bag.schema.json | 49 + .../acme/contract/Bag.update.schema.json | 38 + .../acme/contract/Tree.create.schema.json | 21 + .../schemas/acme/contract/Tree.schema.json | 23 + .../contract-conformance/all-fields/meta.json | 81 + fixtures/contract-conformance/blog/case.json | 1 + .../blog/expected/openapi.json | 724 ++ .../acme/blog/Author.create.schema.json | 30 + .../schemas/acme/blog/Author.schema.json | 57 + .../acme/blog/Author.strict.schema.json | 35 + .../acme/blog/Author.update.schema.json | 25 + .../contract-conformance/decimal/case.json | 1 + .../decimal/expected/openapi.json | 1893 +++++ .../acme/ledger/Entry.create.schema.json | 28 + .../schemas/acme/ledger/Entry.schema.json | 33 + .../acme/ledger/Entry.strict.schema.json | 26 + .../acme/ledger/Entry.update.schema.json | 25 + .../acme/ledger/EntryTotals.schema.json | 73 + .../ledger/EntryTotals.strict.schema.json | 63 + .../acme/ledger/Sample.create.schema.json | 27 + .../schemas/acme/ledger/Sample.schema.json | 38 + .../acme/ledger/Sample.strict.schema.json | 28 + .../acme/ledger/Sample.update.schema.json | 31 + .../expected-error.txt | 1 + .../error-name-collision-list/meta.json | 30 + .../error-name-collision/expected-error.txt | 1 + .../error-name-collision/meta.json | 19 + .../error-strict-map/expected-error.txt | 1 + .../error-strict-map/meta.json | 11 + .../error-strict-recursive/expected-error.txt | 1 + .../error-strict-recursive/meta.json | 14 + fixtures/contract-conformance/jsonb/case.json | 1 + .../jsonb/expected/openapi.json | 918 ++ .../acme/store/Document.create.schema.json | 51 + .../schemas/acme/store/Document.schema.json | 100 + .../acme/store/Document.update.schema.json | 84 + .../acme/store/Marker.create.schema.json | 20 + .../schemas/acme/store/Marker.schema.json | 22 + .../acme/store/Marker.strict.schema.json | 21 + .../acme/store/Measure.create.schema.json | 27 + .../schemas/acme/store/Measure.schema.json | 33 + .../acme/store/Measure.strict.schema.json | 34 + .../acme/store/Pricing.create.schema.json | 14 + .../schemas/acme/store/Pricing.schema.json | 17 + .../acme/store/Telemetry.create.schema.json | 25 + .../schemas/acme/store/Telemetry.schema.json | 27 + .../acme/store/Telemetry.strict.schema.json | 25 + .../acme/store/Tier.create.schema.json | 16 + .../schemas/acme/store/Tier.schema.json | 16 + .../acme/store/Tier.strict.schema.json | 17 + fixtures/contract-conformance/m2m/case.json | 1 + .../m2m/expected/openapi.json | 5982 +++++++++++++ .../schemas/acme/social/Account.schema.json | 13 + .../acme/social/AccountTag.create.schema.json | 20 + .../acme/social/AccountTag.schema.json | 20 + .../acme/social/AccountTag.strict.schema.json | 17 + .../acme/social/AccountTag.update.schema.json | 16 + .../acme/social/Follow.create.schema.json | 20 + .../schemas/acme/social/Follow.schema.json | 20 + .../acme/social/Follow.strict.schema.json | 17 + .../acme/social/Follow.update.schema.json | 16 + .../acme/social/Friendship.create.schema.json | 20 + .../acme/social/Friendship.schema.json | 20 + .../acme/social/Friendship.strict.schema.json | 17 + .../acme/social/Friendship.update.schema.json | 16 + .../social/GuestAccount.create.schema.json | 20 + .../acme/social/GuestAccount.schema.json | 32 + .../social/GuestAccount.strict.schema.json | 22 + .../social/GuestAccount.update.schema.json | 20 + .../social/MemberAccount.create.schema.json | 20 + .../acme/social/MemberAccount.schema.json | 32 + .../social/MemberAccount.strict.schema.json | 21 + .../social/MemberAccount.update.schema.json | 20 + .../MemberAccountTag.create.schema.json | 20 + .../acme/social/MemberAccountTag.schema.json | 20 + .../MemberAccountTag.strict.schema.json | 17 + .../MemberAccountTag.update.schema.json | 16 + .../acme/social/Person.create.schema.json | 16 + .../schemas/acme/social/Person.schema.json | 20 + .../acme/social/Person.strict.schema.json | 14 + .../acme/social/Person.update.schema.json | 13 + .../acme/social/Post.create.schema.json | 16 + .../schemas/acme/social/Post.schema.json | 20 + .../acme/social/Post.strict.schema.json | 14 + .../acme/social/Post.update.schema.json | 13 + .../social/PostCategory.create.schema.json | 16 + .../acme/social/PostCategory.schema.json | 20 + .../social/PostCategory.strict.schema.json | 14 + .../social/PostCategory.update.schema.json | 13 + .../social/PostReviewer.create.schema.json | 20 + .../acme/social/PostReviewer.schema.json | 20 + .../social/PostReviewer.strict.schema.json | 17 + .../social/PostReviewer.update.schema.json | 16 + .../acme/social/PostTag.create.schema.json | 20 + .../schemas/acme/social/PostTag.schema.json | 20 + .../acme/social/PostTag.strict.schema.json | 17 + .../acme/social/PostTag.update.schema.json | 16 + .../ScopedAccountTag.create.schema.json | 20 + .../acme/social/ScopedAccountTag.schema.json | 20 + .../ScopedAccountTag.strict.schema.json | 17 + .../ScopedAccountTag.update.schema.json | 16 + .../acme/social/Tag.create.schema.json | 16 + .../schemas/acme/social/Tag.schema.json | 20 + .../acme/social/Tag.strict.schema.json | 14 + .../acme/social/Tag.update.schema.json | 13 + .../contract-conformance/projection/case.json | 1 + .../projection/expected/openapi.json | 1868 +++++ .../acme/sales/Invoice.create.schema.json | 39 + .../schemas/acme/sales/Invoice.schema.json | 47 + .../acme/sales/Invoice.strict.schema.json | 38 + .../acme/sales/Invoice.update.schema.json | 38 + .../acme/sales/InvoiceLedger.schema.json | 37 + .../sales/InvoiceLedger.strict.schema.json | 33 + .../acme/sales/InvoiceRegister.schema.json | 23 + .../sales/InvoiceRegister.strict.schema.json | 21 + .../acme/sales/InvoiceStub.schema.json | 23 + .../acme/sales/InvoiceStub.strict.schema.json | 21 + .../acme/sales/InvoiceSummary.schema.json | 30 + .../sales/InvoiceSummary.strict.schema.json | 27 + .../contract-conformance/report/case.json | 1 + .../report/expected/openapi.json | 7411 +++++++++++++++++ .../acme/sales/Category.create.schema.json | 15 + .../schemas/acme/sales/Category.schema.json | 19 + .../acme/sales/Category.strict.schema.json | 14 + .../acme/sales/Category.update.schema.json | 12 + .../acme/sales/ExportCells.schema.json | 31 + .../acme/sales/ExportCells.strict.schema.json | 28 + .../acme/sales/ExportLine.create.schema.json | 19 + .../schemas/acme/sales/ExportLine.schema.json | 28 + .../acme/sales/ExportLine.strict.schema.json | 21 + .../acme/sales/ExportLine.update.schema.json | 19 + .../acme/sales/Invoice.create.schema.json | 39 + .../schemas/acme/sales/Invoice.schema.json | 43 + .../acme/sales/Invoice.strict.schema.json | 36 + .../acme/sales/Invoice.update.schema.json | 32 + .../acme/sales/InvoiceCalendar.schema.json | 33 + .../sales/InvoiceCalendar.strict.schema.json | 30 + .../acme/sales/InvoiceDisputes.schema.json | 24 + .../sales/InvoiceDisputes.strict.schema.json | 21 + .../sales/InvoiceStatusTotals.schema.json | 40 + .../InvoiceStatusTotals.strict.schema.json | 36 + .../acme/sales/InvoiceTotals.schema.json | 32 + .../sales/InvoiceTotals.strict.schema.json | 28 + .../acme/sales/InvoicesByMonth.schema.json | 28 + .../sales/InvoicesByMonth.strict.schema.json | 25 + .../acme/sales/Product.create.schema.json | 23 + .../schemas/acme/sales/Product.schema.json | 35 + .../acme/sales/Product.strict.schema.json | 29 + .../acme/sales/Product.update.schema.json | 26 + .../acme/sales/ProductRefunds.schema.json | 35 + .../sales/ProductRefunds.strict.schema.json | 32 + .../acme/sales/ProductRevenue.schema.json | 37 + .../sales/ProductRevenue.strict.schema.json | 32 + .../acme/sales/Reading.create.schema.json | 15 + .../schemas/acme/sales/Reading.schema.json | 20 + .../acme/sales/Reading.strict.schema.json | 13 + .../acme/sales/Reading.update.schema.json | 12 + .../schemas/acme/sales/ReadingLog.schema.json | 20 + .../acme/sales/ReadingLog.strict.schema.json | 17 + .../acme/sales/Sale.create.schema.json | 23 + .../schemas/acme/sales/Sale.schema.json | 32 + .../acme/sales/Sale.strict.schema.json | 24 + .../acme/sales/Sale.update.schema.json | 22 + .../acme/sales/SalesByCategory.schema.json | 43 + .../sales/SalesByCategory.strict.schema.json | 38 + .../schemas/acme/sales/SalesBySku.schema.json | 31 + .../acme/sales/SalesBySku.strict.schema.json | 28 + .../acme/sales/Ticket.create.schema.json | 20 + .../schemas/acme/sales/Ticket.schema.json | 35 + .../acme/sales/Ticket.update.schema.json | 26 + .../acme/sales/TicketsByTags.schema.json | 35 + .../secret-fields/case.json | 1 + .../secret-fields/expected/openapi.json | 543 ++ .../acme/secrets/Account.create.schema.json | 16 + .../schemas/acme/secrets/Account.schema.json | 28 + .../acme/secrets/Account.strict.schema.json | 14 + .../acme/secrets/Account.update.schema.json | 13 + fixtures/contract-conformance/tph/case.json | 1 + .../tph/expected/openapi.json | 2239 +++++ .../schemas/acme/auth/Auth.schema.json | 16 + .../acme/auth/BridgeAuth.create.schema.json | 25 + .../schemas/acme/auth/BridgeAuth.schema.json | 53 + .../acme/auth/BridgeAuth.strict.schema.json | 26 + .../acme/auth/BridgeAuth.update.schema.json | 17 + .../acme/auth/CopayAuth.create.schema.json | 28 + .../schemas/acme/auth/CopayAuth.schema.json | 57 + .../acme/auth/CopayAuth.strict.schema.json | 30 + .../acme/auth/CopayAuth.update.schema.json | 22 + .../auth/PriorAuthAuth.create.schema.json | 24 + .../acme/auth/PriorAuthAuth.schema.json | 56 + .../auth/PriorAuthAuth.strict.schema.json | 30 + .../auth/PriorAuthAuth.update.schema.json | 20 + .../write-through/case.json | 1 + .../write-through/expected/openapi.json | 780 ++ .../acme/orders/Customer.create.schema.json | 16 + .../schemas/acme/orders/Customer.schema.json | 20 + .../acme/orders/Customer.strict.schema.json | 14 + .../acme/orders/Customer.update.schema.json | 13 + .../acme/orders/Order.create.schema.json | 15 + .../schemas/acme/orders/Order.schema.json | 27 + .../acme/orders/Order.strict.schema.json | 13 + .../acme/orders/Order.update.schema.json | 12 + .../generator-registry-conformance/README.md | 2 +- .../registry.json | 16 + server/typescript/packages/cli/README.md | 10 +- .../cli/src/commands/generator-help.ts | 8 +- .../packages/cli/src/commands/generator.ts | 4 +- .../packages/cli/src/commands/init.ts | 2 +- server/typescript/packages/cli/src/index.ts | 2 +- .../cli/test/__snapshots__/cli.test.ts.snap | 2 +- .../cli/test/unit/reporting-inert.test.ts | 32 +- .../packages/codegen-ts/package.json | 6 +- .../scripts/gen-contract-conformance.ts | 138 + .../codegen-ts/src/contract/build-openapi.ts | 401 + .../src/contract/contract-errors.ts | 23 + .../codegen-ts/src/contract/field-schema.ts | 300 + .../codegen-ts/src/contract/object-schema.ts | 177 + .../codegen-ts/src/contract/strict-profile.ts | 161 + .../codegen-ts/src/generator-registry.ts | 37 + .../src/generators/contract-schema-file.ts | 90 + .../codegen-ts/src/generators/index.ts | 5 + .../codegen-ts/src/generators/openapi-file.ts | 66 + .../packages/codegen-ts/src/index.ts | 12 + .../codegen-ts/src/reference-templates.ts | 3 + .../src/reference/contract-schema.ts | 115 + .../codegen-ts/src/reference/openapi.ts | 91 + .../test/contract/contract-behaviours.test.ts | 304 + .../test/contract/contract-corpus.test.ts | 111 + .../test/contract/contract-validators.ts | 48 + .../test/generator-registry.test.ts | 12 +- .../test/reference-byte-identical.test.ts | 6 + .../test/reference-templates.test.ts | 2 +- .../packages/integration-tests/package.json | 2 + .../src/api-contract-openapi-check.ts | 118 + .../src/api-contract-scenario.ts | 7 +- .../test/api-contract-decimal.test.ts | 3 +- .../test/api-contract-generated.test.ts | 4 + .../test/api-contract-jsonb.test.ts | 6 +- .../test/api-contract-m2m.test.ts | 6 +- .../test/api-contract-openapi-check.test.ts | 66 + .../test/api-contract-projection.test.ts | 4 + .../test/api-contract-report.test.ts | 7 +- .../test/api-contract-secret-fields.test.ts | 4 + .../test/api-contract-tph.test.ts | 6 +- .../test/api-contract-write-through.test.ts | 4 + .../test/contract-oasdiff.test.ts | 187 + .../ADR-0034-codegen-scaffold-and-own.md | 24 + spec/roadmap.md | 15 +- 315 files changed, 40303 insertions(+), 130 deletions(-) create mode 100644 docs/features/contract-emitters.md create mode 100644 docs/recipes/check-api-against-last-release.md create mode 100644 fixtures/contract-conformance/README.md create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/case.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/openapi.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.strict.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.strict.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.update.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.update.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/case.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/openapi.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.strict.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.strict.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.update.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.update.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.schema.json create mode 100644 fixtures/contract-conformance/all-fields/case.json create mode 100644 fixtures/contract-conformance/all-fields/expected/openapi.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.strict.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.strict.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.update.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.update.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.create.schema.json create mode 100644 fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.schema.json create mode 100644 fixtures/contract-conformance/all-fields/meta.json create mode 100644 fixtures/contract-conformance/blog/case.json create mode 100644 fixtures/contract-conformance/blog/expected/openapi.json create mode 100644 fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.create.schema.json create mode 100644 fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.schema.json create mode 100644 fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.strict.schema.json create mode 100644 fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.update.schema.json create mode 100644 fixtures/contract-conformance/decimal/case.json create mode 100644 fixtures/contract-conformance/decimal/expected/openapi.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.create.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.strict.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.update.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.create.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.strict.schema.json create mode 100644 fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.update.schema.json create mode 100644 fixtures/contract-conformance/error-name-collision-list/expected-error.txt create mode 100644 fixtures/contract-conformance/error-name-collision-list/meta.json create mode 100644 fixtures/contract-conformance/error-name-collision/expected-error.txt create mode 100644 fixtures/contract-conformance/error-name-collision/meta.json create mode 100644 fixtures/contract-conformance/error-strict-map/expected-error.txt create mode 100644 fixtures/contract-conformance/error-strict-map/meta.json create mode 100644 fixtures/contract-conformance/error-strict-recursive/expected-error.txt create mode 100644 fixtures/contract-conformance/error-strict-recursive/meta.json create mode 100644 fixtures/contract-conformance/jsonb/case.json create mode 100644 fixtures/contract-conformance/jsonb/expected/openapi.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.create.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.update.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.create.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.strict.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.create.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.strict.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.create.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.create.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.strict.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.create.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json create mode 100644 fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/case.json create mode 100644 fixtures/contract-conformance/m2m/expected/openapi.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Account.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.update.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.create.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.strict.schema.json create mode 100644 fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.update.schema.json create mode 100644 fixtures/contract-conformance/projection/case.json create mode 100644 fixtures/contract-conformance/projection/expected/openapi.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.create.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.strict.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.update.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.strict.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.strict.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.schema.json create mode 100644 fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.strict.schema.json create mode 100644 fixtures/contract-conformance/report/case.json create mode 100644 fixtures/contract-conformance/report/expected/openapi.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.strict.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.create.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.update.schema.json create mode 100644 fixtures/contract-conformance/report/expected/schemas/acme/sales/TicketsByTags.schema.json create mode 100644 fixtures/contract-conformance/secret-fields/case.json create mode 100644 fixtures/contract-conformance/secret-fields/expected/openapi.json create mode 100644 fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.create.schema.json create mode 100644 fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.schema.json create mode 100644 fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.strict.schema.json create mode 100644 fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.update.schema.json create mode 100644 fixtures/contract-conformance/tph/case.json create mode 100644 fixtures/contract-conformance/tph/expected/openapi.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/Auth.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.create.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.strict.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.update.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.create.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.strict.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.update.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.create.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.strict.schema.json create mode 100644 fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.update.schema.json create mode 100644 fixtures/contract-conformance/write-through/case.json create mode 100644 fixtures/contract-conformance/write-through/expected/openapi.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.create.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.strict.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.update.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.create.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.strict.schema.json create mode 100644 fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.update.schema.json create mode 100644 server/typescript/packages/codegen-ts/scripts/gen-contract-conformance.ts create mode 100644 server/typescript/packages/codegen-ts/src/contract/build-openapi.ts create mode 100644 server/typescript/packages/codegen-ts/src/contract/contract-errors.ts create mode 100644 server/typescript/packages/codegen-ts/src/contract/field-schema.ts create mode 100644 server/typescript/packages/codegen-ts/src/contract/object-schema.ts create mode 100644 server/typescript/packages/codegen-ts/src/contract/strict-profile.ts create mode 100644 server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts create mode 100644 server/typescript/packages/codegen-ts/src/generators/openapi-file.ts create mode 100644 server/typescript/packages/codegen-ts/src/reference/contract-schema.ts create mode 100644 server/typescript/packages/codegen-ts/src/reference/openapi.ts create mode 100644 server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts create mode 100644 server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts create mode 100644 server/typescript/packages/codegen-ts/test/contract/contract-validators.ts create mode 100644 server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts create mode 100644 server/typescript/packages/integration-tests/test/api-contract-openapi-check.test.ts create mode 100644 server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts diff --git a/AGENTS.md b/AGENTS.md index b3c37a737e..1ce9781050 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken. - **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres. **Cross-port conformance corpora** (every port runs the shared corpus): -- Metamodel: `fixtures/conformance/` (421 fixtures; 34 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green. +- Metamodel: `fixtures/conformance/` (421 fixtures; 35 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green. - Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical. - Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".) - API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived. diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d1e9e3f0..3fd5e7f7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ here.** ### Added +- **Contract emitters: JSON Schema and OpenAPI generators (FR-022).** `jsonSchemaFile()` + (`contract-schema`) writes JSON Schema 2020-12 for each object's read, create and update shapes, and + a strict structured-output subset with `profile: "strict"`. `openApiFile()` (`openapi`) writes one + OpenAPI 3.1 document for the generated REST surface: the routes, the filter, sort and paging + parameters, the error envelope, and those shapes as components. The output follows the REST contract + and the `wire` setting, and a project on any port runs them through the Node `meta` CLI. They are + reference helpers: opt-in, listed by `meta gen --list`, ejectable and gated by `verify --codegen` + (ADR-0034 Amendment 10). Protobuf is not built. See `docs/features/contract-emitters.md`. +- **A recipe for checking an API change against the last release.** Emit the document at the last tag + and at `HEAD`, then run oasdiff. See `docs/recipes/check-api-against-last-release.md`. - **A `pydantic` extra on the Python package.** `pip install "metaobjects[pydantic]"` installs `pydantic>=2.8`, the minimum the generated Python models need, so an adopter can get the enforced floor with the package. The base install is unchanged. diff --git a/README.md b/README.md index d700d62a7f..a045a7ee4e 100644 --- a/README.md +++ b/README.md @@ -235,12 +235,12 @@ roadmap item. The fifth has been dogfooded on maintainer-owned projects only, an sixth ships two libraries at their own stability labels: 1. **Codegen** *(generators you write and own, in every port)* — on the core, you build - the generators your application needs: OpenAPI, JSON Schema, Zod, DTOs, a client, - docs — anything the model describes. A generator is a name plus a function from the + the generators your application needs: Zod, DTOs, a client, docs — anything the + model describes. A generator is a name plus a function from the model to files, and `verify` gates it with nothing to register; `meta generator new ` scaffolds a working one on TypeScript, and [Write your own generator](docs/recipes/write-your-own-generator.md) has every port's - 20-line shape plus JSON Schema and OpenAPI examples to copy. The reference generators + 20-line shape plus worked examples to copy. The reference generators (Drizzle/Zod + Fastify for TS, Spring REST + DTO + repository for Java, `data class` + Exposed for Kotlin, EF Core entity class + ASP.NET routes for C#, Pydantic + FastAPI for Python) are starting points: run one as shipped when it already emits what you want, or diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index df1815ec9b..3014906daf 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -77,7 +77,7 @@ in order and every change that needs action. ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — -or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas, +or none emits it the way this application needs it. Zod schemas, DTOs or a client for a framework nothing here ships, a service layer in the house style, a data dictionary, a search-index mapping, test fixtures. That is a generator, and you write it now, in this change — once it passes the cost test in `references/owning-generators.md` @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently. Make it deterministic: the same model must give the same bytes, or verify reports drift. Every port's minimal generator (each run through gen, verify and a convicted model -change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a -product surface — are in the guide "Write your own generator": +change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a +generator and not a product surface — are in the guide "Write your own generator": . +For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from +1.2.1 (run through the Node `meta` CLI on any port): +. **Use a stock generator as shipped** when one already emits what you need: `meta gen --list --probe` is the catalog, and each row says how to wire it with nothing diff --git a/agent-context/skills/metaobjects-codegen/references/typescript.md b/agent-context/skills/metaobjects-codegen/references/typescript.md index 0765fafc56..d0847d17ce 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript.md @@ -27,7 +27,7 @@ tested repository generator), For any output the model describes and no reference emits, start here: ```bash -meta generator new openapi --scope model # entity (default) | package | model +meta generator new data-dictionary --scope model # entity (default) | package | model meta gen # runs it — it already emits JSON per unit meta verify --codegen # gates it; nothing to register npx tsc -p tsconfig.codegen.json # typecheck it; meta gen loads it untyped @@ -225,6 +225,14 @@ report hook until you resync it (`meta eject`). On SQLite a decimal field of a r mount. A report with no view source, or an abstract one, generates nothing. The route and contract: `references/reporting.md` in the `metaobjects-authoring` skill. +**Contract emitters (`jsonSchemaFile()`, `openApiFile()`), from 1.2.1.** Reference helpers that +write the REST contract as standard documents. `contract-schema` writes JSON Schema 2020-12 for each +object's read, create and update shapes (`profile: "strict"` for a structured-output subset), and +`openapi` writes one OpenAPI 3.1 document. Opt-in: import them from +`@metaobjectsdev/codegen-ts/generators`, or `meta eject contract-schema openapi` to own them, and +`verify --codegen` gates the output. A project that publishes the contract should fix `wire.decimals` +and `wire.nullMembers`. Details: `docs/features/contract-emitters.md`. + **Cube export (`cubeModel()`).** A reference helper that writes the reporting vocabulary (`dimension.*`, `measure.*`, `segment.filter`, a served `object.report`) as Cube data-model files, `model/cubes/.yml`: a cube for each table-backed entity that declares any of it, a join for diff --git a/agent-context/skills/metaobjects-fit-assessment/references/capability-sheet.md b/agent-context/skills/metaobjects-fit-assessment/references/capability-sheet.md index 3cabf838ed..d18d8a70c1 100644 --- a/agent-context/skills/metaobjects-fit-assessment/references/capability-sheet.md +++ b/agent-context/skills/metaobjects-fit-assessment/references/capability-sheet.md @@ -58,16 +58,23 @@ building block. Every generator is a reference helper an adopter selects, ejects on a hand edit, and `verify --db` checks the database Prisma pushed (Postgres, SQLite). Views, expression/partial indexes and SQLite CHECKs are refused, not dropped (`docs/recipes/prisma.md`); -- ship an OpenAPI, AsyncAPI or JSON Schema generator; +- ship an AsyncAPI generator; - reach a consumer outside the five ports (Go, Rust, Ruby, PHP, Swift…) directly. **Each of those is a generator the project writes**, and that is the primary codegen path, not a workaround: a generator is a function from the loaded model to files, a few dozen lines, and `verify --codegen` gates its output like any other. `meta generator new ` scaffolds one in -TypeScript; `docs/recipes/write-your-own-generator.md` shows every port and ships JSON Schema and -OpenAPI 3.1 example generators to copy. An OpenAPI or JSON Schema generator also reaches the -languages outside the five ports through their own generators. Price it in the wedge (a day or -two for a first generator), and do not report it as impossible. +TypeScript; `docs/recipes/write-your-own-generator.md` shows every port. Price it in the wedge (a +day or two for a first generator), and do not report it as impossible. + +**OpenAPI and JSON Schema are stock generators, unreleased, targeting 1.2.1.** `openapi` writes one +OpenAPI 3.1 document for the generated REST surface, and `contract-schema` writes JSON Schema 2020-12 +for each object's read, create and update shapes, plus a strict structured-output subset. A project +on any port runs them through the Node `meta` CLI. They reach the languages outside the five ports +through those languages' own OpenAPI tooling, and oasdiff over two emitted documents checks a change +against the last release (`docs/features/contract-emitters.md`). Until 1.2.1 ships, an assessment of +a released version counts these as generators the project writes, from the examples in +`docs/recipes/write-your-own-generator.md`. **Contract-only cannot** bind a shape to a queue, topic or transport as declared vocabulary (`api.*`, `operation.*`, `binding.*` are not shipped); a generator can still emit per-transport diff --git a/bun.lock b/bun.lock index 195e182cb8..7111acb6f9 100644 --- a/bun.lock +++ b/bun.lock @@ -194,6 +194,9 @@ "@biomejs/biome": "^1.9.0", "@metaobjectsdev/codegen-ts-react": "workspace:*", "@metaobjectsdev/runtime-ts": "workspace:*", + "@seriousme/openapi-schema-validator": "^2.11.0", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "bun-types": "latest", "drizzle-orm": "^0.38.0", "hono": "^4.6.0", @@ -318,6 +321,8 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.5.4", "@types/pg": "^8.11.6", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "bun-types": "^1.1.27", "miniflare": "4.20251210.0", "mysql2": "^3.11.0", @@ -836,6 +841,8 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@seriousme/openapi-schema-validator": ["@seriousme/openapi-schema-validator@2.11.0", "", { "dependencies": { "ajv": "^8.20.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "yaml": "^2.9.1" }, "bin": { "bundle-api": "bin/bundle-api-cli.js", "validate-api": "bin/validate-api-cli.js" } }, "sha512-DL8TjmjkUS76OaK/TinoRtf6PrirsWfMZiv62ifP9eq3UOpoLqSGZKpCErxMzz0i1pjECSPYY+qZ1SRYEgiEvg=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], @@ -950,7 +957,9 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -1200,7 +1209,7 @@ "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - "fast-uri": ["fast-uri@4.2.3", "", {}, "sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg=="], + "fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], @@ -1748,7 +1757,7 @@ "@electric-sql/pglite-tools/@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="], - "@fastify/ajv-compiler/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "@fastify/ajv-compiler/fast-uri": ["fast-uri@4.2.3", "", {}, "sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg=="], "@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], @@ -1780,16 +1789,12 @@ "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], - "@prisma/streams-local/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "@stryker-mutator/core/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "@stryker-mutator/instrumenter/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@visx/vendor/d3-array": ["d3-array@3.2.1", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ=="], - "ajv/fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], - - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -1808,7 +1813,7 @@ "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "fast-json-stringify/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "fast-json-stringify/fast-uri": ["fast-uri@4.2.3", "", {}, "sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg=="], "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -1854,8 +1859,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@fastify/ajv-compiler/ajv/fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.5.0", "", {}, "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ=="], @@ -1872,14 +1875,8 @@ "@metaobjectsdev/runtime-ts/@libsql/client/libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], - "@prisma/streams-local/ajv/fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], - - "ajv-formats/ajv/fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], - "dockerode/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "fast-json-stringify/ajv/fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], "lazystream/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 36834aadbc..3b3fb43ccb 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -1,6 +1,6 @@ # Conformance coverage -The MetaObjects standard ships **34 shared conformance corpora** under +The MetaObjects standard ships **35 shared conformance corpora** under [`fixtures/`](../fixtures/). Every port runs every corpus that is *applicable to it* and asserts the same expected behaviour against the same fixtures. **This page is the inverse index**: fixture → feature doc + per-port pass status, and it is the @@ -60,6 +60,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | [`fixtures/wire-conformance/`](../fixtures/wire-conformance/) (the `wire` setting, ADR-0019 Amendment 2) | 6 cases + 8 refusals | ✓ | ✓ | inherits via Java (one config reader) | ✓ | ✓ | | [`fixtures/codegen-noop/`](../fixtures/codegen-noop/) (FR-044 — reporting vocabulary: what is lowered, what stays inert) | 1 model pair (`reporting/with` vs `reporting/without`) | ✓ (codegen + migrate) | ✓ | ✓ | ✓ | ✓ | | [`fixtures/cube-model/`](../fixtures/cube-model/) (FR-044 Plan 4 — the `cube-model` generator's golden Cube files, plus the canonical model's golden) | 46 cases (36 trees + 10 errors) + 1 canonical golden | ✓ (the exporter is TypeScript-only, spec R6; its `cube` lane also loads the files into a real Cube) | — | — | — | — | +| [`fixtures/contract-conformance/`](../fixtures/contract-conformance/) (FR-022 — the `contract-schema` and `openapi` generators' golden trees and refusals) | 16 cases (12 trees + 4 errors) | ✓ (one neutral engine, ADR-0034 Amendment 10; the generated API lanes also hold real traffic to the documents) | — | — | — | — | A ✓ means the port runs the corpus green; an explicit `n / m` is used where a port carries a ledgered divergence. The two ledgered YAML fixtures are documented @@ -91,7 +92,7 @@ the corpora above do two different jobs. Only the first is a promise to adopters A red cell here is a MetaObjects bug. - **Template quality checks — not a promise.** The generated lane of `api-contract-conformance/`, `generator-registry-conformance/` (stable generator names), - `codegen-noop/` (vocabulary with no lowering yet emits nothing), `cube-model/` (the Cube + `codegen-noop/` (vocabulary with no lowering yet emits nothing), `contract-conformance/` (the JSON Schema and OpenAPI output), `cube-model/` (the Cube exporter's output, a TypeScript reference helper), `requirement-test-identity-conformance/` (which tests the `requirement-tests` reference generator yields from a ledger and what each is called; the text it writes is not diff --git a/docs/README.md b/docs/README.md index 1f3a9dc2c5..2dbfeb8bfb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,6 +45,7 @@ docs/ │ ├── metadata-dependencies.md # building on a metadata model published elsewhere (FR-023) │ ├── libraries.md # reusable declared design you opt into, a layer at a time (FR-043) │ ├── reporting.md # dimensions, measures, segments, reports: vocabulary and load rules (FR-044) +│ ├── contract-emitters.md # the contract-schema and openapi generators: JSON Schema and OpenAPI for the REST contract (FR-022) │ ├── cube-export.md # the cube-model generator: reporting vocabulary as Cube data-model files (FR-044) │ └── own-your-codegen.md # scaffold-and-own generator ownership (ADR-0034) └── ports/ # one file per language/framework port @@ -74,6 +75,7 @@ this tree is documentation, not the source of truth. | Declare what a dashboard groups by and counts (`dimension`, `measure`, `segment`, `object.report`; load-time checked; a view-backed report becomes a SQL view served by a generated read-only list route and a TypeScript list hook) | [`features/reporting.md`](features/reporting.md) | | Download a report as CSV, JSON or XML (`?format=`, the `report-export` generator, its cap and null forms) | [`features/reporting.md`](features/reporting.md#exporting-a-report) | | Keep Prisma as the ORM, with `schema.prisma` generated from the model and gated by `verify` (TypeScript) | [`recipes/prisma.md`](recipes/prisma.md) | +| Publish the API as JSON Schema or OpenAPI, and check a change against the last release | [`features/contract-emitters.md`](features/contract-emitters.md), [`recipes/check-api-against-last-release.md`](recipes/check-api-against-last-release.md) | | Export the reporting vocabulary to Cube (`cube-model`: cubes, joins, rollups; the `cube` lane that checks it against a real Cube) | [`features/cube-export.md`](features/cube-export.md) | | Wire prompt construction (FR-004) | [`features/templates-and-payloads.md`](features/templates-and-payloads.md) | | Share a metadata shape across multiple instances (abstracts, `extends:`) | [`features/abstracts-and-inheritance.md`](features/abstracts-and-inheritance.md) | diff --git a/docs/features/cli.md b/docs/features/cli.md index e2b68fc18f..b098c2c501 100644 --- a/docs/features/cli.md +++ b/docs/features/cli.md @@ -385,6 +385,29 @@ skips: a generated page committed in `agent/` that a fresh run no longer writes previous schema's `agent/schema.md`, say) is reported as drift, so a skip cannot leave a stale page in place, green. A hand-written file in the docs directory is left alone. +## The contract emitters: `contract-schema` and `openapi` + +Two stock generators write the REST contract as standard documents. They are entries in +`generators` like any other, imported from `@metaobjectsdev/codegen-ts/generators`. + +| Generator | Factory | What it writes | +|---|---|---| +| `contract-schema` | `jsonSchemaFile()` | JSON Schema 2020-12 for each object's read, create and update shapes, under `schemas/`. `profile: "strict"` writes one structured-output `.strict.schema.json` per object instead. | +| `openapi` | `openApiFile()` | One OpenAPI 3.1 document for the generated REST surface, `openapi.json`. | + +```bash +meta gen --generators openapi,contract-schema # only these entries +meta verify --codegen # fails when a document is stale +meta eject contract-schema openapi # copy them to change what they write +``` + +There is no verify flag for them and none is needed: `verify --codegen` re-runs every declared entry. +Both run through the Node `meta` CLI on every port, because their output does not depend on the server +language. A project on another port passes `expose` to `openApiFile()`. To check a change against the +last release, compare two emitted documents with oasdiff. See +[contract-emitters.md](contract-emitters.md) and +[the recipe](../recipes/check-api-against-last-release.md). + ## Declarative config (`metaobjects.config.yaml`) — Python codegen The Python `metaobjects` CLI reads its generator list from a declarative project config, diff --git a/docs/features/codegen-concepts.md b/docs/features/codegen-concepts.md index 35de5c2744..73be43de51 100644 --- a/docs/features/codegen-concepts.md +++ b/docs/features/codegen-concepts.md @@ -28,7 +28,7 @@ even see it. The product is the **core** — model, loader, registry, `verify`, `migrate`, render and extract. On it, **you build the generators your application needs**: an output the model -fully describes and no reference emits (OpenAPI, JSON Schema, Zod, DTOs, a client, docs) +fully describes and no reference emits (Zod, DTOs, a client, docs) is a generator you write, not a layer you hand-write and not a feature you wait for ([ADR-0034 Amendment 4](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md#amendment-4-2026-09-26--write-your-own-generator-is-the-primary-path)). A generator is a name plus a function from the model to files, and `verify --codegen` @@ -191,7 +191,7 @@ and breaks under GraalVM native-image / .NET AOT. (ADR-0001.) Keep native code generators **per-port and idiomatic** (Drizzle/Zod for TS, EF Core for C#, Spring for Java/Kotlin, Pydantic for Python) — collapsing them to one generic emitter is the OpenAPI-Generator trap. Keep language-*neutral* artifacts (migrations, -docs, future OpenAPI/Mermaid) as **one shared engine**. The test: does the output depend +docs, [OpenAPI and JSON Schema](contract-emitters.md)) as **one shared engine**. The test: does the output depend on the implementation language? If yes, it's per-port; if no, it's shared. (ADR-0020.) ## 10. Codegen scopes — object, package, app diff --git a/docs/features/contract-emitters.md b/docs/features/contract-emitters.md new file mode 100644 index 0000000000..e85ac28a2c --- /dev/null +++ b/docs/features/contract-emitters.md @@ -0,0 +1,225 @@ +# Contract emitters: `contract-schema` and `openapi` + +_`jsonSchemaFile()` and `openApiFile()`: write the model's wire contract as plain JSON Schema 2020-12 +and OpenAPI 3.1, so a consumer in any language can work from a standard document instead of a fourth +hand-kept source of truth._ + +**Status:** TypeScript reference helpers, unreleased, targeting 1.2.1 (FR-022; +[ADR-0034](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md) Amendment 10). They are listed +by `meta gen --list`, imported from `@metaobjectsdev/codegen-ts/generators` to run as shipped, copied +with `meta eject contract-schema openapi` to change them, and checked by `meta verify --codegen`. They +are not core: what MetaObjects guarantees is the model, and a generator that writes files into your +repo is a helper you can copy and own. They add no vocabulary and no runtime: the files import nothing. + +**What they do.** `contract-schema` writes a JSON Schema for each object's read, create and update +shapes, and on request a strict subset for structured output. `openapi` writes one document for the +generated REST surface, with those shapes as its components. Both state what the routes in every +port actually send and take, under the project's [`wire` setting](api-contract.md#the-wire-setting). + +**What they do not do.** They generate no client and no server, and they do not validate anything at +runtime. Run openapi-generator, Kiota or orval on the document for a client. The protobuf emitter in +the [design](../superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md) is not built; it is +planned with binary protocols for 1.3. + +**Entirely opt-in.** `meta init` wires no generator. A project that does not list them gets no file. + +## Wiring + +```ts +import { defineConfig } from "@metaobjectsdev/cli"; +import { jsonSchemaFile, openApiFile } from "@metaobjectsdev/codegen-ts/generators"; + +export default defineConfig({ + apiPrefix: "/api", + generators: [ + jsonSchemaFile(), + jsonSchemaFile({ profile: "strict", filter: (obj) => obj.name === "SupportTicket" }), + openApiFile({ title: "Shop API", version: "2.1.0" }), + ], +}); +``` + +List `jsonSchemaFile()` twice to get both profiles. Commit what they write, like any generated file: +`meta verify --codegen` re-runs them and fails when a file is stale, so the contract cannot drift from +the model. + +### Options + +| Generator | Option | Default | | +|---|---|---|---| +| both | `filter` | every object | Narrow what is covered. An object a kept one references stays, so a `$ref` never dangles. | +| both | `target` | the default target | The named output target the files land under. | +| `jsonSchemaFile` | `profile` | `"canonical"` | `"strict"` writes the structured-output subset. | +| `jsonSchemaFile` | `baseUri` | the file's relative path | An absolute prefix for each `$id`, when a schema registry needs one. | +| `openApiFile` | `title`, `version`, `description` | `API`, `0.0.0`, none | The document's `info`. | +| `openApiFile` | `servers`, `securitySchemes`, `security` | none | Passed through untouched. See [Security](#security-and-servers). | +| `openApiFile` | `path` | `openapi.json` | Where the file lands under the target. | +| `openApiFile` | `expose` | what the routes generators declare, else every verb | The verbs your routes mount. Needed when another port generates them. | + +## What it writes + +``` +/ +├── openapi.json +└── schemas/acme/commerce/ + ├── Order.schema.json # what a read answers + ├── Order.create.schema.json # what a create takes + ├── Order.update.schema.json # what an update takes + ├── Order.strict.schema.json # with profile: "strict" + └── Address.schema.json # a value object: read and create shapes only +``` + +Schemas reference one another by relative `$ref`, so any validator loads the directory as it is. In +the document, the same shapes are `Order`, `OrderCreate` and `OrderUpdate` under `components.schemas`, +alongside `OrderList`, `OrderPage` and `ApiError`. + +A projection and a report are read-only, so they have a read shape only. + +### Why three shapes + +A read, a create and an update differ in what is required and what may be null, and one schema with +`readOnly` and `writeOnly` could state neither correctly. + +| Shape | Required | Optional fields | Left out | +|---|---|---|---| +| read | every key | present, and `null` when empty | a credential (`view.password`) | +| create | `@required` fields | may be left out, may not be `null` | server-set fields, a generated key, `@formExclude`, credentials, a pinned TPH discriminator | +| update | none | may be left out, and `null` clears them | server-set fields, `@formExclude`, credentials, a TPH discriminator | + +A nested value object in a write takes the target's create shape. + +## Types + +Each field is what [the REST contract](api-contract.md#type-encodings-tier-1-invariant) says it sends. + +| Field | JSON Schema | +|---|---| +| `field.int` | `integer`, `format: int32` | +| `field.long` | `integer`, `format: int64`. A JSON number, not a string | +| `field.currency` | `integer`, `format: int64`, with the ISO code in `description` | +| `field.double`, `field.float` | `number`, `format: double` / `float` | +| `field.boolean` | `boolean` | +| `field.date` | `string`, `format: date` | +| `field.time` | `string`, a `pattern`. RFC 3339 `time` demands an offset, which this type has not got | +| `field.timestamp` | A read is `format: date-time`. A write or a filter also takes a value with no offset, read as UTC, so it is a `pattern`. With `@localTime`, a `pattern` with no offset everywhere | +| `field.uuid` | `string`, `format: uuid` | +| `field.uri`, `field.inet` | `format: uri`, or either of `ipv4` and `ipv6`. With `@lenient`, a plain `string` | +| `field.enum` | `string`, `enum` | +| `field.string` | `string`. `@stringFormat` adds `format: email` / `hostname`, and a `@dbColumnType: jsonb` bag is `{}` | +| `field.object` | a `$ref` to the target | +| `field.decimal` | see below | + +Length and range validators become `minLength`, `maxLength`, `minimum`, `maximum` and `pattern`. + +### Set the wire form if you publish a contract + +Two things on the wire depend on the server's language unless the project fixes them, and the schemas +say exactly that. + +| Setting | Value | A read in the schema | +|---|---|---| +| `wire.decimals` | `"string"` | a string holding the canonical pattern | +| `wire.decimals` | default | `["string", "number"]`, since each language writes its own form | +| `wire.nullMembers` | `"omit"` | a value object's optional member is absent, and not nullable | +| `wire.nullMembers` | `"emit"` | the member is always present, and nullable | +| `wire.nullMembers` | default | absent or `null` | + +A project that publishes this contract should fix both in `.metaobjects/config.json`, because a client +generated from `["string", "number"]` has to handle two types: + +```json +{ "wire": { "decimals": "string", "nullMembers": "omit" } } +``` + +A write takes a decimal as a string or a number in every setting. A top-level optional field is +always written, as `null`, in every setting. + +## The strict profile + +`profile: "strict"` writes one self-contained `.strict.schema.json` per object, for a model to +fill in: OpenAI strict mode, Anthropic structured outputs, an MCP `inputSchema`. It is the intersection +of what those hosts accept. + +- It covers the create shape, or the read shape where there is none. +- Every property is `required`, and an optional one is `["T", "null"]`. +- Every object has `additionalProperties: false`, and every `$ref` is inlined. +- It holds no `$schema` or `$id`. `format`, `pattern` and bounds move into `description` as prose. + +What the subset cannot state is refused, not dropped, because a schema narrowed in silence gives a +model a different contract from the one you declared. A map, an open object, a recursive type or a +polymorphic TPH base stops the run with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path. Leave +that object out with `filter`. + +## The OpenAPI document + +It covers what the generated routes serve: the CRUD paths, the bracketed filter grammar with each +field's allowed operators, `sort`, `limit`, `offset` and `withCount`, M:N traversal, TPH polymorphic +and per-subtype mounts, projections, reports (with the `format` parameter when `report-export` is +wired), and the error envelope. + +A list answers an array, or `{ rows, total }` with `withCount=1`, so its response is a `oneOf` of +`List` and `Page`. + +The document covers the whole model, narrowed by `filter` and the project's `scope`. It never depends +on the run: `meta gen Author` writes the same bytes as a full `meta gen`. + +### Security and servers + +Authentication and deployment are facts about your application, and the model is shared across both, +so they are options: + +```ts +openApiFile({ + title: "Shop API", + version: "2.1.0", + servers: [{ url: "https://api.example.com" }], + securitySchemes: { bearer: { type: "http", scheme: "bearer" } }, + security: [{ bearer: [] }], +}); +``` + +### Left out on purpose + +- The `default` keyword: a default belongs to the database or to `@autoSet`, and is not on the wire. +- `readOnly` and `writeOnly`: the shapes are separate. +- The TypeScript-only `search`, `filter[or]` and `filter[and]` extensions: the document states the + cross-port contract ([api-contract.md](api-contract.md)). +- 405 answers: a verb a read-only surface does not serve is absent from the document. +- Routes you wrote by hand. Eject `openapi` and add them in `buildOpenApiDocument` if you want them + documented. + +## Other ports + +The output depends on the model and the `wire` setting, and never on the server's language, so there +is one engine and every project runs it through the Node `meta` CLI. All five ports answer the same +REST contract, so the document is right for all of them. + +A Java, Kotlin, Python or C# project lists the generators in a `metaobjects.config.ts` beside its +model and passes `expose`, because that run cannot see the routes another port generates: + +```ts +openApiFile({ title: "Shop API", version: "2.1.0", expose: ["list", "get", "create", "update"] }); +``` + +## Check a change against the last release + +Compare the document at your last tag with the one at `HEAD`. A new optional field passes, and a +removed field or filter fails: [Check an API change against the last release](../recipes/check-api-against-last-release.md). + +## How it is checked + +- **The corpus.** [`fixtures/contract-conformance/`](../../fixtures/contract-conformance/) holds golden + trees for each model shape and wire setting, and the refusals. +- **Stock validators.** Each schema compiles in strict Ajv, each strict file holds to the subset, and + each document passes the OpenAPI 3.1 specification's own schema. +- **Real traffic.** Each exchange in the TypeScript generated API lanes is held to the golden + document: the route and status must be documented, a 2xx request must be one it accepts, and the + response must match. A server and a document that drift apart fail there. +- **oasdiff.** A test runs real oasdiff over two emitted documents. + +## Errors + +| Code | Meaning | Fix | +|---|---|---| +| `ERR_CONTRACT_NAME_COLLISION` | Two things would take one component name, such as an object `OrderCreate` beside the create shape of `Order` | Rename one, or leave one out with `filter` | +| `ERR_CONTRACT_STRICT_UNREPRESENTABLE` | The strict subset cannot state an object | Leave it out of the strict entry with `filter` | diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index 1ae5208fa7..3d40f8f739 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -14,11 +14,11 @@ there; your copy is yours. **Need an output nothing here emits? Write the generator.** That is the primary path, not an escape hatch ([ADR-0034 Amendment 4](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md#amendment-4-2026-09-26--write-your-own-generator-is-the-primary-path)): -OpenAPI, JSON Schema, a client, a DTO or service layer, docs — anything the model -describes. A generator is a name plus a function from the model to files; `verify +a client, a DTO or service layer, docs — anything the model +describes. OpenAPI and JSON Schema are [stock generators](contract-emitters.md). A generator is a name plus a function from the model to files; `verify --codegen` gates it with nothing to register. On TypeScript, `meta generator new ` scaffolds a working one and wires it. Every port's 20-line shape, run end to end, plus -JSON Schema and OpenAPI 3.1 examples to copy: +worked examples to copy: [Write your own generator](../recipes/write-your-own-generator.md). Eject a reference (below) only when one already emits something close to what you want. A worked TypeScript example that sits on top of the stock routes: [a typed fetch client for the reference diff --git a/docs/llms/llms-full.txt b/docs/llms/llms-full.txt index 93238df7e7..ce7886bf3f 100644 --- a/docs/llms/llms-full.txt +++ b/docs/llms/llms-full.txt @@ -23,7 +23,7 @@ Underneath those two verbs, the model is a cross-language metadata standard for The model is the part you keep, and the generated code can be thrown away and regenerated. Your typed metadata lives in your repo, and the generated code is idiomatic per-language output with **no proprietary runtime**: the entity and model tier has no dependencies, and the optional client, prompt-render and runtime tiers are ordinary Apache-2.0 packages you could vendor or fork. If the `@metaobjectsdev/*` (npm) or `com.metaobjects:*` (Maven) packages disappeared tomorrow, your generated code keeps working in every language. -**Write your own generators; expect to.** Documentation is a generator too (`docsFile()`, `apiDocsFile()`; `meta init --with-docs` wires them). The reference generators cover the common cases, but a team should plan to write the ones its application needs: an OpenAPI file, a typed client, a service layer in your house style. A generator you own is regenerated on every model change and gated by `verify --codegen`, which is what keeps the output from drifting. In TypeScript, `meta generator new ` writes a working one and wires it. See [Write your own generator](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/write-your-own-generator.md) for every port, and [a typed client for the reference routes](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/typed-client-for-the-reference-routes.md) for a worked example. +**Write your own generators; expect to.** Documentation is a generator too (`docsFile()`, `apiDocsFile()`; `meta init --with-docs` wires them). The reference generators cover the common cases, but a team should plan to write the ones its application needs: a typed client, a service layer in your house style. A generator you own is regenerated on every model change and gated by `verify --codegen`, which is what keeps the output from drifting. In TypeScript, `meta generator new ` writes a working one and wires it. See [Write your own generator](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/write-your-own-generator.md) for every port, and [a typed client for the reference routes](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/typed-client-for-the-reference-routes.md) for a worked example. --- diff --git a/docs/llms/llms.txt b/docs/llms/llms.txt index a763a62e16..55839597a8 100644 --- a/docs/llms/llms.txt +++ b/docs/llms/llms.txt @@ -10,7 +10,7 @@ It protects what the model declares. Your hand-written logic is still yours. -**Write your own generators; expect to.** Documentation is a generator too (`docsFile()`, `apiDocsFile()`; `meta init --with-docs` wires them). The reference generators cover the common cases, but a team should plan to write the ones its application needs: an OpenAPI file, a typed client, a service layer in your house style. A generator you own is what keeps the output from drifting, because it is regenerated on every model change and `verify --codegen` fails the build when committed output is stale. Hand-writing something the model fully describes is the thing to avoid. In TypeScript, `meta generator new ` writes a working one and wires it. See [Write your own generator](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/write-your-own-generator.md) for every port, and [a typed client for the reference routes](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/typed-client-for-the-reference-routes.md) for a worked example. +**Write your own generators; expect to.** Documentation is a generator too (`docsFile()`, `apiDocsFile()`; `meta init --with-docs` wires them). The reference generators cover the common cases, but a team should plan to write the ones its application needs: a typed client, a service layer in your house style. A generator you own is what keeps the output from drifting, because it is regenerated on every model change and `verify --codegen` fails the build when committed output is stale. Hand-writing something the model fully describes is the thing to avoid. In TypeScript, `meta generator new ` writes a working one and wires it. See [Write your own generator](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/write-your-own-generator.md) for every port, and [a typed client for the reference routes](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/typed-client-for-the-reference-routes.md) for a worked example. **What is guaranteed, and what is yours** (ADR-0034 Amendment 3). The core is the promise. It is the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager`, not the HTTP adapters that mount it); schema migrations (`meta migrate`); the drift gates (`meta verify`); and prompt render and the reply parser. All of it is conformance-gated and the same in every port that ships it. Every generator that writes application code into the repo (routes, controllers, ORM wiring, DTOs, forms, grids, hooks) is a reference helper: a starting point that compiles and passes its reference fixtures, which the adopter copies with `meta eject` and owns. A defect in the core is a MetaObjects bug; a defect in a generator is fixed in the reference, and the adopter's copy is theirs. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). If an ejected copy never changed from the shipped reference, `meta eject --undo` removes it, and `meta gen` says when it finds one. Python, Java/Kotlin and C# have the same switch (`metaobjects eject --undo`, `mvn metaobjects:eject -Dundo=true`, `dotnet meta eject --undo`). There is no `meta doctor`; `meta eject --list` is the report. diff --git a/docs/recipes/check-api-against-last-release.md b/docs/recipes/check-api-against-last-release.md new file mode 100644 index 0000000000..226e1d1350 --- /dev/null +++ b/docs/recipes/check-api-against-last-release.md @@ -0,0 +1,78 @@ +# Check an API change against the last release + +Find out in CI whether a change to the model breaks consumers of your API, before they do. Emit the +OpenAPI document at your last release tag and at `HEAD`, then let +[oasdiff](https://github.com/oasdiff/oasdiff) compare them. No MetaObjects flag is involved: the +generator writes a standard document, and oasdiff reads it like any other. + +You need the [`openapi` generator](../features/contract-emitters.md) in your config, and oasdiff on +the machine or through its image, `tufin/oasdiff`. + +## The check + +```bash +#!/usr/bin/env bash +# scripts/check-api-compat.sh +set -euo pipefail + +DOCUMENT=generated/openapi.json # your outDir, plus openapi.json +LAST_TAG=$(git describe --tags --abbrev=0) + +BASE=$(mktemp -d) +trap 'git worktree remove --force "$BASE"' EXIT +git worktree add --detach "$BASE" "$LAST_TAG" + +( cd "$BASE" && npm ci && npx meta gen --generators openapi ) +npx meta gen --generators openapi + +oasdiff breaking "$BASE/$DOCUMENT" "$DOCUMENT" --fail-on WARN +``` + +`meta gen --generators openapi` runs only that entry of your config. Both sides are generated, not +read from git, so the comparison holds even if someone committed a stale file. If you commit the +document and trust `meta verify --codegen` to keep it current, the worktree is unnecessary: + +```bash +git show "$LAST_TAG:$DOCUMENT" > "$BASE/openapi.json" +oasdiff breaking "$BASE/openapi.json" "$DOCUMENT" --fail-on WARN +``` + +If the last tag predates the generator, the first form fails at that tag. Use the second form once +the document is committed, starting from the first tag that holds it. + +## Why `--fail-on WARN` + +oasdiff grades a removed query parameter as a warning, because most APIs ignore a parameter they do +not know. The generated routes do not: an unknown filter or sort field answers +`400`, so a consumer that sends it breaks. `--fail-on ERR` would pass that change. + +## What it reports + +| Change to the model | oasdiff | Why | +|---|---|---| +| Add an optional field | passes | A read gains a key, and a write gains an optional one | +| Remove a field | fails | A consumer reading it finds nothing | +| Add a `@required` field | fails | Each existing create is now refused | +| Tighten `@maxLength` or a validator | fails | A request that was valid is now refused | +| Remove `@filterable` | fails | The `filter[...]` parameters are gone, and sending one is a 400 | +| Add a new entity | passes | New paths break nobody | + +A decimal's form and a value object's null members are part of the contract too, so changing `wire` +in `.metaobjects/config.json` shows as a schema change. Fix them before you publish: +[Set the wire form](../features/contract-emitters.md#set-the-wire-form-if-you-publish-a-contract). + +## In CI + +```yaml +- uses: actions/checkout@v4 + with: { fetch-depth: 0 } # tags and history for git describe +- run: npm ci +- run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh +- run: scripts/check-api-compat.sh +``` + +Pin oasdiff to a version you have tried. New releases add checks, so a build can go red on a change +that was passing before. + +To accept a break on purpose, release it as one: a new major version of your API, with the tag that +moves the baseline. oasdiff also has an `--err-ignore` file for a single known change. diff --git a/docs/recipes/generators/typescript/json-schema.ts b/docs/recipes/generators/typescript/json-schema.ts index b6f1608a2f..42845edac6 100644 --- a/docs/recipes/generators/typescript/json-schema.ts +++ b/docs/recipes/generators/typescript/json-schema.ts @@ -1,6 +1,7 @@ // EXAMPLE GENERATOR — copy it into your repo (codegen/generators/json-schema.ts) and own it. -// It is a worked example, NOT a supported MetaObjects product surface: no release promises -// its output, and when you need a different shape you edit your copy. +// It is a worked example of writing a generator, NOT a supported MetaObjects product surface: no +// release promises its output, and when you need a different shape you edit your copy. For the +// schemas themselves, use the stock `contract-schema` generator (docs/features/contract-emitters.md). // // Wire it in metaobjects.config.ts: // import { jsonSchemaFile } from "./codegen/generators/json-schema.js"; diff --git a/docs/recipes/generators/typescript/openapi.ts b/docs/recipes/generators/typescript/openapi.ts index fbc110acb9..9938bb9991 100644 --- a/docs/recipes/generators/typescript/openapi.ts +++ b/docs/recipes/generators/typescript/openapi.ts @@ -1,6 +1,7 @@ // EXAMPLE GENERATOR — copy it into your repo (codegen/generators/openapi.ts) and own it. -// It is a worked example, NOT a supported MetaObjects product surface: no release promises -// its output, and when your API differs you edit your copy. +// It is a worked example of writing a generator, NOT a supported MetaObjects product surface: no +// release promises its output, and when your API differs you edit your copy. For the document +// itself, use the stock `openapi` generator (docs/features/contract-emitters.md). // // Wire it in metaobjects.config.ts (it reuses the field mapping in json-schema.ts, so copy // both files into the same directory): diff --git a/docs/recipes/write-your-own-generator.md b/docs/recipes/write-your-own-generator.md index ff5591da9e..fe3265453f 100644 --- a/docs/recipes/write-your-own-generator.md +++ b/docs/recipes/write-your-own-generator.md @@ -2,7 +2,7 @@ MetaObjects is a core — the typed model, the loader and registry, `verify`, `migrate`, prompt render and reply extract — and on that core **you build the generators your -application needs**: OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs, +application needs**: Zod, DTOs, a client, a service layer, a data dictionary, anything the model describes. The generators MetaObjects ships are reference examples to eject and modify, not the product ([ADR-0034 Amendment 4](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md#amendment-4-2026-09-26--write-your-own-generator-is-the-primary-path)). @@ -16,8 +16,9 @@ fails the build when committed output is stale. You register nothing for that. Write a generator when **the model fully describes an output and no reference generator emits it**, or none emits it the way your application needs it: -- a document derived from the model: JSON Schema, OpenAPI, AsyncAPI, a GraphQL SDL, an ER - diagram, a data dictionary; +- a document derived from the model: AsyncAPI, a GraphQL SDL, an ER diagram, a data + dictionary (for JSON Schema and OpenAPI, use the [stock + generators](../features/contract-emitters.md)); - code derived from the model: a DTO or client layer for a framework nothing here ships, a service or repository layer in your house style, validators in another library, test fixtures, seed data shapes; @@ -364,10 +365,11 @@ report drift on every run. ## Two worked examples: JSON Schema and OpenAPI 3.1 -These are **examples to copy, not a supported product surface.** Nothing promises their -output across releases; the moment your API differs from the reference contract they -describe, edit your copy. Each is exercised by a test so it keeps running against the -current engine. +These are **examples of writing a generator, not the way to get those files.** For the +documents themselves, use the stock [`contract-schema` and `openapi` +generators](../features/contract-emitters.md). They follow the full REST contract and are +held to it, and these examples are not. Nothing promises the examples' output across +releases. Each is exercised by a test so it keeps running against the current engine. | Port | Files | Gated by | |---|---|---| diff --git a/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md b/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md index 55cd0eb2af..1605b44375 100644 --- a/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md +++ b/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md @@ -1,7 +1,12 @@ # FR-022 — Contract emitters: JSON Schema → OpenAPI 3.1 → protobuf (design) -_Status: PROPOSED (design sketch — needs brainstorming + plan before implementation)._ -_Date: 2026-06-11._ +_Status: ACCEPTED. Phases 1 and 2 (JSON Schema, OpenAPI) are implemented for 1.2.1. Phase 3 (protobuf) +is not started and stays with binary protocols under 1.3._ +_Date: 2026-06-11. Settled and revised 2026-10-30._ + +The sketch below keeps its direction. Where building it showed a claim to be wrong, the text is +corrected and "Decisions settled" says why. The user-facing page is +[`docs/features/contract-emitters.md`](../../features/contract-emitters.md). ## Problem @@ -12,19 +17,21 @@ hand-write them — a drift surface the metadata fully describes (raison-d'être ## Design direction -All three emitters are **Tier-2** per ADR-0020 (output bytes don't depend on the -implementing language → ONE shared TS engine + golden-file conformance fixtures), wired -as ordinary generators (stable names in the generator registry, `gen --list`). They are -**one layered investment**, built in this order: +All three emitters are **Tier-2** per ADR-0020, superseded by ADR-0022 (output bytes don't +depend on the implementing language → ONE shared TS engine + golden-file conformance +fixtures), wired as ordinary generators (stable names in the generator registry, `gen +--list`). They are **one layered investment**, built in this order: ### Phase 1 — JSON Schema 2020-12, two profiles -- `jsonSchemaFile()` — per-entity / per-projection `.schema.json`: - - **Canonical profile**: full fidelity. `$defs` for shared abstracts, `format: - uuid/date-time/...`, `field.enum @values` → `enum`, currency → integer minor units - (description carries the ISO code), wire-contract-faithful types (decimal as string, - int64 as string — must match `normalization.md` exactly or the emitter itself becomes - a drift source; conformance-gated). +- `jsonSchemaFile()` (stable name `contract-schema`) — per-object `.schema.json`, with + `.create.schema.json` and `.update.schema.json` for the write shapes: + - **Canonical profile**: full fidelity. A file per object, referenced by a relative `$ref`, in + place of the sketch's `$defs`; `format: uuid/date-time/...`, `field.enum @values` → `enum`, + currency → integer minor units (description carries the ISO code), wire-contract-faithful + types: what `docs/features/api-contract.md` says each field sends, under the project's + `wire` setting. It must match exactly or the emitter itself becomes a drift source; + conformance-gated. - **Strict profile** (`profile: "strict"`): the cross-provider structured-output intersection — root `type: object`; every property `required` with `["T","null"]` unions for optionals; `additionalProperties: false` everywhere; **no recursion**; no @@ -37,14 +44,16 @@ as ordinary generators (stable names in the generator registry, `gen --list`). T ### Phase 2 — OpenAPI 3.1 -- `openapiFile()` — one document describing the generated REST surface - (accurate-by-construction: derived-CRUD routes + FR-021 declared operations), with +- `openApiFile()` — one document describing the generated REST surface + (accurate-by-construction: derived-CRUD routes), with `components.schemas` = Phase 1 canonical output (OpenAPI 3.1 schemas ARE JSON Schema 2020-12). Covers: CRUD paths, the bracketed filter grammar + operator-per-subtype gating, sort/limit/offset (+`withCount`), M:N traversal routes, error envelopes. -- `meta verify` gains a spec↔routes drift check (regen-to-temp diff, like `--codegen`). -- Emit 3.1 canonical; optional 3.0-downlevel flag (consumer tooling lag is real — - evaluate per demand). +- Drift is `meta verify --codegen`, which already re-runs every declared generator. No new + flag and no new check. +- Emit 3.1 only. A 3.0 downlevel is not built; it waits for an adopter who needs one. +- A change to the API is checked against the last release by oasdiff over two emitted + documents. It is a recipe, with no verify flag: the tool already exists and does this well. - Multiplier: openapi-generator / Kiota / orval turn the document into client SDKs in languages MetaObjects doesn't port. @@ -80,16 +89,90 @@ artifacts byte-gated from shared input metadata; strict-profile outputs validate against provider schema-subset rules; proto outputs additionally `buf lint`/`buf breaking`-clean in CI. -## Open questions - -1. Package/file layout for emitted artifacts (per-entity vs per-package files; where in - `targets` they land). -2. OpenAPI security schemes: out of metadata scope (config passthrough?) or modeled? -3. Whether Phase-1 strict profile needs per-provider sub-variants or one intersection - profile suffices (start: one). -4. proto package naming + `option java_package` etc. mapping from `::` packages. +## Decisions settled + +The sketch's open questions, and what building it decided. Each one is settled from the repo's +own rules. None needed a product decision from outside them. + +1. **Layout (open question 1).** One file per object under + `schemas//`, because a validator and a schema registry both address a schema + by file, and a per-package file would change for every object in it. One `openapi.json`, the + shape every OpenAPI tool expects. Both land under the default target and take a `target` + option like any generator. Two objects that would take one component name are refused with + `ERR_CONTRACT_NAME_COLLISION`, never suffixed: a suffix would change a public name when an + unrelated object was added. +2. **Security schemes (open question 2): options, not metadata.** Authentication sits at the + application's seam, and ADR-0030 keeps the deployment tier out of the model. An attribute for + it would have to pass ADR-0023's "cannot be computed" test for a fact the model should not + hold at all. `securitySchemes`, `security`, `servers`, `title`, `version` and `description` + pass through `openApiFile()` untouched. +3. **Strict profile (open question 3): one intersection profile, no per-provider variants.** A + strict file covers the object's create shape, the thing a model is asked to fill in, or its + read shape when it has no create shape. It carries no `$schema` or `$id`, because some hosts + refuse them, and `format`, `pattern` and the bound keywords become `description` prose. What the + subset cannot state (a map, an open object, a recursive type, a polymorphic base) is + **refused with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path**, never dropped: a + silently narrowed schema gives a model a different contract from the one declared. A project + leaves such an object out with `filter`. +4. **Proto naming (open question 4).** Left to Phase 3 with the rest of it. +5. **Stable names.** `json-schema` was already taken: it is the Java generator for metadata + files, a different concept. The emitter's stable name is `contract-schema`, its factory + `jsonSchemaFile()`. The OpenAPI generator is `openapi`, factory `openApiFile()`, the spelling + the recipe already used. +6. **A 64-bit integer is a JSON number, not a string.** The sketch said int64 as string, citing + `normalization.md`. That document is the persistence comparator; the REST contract + (`docs/features/api-contract.md`, "Type encodings") sends `field.long` as a number. So + `field.long` is `integer` with `format: int64`, and the Phase 3 note that ProtoJSON strings + 64-bit integers now states a difference from the REST wire, not a match. +7. **Decimals follow the wire setting (ADR-0019 Amendment 2).** Under `wire.decimals: "string"` a + read is a string holding the canonical pattern. Under the default it is `["string", "number"]`, + because the form belongs to the server's language and this engine is neutral: it reads the + setting and never the port. A write takes either in every mode, as the contract says. The docs + tell a project that publishes a contract to set the string form. +8. **Null members follow the wire setting too.** A top-level optional field is always present, + null when empty. A value object's optional member follows `wire.nullMembers`: `omit` is absent + and non-null, `emit` is present and nullable, and the default takes either. +9. **Three shapes per writable object.** A read, a create and an update differ in what is + required and what may be null, so one schema with `readOnly`/`writeOnly` would state neither + correctly. A create's optional field may be left out and may not be null (the generated + `InsertSchema` refuses null). An update's may be null, which clears it (FR-035). A value object + has a read and a create shape: a nested write uses the target's create shape. +10. **Times use patterns.** RFC 3339 `time` and `date-time` demand an offset, which `field.time` + and an `@localTime` timestamp do not have. A `format` would reject every real value. An instant + is `date-time` on a read, which is what routes answer, and a pattern on a write and in a filter, + because routes also take a value with no offset and read it as UTC. Holding real traffic to + the document found this: the api-contract corpus sends one, and `date-time` refused it. +11. **Generated by every project through Node.** `ports: ["typescript"]`, `tier: "neutral"`, like + `docs`. A project on another port runs the Node `meta` CLI over its model and passes `expose` + to `openApiFile()` for the verbs it mounts, since this run cannot see those routes. All five + ports answer the same REST contract, so the document is right for all of them. +12. **Stock generators, by decision (ADR-0034 Amendment 10).** Amendment 4 made these two recipes + and "not a product surface". The captain reversed that for these two. They are still reference + helpers: ejectable, and not core. +13. **List responses have named components.** `List` and `Page`, in a `oneOf`. Found by + running oasdiff: inline, every new field on a row read as a replaced `oneOf` and a compatible + change was reported as a breaking one. +14. **The recipe uses `--fail-on WARN`.** oasdiff grades a removed query parameter a warning, + because most APIs ignore a parameter they do not know. These routes answer it with a 400, so + here it is a break. +15. **Left out on purpose.** The `default` keyword (a database or `@autoSet` fact, not a wire one), + the TypeScript-only `search`, `filter[or]` and `filter[and]` extensions, and the 405 answers on read-only + surfaces (a route that does not exist is absent from the document). + +## Implementation + +- Engine: `server/typescript/packages/codegen-ts/src/contract/`. +- Generators: `generators/contract-schema-file.ts` and `generators/openapi-file.ts`, each with a + reference copy under `reference/`. +- Corpus: `fixtures/contract-conformance/`. Golden trees, every document put through Ajv and the + OpenAPI 3.1 schema, refusals as `expected-error.txt`. +- Integration: every exchange in the TypeScript generated API lanes is held to the golden document + by `integration-tests/src/api-contract-openapi-check.ts`. `integration-tests/test/contract-oasdiff.test.ts` + runs real oasdiff over two documents. ## Dependencies -- FR-021 (declared operations + `wireId`) for Phase 3 and for non-CRUD coverage in - Phase 2; Phases 1–2 are useful with derived CRUD alone and can ship first. +- Phases 1 and 2 stand on derived CRUD alone. FR-021 was closed as not planned, so the document + covers the routes the generators derive and no declared operations. +- FR-021's `wireId` was to number Phase 3's fields. That phase has to settle where they come from + before it starts. diff --git a/examples/showcase/site-payload.json b/examples/showcase/site-payload.json index cc09d97e22..82e51fcbdd 100644 --- a/examples/showcase/site-payload.json +++ b/examples/showcase/site-payload.json @@ -8,7 +8,7 @@ }, "counts": { "fixtures": 421, - "corpora": 34, + "corpora": 35, "baseTypes": 17 }, "snippets": { diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index df1815ec9b..3014906daf 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -77,7 +77,7 @@ in order and every change that needs action. ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — -or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas, +or none emits it the way this application needs it. Zod schemas, DTOs or a client for a framework nothing here ships, a service layer in the house style, a data dictionary, a search-index mapping, test fixtures. That is a generator, and you write it now, in this change — once it passes the cost test in `references/owning-generators.md` @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently. Make it deterministic: the same model must give the same bytes, or verify reports drift. Every port's minimal generator (each run through gen, verify and a convicted model -change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a -product surface — are in the guide "Write your own generator": +change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a +generator and not a product surface — are in the guide "Write your own generator": . +For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from +1.2.1 (run through the Node `meta` CLI on any port): +. **Use a stock generator as shipped** when one already emits what you need: `meta gen --list --probe` is the catalog, and each row says how to wire it with nothing diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index df1815ec9b..3014906daf 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -77,7 +77,7 @@ in order and every change that needs action. ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — -or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas, +or none emits it the way this application needs it. Zod schemas, DTOs or a client for a framework nothing here ships, a service layer in the house style, a data dictionary, a search-index mapping, test fixtures. That is a generator, and you write it now, in this change — once it passes the cost test in `references/owning-generators.md` @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently. Make it deterministic: the same model must give the same bytes, or verify reports drift. Every port's minimal generator (each run through gen, verify and a convicted model -change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a -product surface — are in the guide "Write your own generator": +change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a +generator and not a product surface — are in the guide "Write your own generator": . +For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from +1.2.1 (run through the Node `meta` CLI on any port): +. **Use a stock generator as shipped** when one already emits what you need: `meta gen --list --probe` is the catalog, and each row says how to wire it with nothing diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index df1815ec9b..3014906daf 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -77,7 +77,7 @@ in order and every change that needs action. ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — -or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas, +or none emits it the way this application needs it. Zod schemas, DTOs or a client for a framework nothing here ships, a service layer in the house style, a data dictionary, a search-index mapping, test fixtures. That is a generator, and you write it now, in this change — once it passes the cost test in `references/owning-generators.md` @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently. Make it deterministic: the same model must give the same bytes, or verify reports drift. Every port's minimal generator (each run through gen, verify and a convicted model -change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a -product surface — are in the guide "Write your own generator": +change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a +generator and not a product surface — are in the guide "Write your own generator": . +For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from +1.2.1 (run through the Node `meta` CLI on any port): +. **Use a stock generator as shipped** when one already emits what you need: `meta gen --list --probe` is the catalog, and each row says how to wire it with nothing diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index df1815ec9b..3014906daf 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -77,7 +77,7 @@ in order and every change that needs action. ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — -or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas, +or none emits it the way this application needs it. Zod schemas, DTOs or a client for a framework nothing here ships, a service layer in the house style, a data dictionary, a search-index mapping, test fixtures. That is a generator, and you write it now, in this change — once it passes the cost test in `references/owning-generators.md` @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently. Make it deterministic: the same model must give the same bytes, or verify reports drift. Every port's minimal generator (each run through gen, verify and a convicted model -change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a -product surface — are in the guide "Write your own generator": +change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a +generator and not a product surface — are in the guide "Write your own generator": . +For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from +1.2.1 (run through the Node `meta` CLI on any port): +. **Use a stock generator as shipped** when one already emits what you need: `meta gen --list --probe` is the catalog, and each row says how to wire it with nothing diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md index 0765fafc56..d0847d17ce 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -27,7 +27,7 @@ tested repository generator), For any output the model describes and no reference emits, start here: ```bash -meta generator new openapi --scope model # entity (default) | package | model +meta generator new data-dictionary --scope model # entity (default) | package | model meta gen # runs it — it already emits JSON per unit meta verify --codegen # gates it; nothing to register npx tsc -p tsconfig.codegen.json # typecheck it; meta gen loads it untyped @@ -225,6 +225,14 @@ report hook until you resync it (`meta eject`). On SQLite a decimal field of a r mount. A report with no view source, or an abstract one, generates nothing. The route and contract: `references/reporting.md` in the `metaobjects-authoring` skill. +**Contract emitters (`jsonSchemaFile()`, `openApiFile()`), from 1.2.1.** Reference helpers that +write the REST contract as standard documents. `contract-schema` writes JSON Schema 2020-12 for each +object's read, create and update shapes (`profile: "strict"` for a structured-output subset), and +`openapi` writes one OpenAPI 3.1 document. Opt-in: import them from +`@metaobjectsdev/codegen-ts/generators`, or `meta eject contract-schema openapi` to own them, and +`verify --codegen` gates the output. A project that publishes the contract should fix `wire.decimals` +and `wire.nullMembers`. Details: `docs/features/contract-emitters.md`. + **Cube export (`cubeModel()`).** A reference helper that writes the reporting vocabulary (`dimension.*`, `measure.*`, `segment.filter`, a served `object.report`) as Cube data-model files, `model/cubes/.yml`: a cube for each table-backed entity that declares any of it, a join for diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md index df1815ec9b..3014906daf 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -77,7 +77,7 @@ in order and every change that needs action. ## Need an output? Write the generator — this is the primary path **When.** You need an output the model fully describes and no reference generator emits — -or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas, +or none emits it the way this application needs it. Zod schemas, DTOs or a client for a framework nothing here ships, a service layer in the house style, a data dictionary, a search-index mapping, test fixtures. That is a generator, and you write it now, in this change — once it passes the cost test in `references/owning-generators.md` @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently. Make it deterministic: the same model must give the same bytes, or verify reports drift. Every port's minimal generator (each run through gen, verify and a convicted model -change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a -product surface — are in the guide "Write your own generator": +change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a +generator and not a product surface — are in the guide "Write your own generator": . +For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from +1.2.1 (run through the Node `meta` CLI on any port): +. **Use a stock generator as shipped** when one already emits what you need: `meta gen --list --probe` is the catalog, and each row says how to wire it with nothing diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md index 0765fafc56..d0847d17ce 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -27,7 +27,7 @@ tested repository generator), For any output the model describes and no reference emits, start here: ```bash -meta generator new openapi --scope model # entity (default) | package | model +meta generator new data-dictionary --scope model # entity (default) | package | model meta gen # runs it — it already emits JSON per unit meta verify --codegen # gates it; nothing to register npx tsc -p tsconfig.codegen.json # typecheck it; meta gen loads it untyped @@ -225,6 +225,14 @@ report hook until you resync it (`meta eject`). On SQLite a decimal field of a r mount. A report with no view source, or an abstract one, generates nothing. The route and contract: `references/reporting.md` in the `metaobjects-authoring` skill. +**Contract emitters (`jsonSchemaFile()`, `openApiFile()`), from 1.2.1.** Reference helpers that +write the REST contract as standard documents. `contract-schema` writes JSON Schema 2020-12 for each +object's read, create and update shapes (`profile: "strict"` for a structured-output subset), and +`openapi` writes one OpenAPI 3.1 document. Opt-in: import them from +`@metaobjectsdev/codegen-ts/generators`, or `meta eject contract-schema openapi` to own them, and +`verify --codegen` gates the output. A project that publishes the contract should fix `wire.decimals` +and `wire.nullMembers`. Details: `docs/features/contract-emitters.md`. + **Cube export (`cubeModel()`).** A reference helper that writes the reporting vocabulary (`dimension.*`, `measure.*`, `segment.filter`, a served `object.report`) as Cube data-model files, `model/cubes/.yml`: a cube for each table-backed entity that declares any of it, a join for diff --git a/fixtures/contract-conformance/README.md b/fixtures/contract-conformance/README.md new file mode 100644 index 0000000000..76d631643e --- /dev/null +++ b/fixtures/contract-conformance/README.md @@ -0,0 +1,44 @@ +# `contract-conformance/` — the contract emitters' corpus (FR-022) + +Golden output for `jsonSchemaFile()` (stable name `contract-schema`) and `openApiFile()` (stable +name `openapi`). Both are neutral artifacts with one engine in TypeScript (ADR-0022), so this corpus +has one runner and gates bytes, not behaviour between ports: +`server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts`. + +Each case is a directory. + +- `meta.json`, or `case.json` naming a shared metadata file. Most cases reuse an api-contract + corpus model, because that is the surface every port's routes are held to. +- `case.json` keys: `meta` (repository-relative), `wire` (the setting, as in + `.metaobjects/config.json`), `strictExclude` (objects the strict profile leaves out, because the + strict subset cannot state them) and `reportExport` (the project wires `report-export`). +- `expected/`: the exact tree the emitters write: the canonical schemas, the strict schemas and + `openapi.json`. Or `expected-error.txt`: the exact refusal. + +Regenerate with `bun run gen:contract-conformance` from `server/typescript/packages/codegen-ts`. An +`expected-error.txt` is written by hand. + +The corpus test also passes every document through tools that know nothing about MetaObjects: Ajv +in strict mode for JSON Schema 2020-12, a validator of the OpenAPI 3.1 specification's schema, and +the strict subset's rules. The integration suite then holds the OpenAPI goldens to real answers: it +checks every request of the api-contract corpus, on the generated TypeScript routes, against the +document for that corpus. + +| Case | Holds | +|---|---| +| `all-fields` | Every wire type, every constraint, a value object, a map, a recursive value object, a credential, and the write-mode fields. | +| `all-fields-wire-string-emit` | `all-fields` under `wire: { decimals: "string", nullMembers: "emit" }`. | +| `all-fields-wire-string-omit` | `all-fields` under `wire: { decimals: "string", nullMembers: "omit" }`. | +| `blog` | The api-contract corpus's root model: CRUD, filter, sort and paging. | +| `decimal` | The api-contract `decimal/` model. | +| `error-name-collision` | Refused: a real object takes the name of another object's create shape. | +| `error-name-collision-list` | Refused: a real object takes the name of another object's list response. | +| `error-strict-map` | Refused: the strict subset has no map. | +| `error-strict-recursive` | Refused: the strict subset has no recursion. | +| `jsonb` | The api-contract `jsonb/` model: value objects, arrays of them and maps. | +| `m2m` | The api-contract `m2m/` model: traversal routes, also over TPH. | +| `projection` | The api-contract `projection/` model: read-only surfaces, keyed and keyless. | +| `report` | The api-contract `report/` model: served reports, and the rest of the model around them. | +| `secret-fields` | The api-contract `secret-fields/` model: a credential is not on the wire. | +| `tph` | The api-contract `tph/` model: a polymorphic base and per-subtype routes. | +| `write-through` | The api-contract `write-through/` model. | diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/case.json b/fixtures/contract-conformance/all-fields-wire-string-emit/case.json new file mode 100644 index 0000000000..d65a4fc249 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/contract-conformance/all-fields/meta.json", "wire": { "decimals": "string", "nullMembers": "emit" }, "strictExclude": ["Bag", "Tree"] } diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/openapi.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/openapi.json new file mode 100644 index 0000000000..ee79a8a7ec --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/openapi.json @@ -0,0 +1,2346 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "all-fields-wire-string-emit", + "version": "1.0.0" + }, + "paths": { + "/api/all_fields": { + "get": { + "operationId": "listAllFields", + "tags": [ + "AllFields" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[title]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[title][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[email]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[email][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[quantity]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[quantity][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[views]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[views][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[ratio]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][eq]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][ne]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][gt]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][gte]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][lt]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][lte]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[ratio][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[amount]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[amount][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active][eq]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[startsOn]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][gt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][gte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][lt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][lte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[startsOn][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[startsAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[startsAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[publishedAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[publishedAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[priceCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[priceCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[status]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[status][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[stage]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[stage][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[stage][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "title", + "title:asc", + "title:desc", + "email", + "email:asc", + "email:desc", + "quantity", + "quantity:asc", + "quantity:desc", + "views", + "views:asc", + "views:desc", + "ratio", + "ratio:asc", + "ratio:desc", + "amount", + "amount:asc", + "amount:desc", + "active", + "active:asc", + "active:desc", + "startsOn", + "startsOn:asc", + "startsOn:desc", + "startsAt", + "startsAt:asc", + "startsAt:desc", + "publishedAt", + "publishedAt:asc", + "publishedAt:desc", + "priceCents", + "priceCents:asc", + "priceCents:desc", + "status", + "status:asc", + "status:desc", + "stage", + "stage:asc", + "stage:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AllFieldsList" + }, + { + "$ref": "#/components/schemas/AllFieldsPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/all_fields/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "get": { + "operationId": "getAllFields", + "tags": [ + "AllFields" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAllFields", + "tags": [ + "AllFields" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/bags": { + "get": { + "operationId": "listBag", + "tags": [ + "Bag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/BagList" + }, + { + "$ref": "#/components/schemas/BagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/bags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getBag", + "tags": [ + "Bag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteBag", + "tags": [ + "Bag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "maxLength": 80 + }, + "unit": { + "type": [ + "string", + "null" + ], + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + } + }, + "required": [ + "street", + "unit", + "latitude" + ] + }, + "AddressCreate": { + "title": "AddressCreate", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] + }, + "AllFields": { + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "#/components/schemas/Address" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Address" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "internalNote": { + "type": [ + "string", + "null" + ] + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + }, + "syncedBy": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "updatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "internalNote", + "issuedBy", + "syncedBy", + "createdAt", + "updatedAt" + ] + }, + "AllFieldsCreate": { + "title": "AllFieldsCreate", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": "string", + "format": "email" + }, + "host": { + "type": "string", + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": "integer", + "format": "int64" + }, + "ratio": { + "type": "number", + "format": "double" + }, + "score": { + "type": "number", + "format": "float" + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": "boolean" + }, + "startsOn": { + "type": "string", + "format": "date" + }, + "startsAt": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": "integer", + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": "string", + "enum": [ + "early", + "late" + ] + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "looseLink": { + "type": "string" + }, + "address": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "$ref": "#/components/schemas/AddressCreate" + }, + "stops": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressCreate" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": "string" + } + }, + "required": [ + "title", + "quantity", + "status" + ] + }, + "AllFieldsList": { + "title": "AllFieldsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/AllFields" + } + }, + "AllFieldsPage": { + "title": "AllFieldsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AllFields" + } + }, + "total": { + "type": "integer" + } + } + }, + "AllFieldsUpdate": { + "title": "AllFieldsUpdate", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "#/components/schemas/AddressCreate" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AddressCreate" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Bag": { + "title": "Bag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/Address" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tree" + }, + { + "type": "null" + } + ] + }, + "open": {} + }, + "required": [ + "id", + "counters", + "places", + "tree", + "open" + ] + }, + "BagCreate": { + "title": "BagCreate", + "type": "object", + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AddressCreate" + } + }, + "tree": { + "$ref": "#/components/schemas/TreeCreate" + }, + "open": {} + } + }, + "BagList": { + "title": "BagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Bag" + } + }, + "BagPage": { + "title": "BagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bag" + } + }, + "total": { + "type": "integer" + } + } + }, + "BagUpdate": { + "title": "BagUpdate", + "type": "object", + "properties": { + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/AddressCreate" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "#/components/schemas/TreeCreate" + }, + { + "type": "null" + } + ] + }, + "open": {} + } + }, + "Tree": { + "title": "Tree", + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Tree" + } + } + }, + "required": [ + "label", + "children" + ] + }, + "TreeCreate": { + "title": "TreeCreate", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TreeCreate" + } + } + }, + "required": [ + "label" + ] + } + } + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.create.schema.json new file mode 100644 index 0000000000..e6a5831fbd --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.create.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Address.create.schema.json", + "title": "Address.create", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.schema.json new file mode 100644 index 0000000000..5201c3e3c8 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Address.schema.json", + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "maxLength": 80 + }, + "unit": { + "type": [ + "string", + "null" + ], + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + } + }, + "required": [ + "street", + "unit", + "latitude" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.strict.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.strict.schema.json new file mode 100644 index 0000000000..07e408f400 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Address.strict.schema.json @@ -0,0 +1,31 @@ +{ + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.create.schema.json new file mode 100644 index 0000000000..9617555a35 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.create.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.create.schema.json", + "title": "AllFields.create", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": "string", + "format": "email" + }, + "host": { + "type": "string", + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": "integer", + "format": "int64" + }, + "ratio": { + "type": "number", + "format": "double" + }, + "score": { + "type": "number", + "format": "float" + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": "boolean" + }, + "startsOn": { + "type": "string", + "format": "date" + }, + "startsAt": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": "integer", + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": "string", + "enum": [ + "early", + "late" + ] + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "looseLink": { + "type": "string" + }, + "address": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "$ref": "./Address.create.schema.json" + }, + "stops": { + "type": "array", + "items": { + "$ref": "./Address.create.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": "string" + } + }, + "required": [ + "title", + "quantity", + "status" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.schema.json new file mode 100644 index 0000000000..e9b3852a45 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.schema.json @@ -0,0 +1,247 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.schema.json", + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "./Address.schema.json" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Address.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "internalNote": { + "type": [ + "string", + "null" + ] + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + }, + "syncedBy": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "updatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "internalNote", + "issuedBy", + "syncedBy", + "createdAt", + "updatedAt" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.strict.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.strict.schema.json new file mode 100644 index 0000000000..61e121e4bf --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.strict.schema.json @@ -0,0 +1,244 @@ +{ + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "description": "The display title. Pattern: ^(?:[A-Z].*)$. Minimum length: 3. Maximum length: 120.", + "type": "string" + }, + "email": { + "description": "Format: email.", + "type": [ + "string", + "null" + ] + }, + "host": { + "description": "Format: hostname.", + "type": [ + "string", + "null" + ] + }, + "quantity": { + "description": "Minimum: 0. Maximum: 1000.", + "type": "integer" + }, + "views": { + "type": [ + "integer", + "null" + ] + }, + "ratio": { + "type": [ + "number", + "null" + ] + }, + "score": { + "type": [ + "number", + "null" + ] + }, + "amount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "description": "Format: date.", + "type": [ + "string", + "null" + ] + }, + "startsAt": { + "description": "Pattern: ^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "publishedAt": { + "description": "An instant. A value with no offset is read as UTC. Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$.", + "type": [ + "string", + "null" + ] + }, + "wallClock": { + "description": "Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "priceCents": { + "description": "Integer minor units of USD.", + "type": [ + "integer", + "null" + ] + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "description": "Format: uri.", + "type": [ + "string", + "null" + ] + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "description": "Format: ipv4 or ipv6.", + "type": [ + "string", + "null" + ] + }, + "shipTo": { + "description": "A postal address.", + "type": [ + "object", + "null" + ], + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false + }, + "stops": { + "description": "Minimum items: 1. Maximum items: 5.", + "type": [ + "array", + "null" + ], + "items": { + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false + } + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "issuedBy" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.update.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.update.schema.json new file mode 100644 index 0000000000..5e867a0dcf --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/AllFields.update.schema.json @@ -0,0 +1,184 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.update.schema.json", + "title": "AllFields.update", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "./Address.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Address.create.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.create.schema.json new file mode 100644 index 0000000000..0e93c06b14 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.create.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.create.schema.json", + "title": "Bag.create", + "type": "object", + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": "object", + "additionalProperties": { + "$ref": "./Address.create.schema.json" + } + }, + "tree": { + "$ref": "./Tree.create.schema.json" + }, + "open": {} + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.schema.json new file mode 100644 index 0000000000..9ee7ca2b5a --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.schema.json", + "title": "Bag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Address.schema.json" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "./Tree.schema.json" + }, + { + "type": "null" + } + ] + }, + "open": {} + }, + "required": [ + "id", + "counters", + "places", + "tree", + "open" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.update.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.update.schema.json new file mode 100644 index 0000000000..ff95fa4b43 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Bag.update.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.update.schema.json", + "title": "Bag.update", + "type": "object", + "properties": { + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Address.create.schema.json" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "./Tree.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "open": {} + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.create.schema.json new file mode 100644 index 0000000000..0cd7e57d05 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.create.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Tree.create.schema.json", + "title": "Tree.create", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "items": { + "$ref": "./Tree.create.schema.json" + } + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.schema.json b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.schema.json new file mode 100644 index 0000000000..2a236c5c3f --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-emit/expected/schemas/acme/contract/Tree.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Tree.schema.json", + "title": "Tree", + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Tree.schema.json" + } + } + }, + "required": [ + "label", + "children" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/case.json b/fixtures/contract-conformance/all-fields-wire-string-omit/case.json new file mode 100644 index 0000000000..d68f000263 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/contract-conformance/all-fields/meta.json", "wire": { "decimals": "string", "nullMembers": "omit" }, "strictExclude": ["Bag", "Tree"] } diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/openapi.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/openapi.json new file mode 100644 index 0000000000..0f3e79d1c5 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/openapi.json @@ -0,0 +1,2334 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "all-fields-wire-string-omit", + "version": "1.0.0" + }, + "paths": { + "/api/all_fields": { + "get": { + "operationId": "listAllFields", + "tags": [ + "AllFields" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[title]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[title][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[email]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[email][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[quantity]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[quantity][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[views]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[views][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[ratio]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][eq]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][ne]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][gt]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][gte]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][lt]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][lte]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[ratio][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[amount]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[amount][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active][eq]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[startsOn]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][gt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][gte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][lt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][lte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[startsOn][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[startsAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[startsAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[publishedAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[publishedAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[priceCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[priceCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[status]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[status][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[stage]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[stage][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[stage][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "title", + "title:asc", + "title:desc", + "email", + "email:asc", + "email:desc", + "quantity", + "quantity:asc", + "quantity:desc", + "views", + "views:asc", + "views:desc", + "ratio", + "ratio:asc", + "ratio:desc", + "amount", + "amount:asc", + "amount:desc", + "active", + "active:asc", + "active:desc", + "startsOn", + "startsOn:asc", + "startsOn:desc", + "startsAt", + "startsAt:asc", + "startsAt:desc", + "publishedAt", + "publishedAt:asc", + "publishedAt:desc", + "priceCents", + "priceCents:asc", + "priceCents:desc", + "status", + "status:asc", + "status:desc", + "stage", + "stage:asc", + "stage:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AllFieldsList" + }, + { + "$ref": "#/components/schemas/AllFieldsPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/all_fields/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "get": { + "operationId": "getAllFields", + "tags": [ + "AllFields" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAllFields", + "tags": [ + "AllFields" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/bags": { + "get": { + "operationId": "listBag", + "tags": [ + "Bag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/BagList" + }, + { + "$ref": "#/components/schemas/BagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/bags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getBag", + "tags": [ + "Bag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteBag", + "tags": [ + "Bag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + } + }, + "required": [ + "street" + ] + }, + "AddressCreate": { + "title": "AddressCreate", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] + }, + "AllFields": { + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "#/components/schemas/Address" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Address" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "internalNote": { + "type": [ + "string", + "null" + ] + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + }, + "syncedBy": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "updatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "internalNote", + "issuedBy", + "syncedBy", + "createdAt", + "updatedAt" + ] + }, + "AllFieldsCreate": { + "title": "AllFieldsCreate", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": "string", + "format": "email" + }, + "host": { + "type": "string", + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": "integer", + "format": "int64" + }, + "ratio": { + "type": "number", + "format": "double" + }, + "score": { + "type": "number", + "format": "float" + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": "boolean" + }, + "startsOn": { + "type": "string", + "format": "date" + }, + "startsAt": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": "integer", + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": "string", + "enum": [ + "early", + "late" + ] + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "looseLink": { + "type": "string" + }, + "address": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "$ref": "#/components/schemas/AddressCreate" + }, + "stops": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressCreate" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": "string" + } + }, + "required": [ + "title", + "quantity", + "status" + ] + }, + "AllFieldsList": { + "title": "AllFieldsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/AllFields" + } + }, + "AllFieldsPage": { + "title": "AllFieldsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AllFields" + } + }, + "total": { + "type": "integer" + } + } + }, + "AllFieldsUpdate": { + "title": "AllFieldsUpdate", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "#/components/schemas/AddressCreate" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AddressCreate" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Bag": { + "title": "Bag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/Address" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tree" + }, + { + "type": "null" + } + ] + }, + "open": {} + }, + "required": [ + "id", + "counters", + "places", + "tree", + "open" + ] + }, + "BagCreate": { + "title": "BagCreate", + "type": "object", + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AddressCreate" + } + }, + "tree": { + "$ref": "#/components/schemas/TreeCreate" + }, + "open": {} + } + }, + "BagList": { + "title": "BagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Bag" + } + }, + "BagPage": { + "title": "BagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bag" + } + }, + "total": { + "type": "integer" + } + } + }, + "BagUpdate": { + "title": "BagUpdate", + "type": "object", + "properties": { + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/AddressCreate" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "#/components/schemas/TreeCreate" + }, + { + "type": "null" + } + ] + }, + "open": {} + } + }, + "Tree": { + "title": "Tree", + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tree" + } + } + }, + "required": [ + "label" + ] + }, + "TreeCreate": { + "title": "TreeCreate", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TreeCreate" + } + } + }, + "required": [ + "label" + ] + } + } + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.create.schema.json new file mode 100644 index 0000000000..e6a5831fbd --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.create.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Address.create.schema.json", + "title": "Address.create", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.schema.json new file mode 100644 index 0000000000..e6658fe606 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Address.schema.json", + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + } + }, + "required": [ + "street" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.strict.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.strict.schema.json new file mode 100644 index 0000000000..07e408f400 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Address.strict.schema.json @@ -0,0 +1,31 @@ +{ + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.create.schema.json new file mode 100644 index 0000000000..9617555a35 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.create.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.create.schema.json", + "title": "AllFields.create", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": "string", + "format": "email" + }, + "host": { + "type": "string", + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": "integer", + "format": "int64" + }, + "ratio": { + "type": "number", + "format": "double" + }, + "score": { + "type": "number", + "format": "float" + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": "boolean" + }, + "startsOn": { + "type": "string", + "format": "date" + }, + "startsAt": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": "integer", + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": "string", + "enum": [ + "early", + "late" + ] + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "looseLink": { + "type": "string" + }, + "address": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "$ref": "./Address.create.schema.json" + }, + "stops": { + "type": "array", + "items": { + "$ref": "./Address.create.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": "string" + } + }, + "required": [ + "title", + "quantity", + "status" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.schema.json new file mode 100644 index 0000000000..e9b3852a45 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.schema.json @@ -0,0 +1,247 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.schema.json", + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "./Address.schema.json" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Address.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "internalNote": { + "type": [ + "string", + "null" + ] + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + }, + "syncedBy": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "updatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "internalNote", + "issuedBy", + "syncedBy", + "createdAt", + "updatedAt" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.strict.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.strict.schema.json new file mode 100644 index 0000000000..61e121e4bf --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.strict.schema.json @@ -0,0 +1,244 @@ +{ + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "description": "The display title. Pattern: ^(?:[A-Z].*)$. Minimum length: 3. Maximum length: 120.", + "type": "string" + }, + "email": { + "description": "Format: email.", + "type": [ + "string", + "null" + ] + }, + "host": { + "description": "Format: hostname.", + "type": [ + "string", + "null" + ] + }, + "quantity": { + "description": "Minimum: 0. Maximum: 1000.", + "type": "integer" + }, + "views": { + "type": [ + "integer", + "null" + ] + }, + "ratio": { + "type": [ + "number", + "null" + ] + }, + "score": { + "type": [ + "number", + "null" + ] + }, + "amount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "description": "Format: date.", + "type": [ + "string", + "null" + ] + }, + "startsAt": { + "description": "Pattern: ^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "publishedAt": { + "description": "An instant. A value with no offset is read as UTC. Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$.", + "type": [ + "string", + "null" + ] + }, + "wallClock": { + "description": "Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "priceCents": { + "description": "Integer minor units of USD.", + "type": [ + "integer", + "null" + ] + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "description": "Format: uri.", + "type": [ + "string", + "null" + ] + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "description": "Format: ipv4 or ipv6.", + "type": [ + "string", + "null" + ] + }, + "shipTo": { + "description": "A postal address.", + "type": [ + "object", + "null" + ], + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false + }, + "stops": { + "description": "Minimum items: 1. Maximum items: 5.", + "type": [ + "array", + "null" + ], + "items": { + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false + } + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "issuedBy" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.update.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.update.schema.json new file mode 100644 index 0000000000..5e867a0dcf --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/AllFields.update.schema.json @@ -0,0 +1,184 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.update.schema.json", + "title": "AllFields.update", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "./Address.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Address.create.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.create.schema.json new file mode 100644 index 0000000000..0e93c06b14 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.create.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.create.schema.json", + "title": "Bag.create", + "type": "object", + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": "object", + "additionalProperties": { + "$ref": "./Address.create.schema.json" + } + }, + "tree": { + "$ref": "./Tree.create.schema.json" + }, + "open": {} + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.schema.json new file mode 100644 index 0000000000..9ee7ca2b5a --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.schema.json", + "title": "Bag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Address.schema.json" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "./Tree.schema.json" + }, + { + "type": "null" + } + ] + }, + "open": {} + }, + "required": [ + "id", + "counters", + "places", + "tree", + "open" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.update.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.update.schema.json new file mode 100644 index 0000000000..ff95fa4b43 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Bag.update.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.update.schema.json", + "title": "Bag.update", + "type": "object", + "properties": { + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Address.create.schema.json" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "./Tree.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "open": {} + } +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.create.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.create.schema.json new file mode 100644 index 0000000000..0cd7e57d05 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.create.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Tree.create.schema.json", + "title": "Tree.create", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "items": { + "$ref": "./Tree.create.schema.json" + } + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.schema.json b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.schema.json new file mode 100644 index 0000000000..a16e8378a2 --- /dev/null +++ b/fixtures/contract-conformance/all-fields-wire-string-omit/expected/schemas/acme/contract/Tree.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Tree.schema.json", + "title": "Tree", + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "./Tree.schema.json" + } + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/all-fields/case.json b/fixtures/contract-conformance/all-fields/case.json new file mode 100644 index 0000000000..828f818666 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/case.json @@ -0,0 +1 @@ +{ "strictExclude": ["Bag", "Tree"] } diff --git a/fixtures/contract-conformance/all-fields/expected/openapi.json b/fixtures/contract-conformance/all-fields/expected/openapi.json new file mode 100644 index 0000000000..975be7c0ab --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/openapi.json @@ -0,0 +1,2345 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "all-fields", + "version": "1.0.0" + }, + "paths": { + "/api/all_fields": { + "get": { + "operationId": "listAllFields", + "tags": [ + "AllFields" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[title]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[title][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[email]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[email][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[email][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[quantity]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[quantity][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[views]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[views][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[views][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[ratio]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][eq]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][ne]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][gt]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][gte]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][lt]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][lte]", + "in": "query", + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "filter[ratio][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[ratio][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[amount]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[amount][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active][eq]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[active][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[startsOn]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][gt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][gte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][lt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][lte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[startsOn][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[startsOn][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[startsAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + } + }, + { + "name": "filter[startsAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[startsAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[publishedAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[publishedAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[publishedAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[priceCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[priceCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[priceCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[status]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "draft", + "live" + ] + } + }, + { + "name": "filter[status][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[status][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[stage]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "early", + "late" + ] + } + }, + { + "name": "filter[stage][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[stage][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[stage][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "title", + "title:asc", + "title:desc", + "email", + "email:asc", + "email:desc", + "quantity", + "quantity:asc", + "quantity:desc", + "views", + "views:asc", + "views:desc", + "ratio", + "ratio:asc", + "ratio:desc", + "amount", + "amount:asc", + "amount:desc", + "active", + "active:asc", + "active:desc", + "startsOn", + "startsOn:asc", + "startsOn:desc", + "startsAt", + "startsAt:asc", + "startsAt:desc", + "publishedAt", + "publishedAt:asc", + "publishedAt:desc", + "priceCents", + "priceCents:asc", + "priceCents:desc", + "status", + "status:asc", + "status:desc", + "stage", + "stage:asc", + "stage:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AllFieldsList" + }, + { + "$ref": "#/components/schemas/AllFieldsPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/all_fields/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "get": { + "operationId": "getAllFields", + "tags": [ + "AllFields" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putAllFields", + "tags": [ + "AllFields" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFieldsUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllFields" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAllFields", + "tags": [ + "AllFields" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/bags": { + "get": { + "operationId": "listBag", + "tags": [ + "Bag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/BagList" + }, + { + "$ref": "#/components/schemas/BagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/bags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getBag", + "tags": [ + "Bag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putBag", + "tags": [ + "Bag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Bag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteBag", + "tags": [ + "Bag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "maxLength": 80 + }, + "unit": { + "type": [ + "string", + "null" + ], + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] + }, + "AddressCreate": { + "title": "AddressCreate", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] + }, + "AllFields": { + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "#/components/schemas/Address" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Address" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "internalNote": { + "type": [ + "string", + "null" + ] + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + }, + "syncedBy": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "updatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "internalNote", + "issuedBy", + "syncedBy", + "createdAt", + "updatedAt" + ] + }, + "AllFieldsCreate": { + "title": "AllFieldsCreate", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": "string", + "format": "email" + }, + "host": { + "type": "string", + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": "integer", + "format": "int64" + }, + "ratio": { + "type": "number", + "format": "double" + }, + "score": { + "type": "number", + "format": "float" + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": "boolean" + }, + "startsOn": { + "type": "string", + "format": "date" + }, + "startsAt": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": "integer", + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": "string", + "enum": [ + "early", + "late" + ] + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "looseLink": { + "type": "string" + }, + "address": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "$ref": "#/components/schemas/AddressCreate" + }, + "stops": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddressCreate" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": "string" + } + }, + "required": [ + "title", + "quantity", + "status" + ] + }, + "AllFieldsList": { + "title": "AllFieldsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/AllFields" + } + }, + "AllFieldsPage": { + "title": "AllFieldsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AllFields" + } + }, + "total": { + "type": "integer" + } + } + }, + "AllFieldsUpdate": { + "title": "AllFieldsUpdate", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "#/components/schemas/AddressCreate" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AddressCreate" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Bag": { + "title": "Bag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/Address" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tree" + }, + { + "type": "null" + } + ] + }, + "open": {} + }, + "required": [ + "id", + "counters", + "places", + "tree", + "open" + ] + }, + "BagCreate": { + "title": "BagCreate", + "type": "object", + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AddressCreate" + } + }, + "tree": { + "$ref": "#/components/schemas/TreeCreate" + }, + "open": {} + } + }, + "BagList": { + "title": "BagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Bag" + } + }, + "BagPage": { + "title": "BagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bag" + } + }, + "total": { + "type": "integer" + } + } + }, + "BagUpdate": { + "title": "BagUpdate", + "type": "object", + "properties": { + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/AddressCreate" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "#/components/schemas/TreeCreate" + }, + { + "type": "null" + } + ] + }, + "open": {} + } + }, + "Tree": { + "title": "Tree", + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Tree" + } + } + }, + "required": [ + "label" + ] + }, + "TreeCreate": { + "title": "TreeCreate", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TreeCreate" + } + } + }, + "required": [ + "label" + ] + } + } + } +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.create.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.create.schema.json new file mode 100644 index 0000000000..e6a5831fbd --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.create.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Address.create.schema.json", + "title": "Address.create", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "unit": { + "type": "string", + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.schema.json new file mode 100644 index 0000000000..38793b0d70 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Address.schema.json", + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "type": "string", + "maxLength": 80 + }, + "unit": { + "type": [ + "string", + "null" + ], + "maxLength": 10 + }, + "latitude": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 9, scale 6." + } + }, + "required": [ + "street" + ] +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.strict.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.strict.schema.json new file mode 100644 index 0000000000..07e408f400 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Address.strict.schema.json @@ -0,0 +1,31 @@ +{ + "title": "Address", + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.create.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.create.schema.json new file mode 100644 index 0000000000..9617555a35 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.create.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.create.schema.json", + "title": "AllFields.create", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": "string", + "format": "email" + }, + "host": { + "type": "string", + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": "integer", + "format": "int64" + }, + "ratio": { + "type": "number", + "format": "double" + }, + "score": { + "type": "number", + "format": "float" + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": "boolean" + }, + "startsOn": { + "type": "string", + "format": "date" + }, + "startsAt": { + "type": "string", + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": "integer", + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": "string", + "enum": [ + "early", + "late" + ] + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "looseLink": { + "type": "string" + }, + "address": { + "type": "string", + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "$ref": "./Address.create.schema.json" + }, + "stops": { + "type": "array", + "items": { + "$ref": "./Address.create.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": "string" + } + }, + "required": [ + "title", + "quantity", + "status" + ] +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.schema.json new file mode 100644 index 0000000000..3b7f7d4c4b --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.schema.json @@ -0,0 +1,248 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.schema.json", + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "./Address.schema.json" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Address.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "internalNote": { + "type": [ + "string", + "null" + ] + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + }, + "syncedBy": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "updatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "internalNote", + "issuedBy", + "syncedBy", + "createdAt", + "updatedAt" + ] +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.strict.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.strict.schema.json new file mode 100644 index 0000000000..61e121e4bf --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.strict.schema.json @@ -0,0 +1,244 @@ +{ + "title": "AllFields", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "description": "The display title. Pattern: ^(?:[A-Z].*)$. Minimum length: 3. Maximum length: 120.", + "type": "string" + }, + "email": { + "description": "Format: email.", + "type": [ + "string", + "null" + ] + }, + "host": { + "description": "Format: hostname.", + "type": [ + "string", + "null" + ] + }, + "quantity": { + "description": "Minimum: 0. Maximum: 1000.", + "type": "integer" + }, + "views": { + "type": [ + "integer", + "null" + ] + }, + "ratio": { + "type": [ + "number", + "null" + ] + }, + "score": { + "type": [ + "number", + "null" + ] + }, + "amount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "description": "Format: date.", + "type": [ + "string", + "null" + ] + }, + "startsAt": { + "description": "Pattern: ^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "publishedAt": { + "description": "An instant. A value with no offset is read as UTC. Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$.", + "type": [ + "string", + "null" + ] + }, + "wallClock": { + "description": "Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "priceCents": { + "description": "Integer minor units of USD.", + "type": [ + "integer", + "null" + ] + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "description": "Format: uri.", + "type": [ + "string", + "null" + ] + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "description": "Format: ipv4 or ipv6.", + "type": [ + "string", + "null" + ] + }, + "shipTo": { + "description": "A postal address.", + "type": [ + "object", + "null" + ], + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false + }, + "stops": { + "description": "Minimum items: 1. Maximum items: 5.", + "type": [ + "array", + "null" + ], + "items": { + "description": "A postal address.", + "type": "object", + "properties": { + "street": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "unit": { + "description": "Maximum length: 10.", + "type": [ + "string", + "null" + ] + }, + "latitude": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 6. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street", + "unit", + "latitude" + ], + "additionalProperties": false + } + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "issuedBy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "title", + "email", + "host", + "quantity", + "views", + "ratio", + "score", + "amount", + "active", + "startsOn", + "startsAt", + "publishedAt", + "wallClock", + "priceCents", + "status", + "stage", + "homepage", + "looseLink", + "address", + "shipTo", + "stops", + "tags", + "issuedBy" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.update.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.update.schema.json new file mode 100644 index 0000000000..5e867a0dcf --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/AllFields.update.schema.json @@ -0,0 +1,184 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/AllFields.update.schema.json", + "title": "AllFields.update", + "description": "One field of every wire type.", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 120, + "pattern": "^(?:[A-Z].*)$", + "description": "The display title." + }, + "email": { + "type": [ + "string", + "null" + ], + "format": "email" + }, + "host": { + "type": [ + "string", + "null" + ], + "format": "hostname" + }, + "quantity": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 1000 + }, + "views": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "score": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "active": { + "type": [ + "boolean", + "null" + ] + }, + "startsOn": { + "type": [ + "string", + "null" + ], + "format": "date" + }, + "startsAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "wallClock": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$" + }, + "priceCents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Integer minor units of USD." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "live" + ] + }, + "stage": { + "type": [ + "string", + "null" + ], + "enum": [ + "early", + "late", + null + ] + }, + "homepage": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "looseLink": { + "type": [ + "string", + "null" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "anyOf": [ + { + "format": "ipv4" + }, + { + "format": "ipv6" + } + ] + }, + "shipTo": { + "anyOf": [ + { + "$ref": "./Address.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "stops": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Address.create.schema.json" + }, + "minItems": 1, + "maxItems": 5 + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.create.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.create.schema.json new file mode 100644 index 0000000000..0e93c06b14 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.create.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.create.schema.json", + "title": "Bag.create", + "type": "object", + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": "object", + "additionalProperties": { + "$ref": "./Address.create.schema.json" + } + }, + "tree": { + "$ref": "./Tree.create.schema.json" + }, + "open": {} + } +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.schema.json new file mode 100644 index 0000000000..9ee7ca2b5a --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.schema.json", + "title": "Bag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Address.schema.json" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "./Tree.schema.json" + }, + { + "type": "null" + } + ] + }, + "open": {} + }, + "required": [ + "id", + "counters", + "places", + "tree", + "open" + ] +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.update.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.update.schema.json new file mode 100644 index 0000000000..ff95fa4b43 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Bag.update.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Bag.update.schema.json", + "title": "Bag.update", + "type": "object", + "properties": { + "counters": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "integer", + "format": "int32" + } + }, + "places": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Address.create.schema.json" + } + }, + "tree": { + "anyOf": [ + { + "$ref": "./Tree.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "open": {} + } +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.create.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.create.schema.json new file mode 100644 index 0000000000..0cd7e57d05 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.create.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Tree.create.schema.json", + "title": "Tree.create", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "items": { + "$ref": "./Tree.create.schema.json" + } + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.schema.json b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.schema.json new file mode 100644 index 0000000000..288001dd13 --- /dev/null +++ b/fixtures/contract-conformance/all-fields/expected/schemas/acme/contract/Tree.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Tree.schema.json", + "title": "Tree", + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Tree.schema.json" + } + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/all-fields/meta.json b/fixtures/contract-conformance/all-fields/meta.json new file mode 100644 index 0000000000..19cb18136e --- /dev/null +++ b/fixtures/contract-conformance/all-fields/meta.json @@ -0,0 +1,81 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { "object.value": { + "name": "Address", + "@description": "A postal address.", + "children": [ + { "field.string": { "name": "street", "@required": true, "@maxLength": 80 } }, + { "field.string": { "name": "unit", "@maxLength": 10 } }, + { "field.decimal": { "name": "latitude", "@precision": 9, "@scale": 6 } } + ] + }}, + { "object.value": { + "name": "Tree", + "children": [ + { "field.string": { "name": "label", "@required": true } }, + { "field.object": { "name": "children", "@objectRef": "Tree", "isArray": true, "@storage": "jsonb" } } + ] + }}, + { "object.entity": { + "name": "AllFields", + "@description": "One field of every wire type.", + "children": [ + { "source.rdb": { "@table": "all_fields" } }, + { "field.uuid": { "name": "id", "@filterable": true } }, + { "field.string": { "name": "title", "@required": true, "@maxLength": 120, "@filterable": true, "@sortable": true, "@description": "The display title.", "children": [ + { "validator.length": { "name": "titleLength", "@min": 3 } }, + { "validator.regex": { "name": "titleShape", "@pattern": "[A-Z].*" } } + ]}}, + { "field.string": { "name": "email", "@stringFormat": "email", "@filterable": true } }, + { "field.string": { "name": "host", "@stringFormat": "hostname" } }, + { "field.int": { "name": "quantity", "@required": true, "@filterable": true, "@sortable": true, "children": [ + { "validator.numeric": { "name": "quantityRange", "@min": 0, "@max": 1000 } } + ]}}, + { "field.long": { "name": "views", "@filterable": true } }, + { "field.double": { "name": "ratio", "@filterable": true } }, + { "field.float": { "name": "score" } }, + { "field.decimal": { "name": "amount", "@precision": 10, "@scale": 2, "@filterable": true } }, + { "field.boolean": { "name": "active", "@filterable": true } }, + { "field.date": { "name": "startsOn", "@filterable": true } }, + { "field.time": { "name": "startsAt", "@filterable": true } }, + { "field.timestamp": { "name": "publishedAt", "@filterable": true } }, + { "field.timestamp": { "name": "wallClock", "@localTime": true } }, + { "field.currency": { "name": "priceCents", "@currency": "USD", "@filterable": true } }, + { "field.enum": { "name": "status", "@values": ["draft", "live"], "@required": true, "@filterable": true } }, + { "field.enum": { "name": "stage", "@values": ["early", "late"], "@filterable": true } }, + { "field.uri": { "name": "homepage" } }, + { "field.uri": { "name": "looseLink", "@lenient": true } }, + { "field.inet": { "name": "address" } }, + { "field.object": { "name": "shipTo", "@objectRef": "Address", "@storage": "jsonb" } }, + { "field.object": { "name": "stops", "@objectRef": "Address", "isArray": true, "@storage": "jsonb", "children": [ + { "validator.array": { "name": "stopsCount", "@min": 1, "@max": 5 } } + ]}}, + { "field.string": { "name": "tags", "isArray": true } }, + { "field.string": { "name": "password", "children": [ + { "view.password": { "name": "secret" } } + ]}}, + { "field.string": { "name": "internalNote", "@formExclude": true } }, + { "field.string": { "name": "issuedBy", "@mutability": "writeOnce" } }, + { "field.string": { "name": "syncedBy", "@mutability": "readOnly" } }, + { "field.timestamp": { "name": "createdAt", "@autoSet": "onCreate" } }, + { "field.timestamp": { "name": "updatedAt", "@autoSet": "onUpdate" } }, + { "identity.primary": { "name": "pk", "@fields": "id", "@generation": "uuid" } } + ] + }}, + { "object.entity": { + "name": "Bag", + "children": [ + { "source.rdb": { "@table": "bags" } }, + { "field.long": { "name": "id" } }, + { "field.map": { "name": "counters", "@valueType": "int", "@storage": "jsonb" } }, + { "field.map": { "name": "places", "@objectRef": "Address", "@storage": "jsonb" } }, + { "field.object": { "name": "tree", "@objectRef": "Tree", "@storage": "jsonb" } }, + { "field.string": { "name": "open", "@dbColumnType": "jsonb" } }, + { "identity.primary": { "name": "pk", "@fields": "id", "@generation": "increment" } } + ] + }} + ] + } +} diff --git a/fixtures/contract-conformance/blog/case.json b/fixtures/contract-conformance/blog/case.json new file mode 100644 index 0000000000..c90b3c8185 --- /dev/null +++ b/fixtures/contract-conformance/blog/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/meta.json" } diff --git a/fixtures/contract-conformance/blog/expected/openapi.json b/fixtures/contract-conformance/blog/expected/openapi.json new file mode 100644 index 0000000000..b9840d0b52 --- /dev/null +++ b/fixtures/contract-conformance/blog/expected/openapi.json @@ -0,0 +1,724 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "blog", + "version": "1.0.0" + }, + "paths": { + "/api/authors": { + "get": { + "operationId": "listAuthor", + "tags": [ + "Author" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[name]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[name][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[bio]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bio][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bio][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bio][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[bio][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bio][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[createdAt]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + }, + { + "name": "filter[createdAt][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[createdAt][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "name", + "name:asc", + "name:desc", + "createdAt", + "createdAt:asc", + "createdAt:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AuthorList" + }, + { + "$ref": "#/components/schemas/AuthorPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createAuthor", + "tags": [ + "Author" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthorCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Author" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/authors/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getAuthor", + "tags": [ + "Author" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Author" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAuthor", + "tags": [ + "Author" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthorUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Author" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putAuthor", + "tags": [ + "Author" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthorUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Author" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAuthor", + "tags": [ + "Author" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Author": { + "title": "Author", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "bio": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "bio", + "createdAt", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt" + ] + }, + "AuthorCreate": { + "title": "AuthorCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "bio": { + "type": "string", + "maxLength": 1000 + }, + "createdAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + } + }, + "required": [ + "name", + "createdAt" + ] + }, + "AuthorList": { + "title": "AuthorList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Author" + } + }, + "AuthorPage": { + "title": "AuthorPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Author" + } + }, + "total": { + "type": "integer" + } + } + }, + "AuthorUpdate": { + "title": "AuthorUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "bio": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "createdAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.create.schema.json b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.create.schema.json new file mode 100644 index 0000000000..dd93120f2e --- /dev/null +++ b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.create.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/blog/Author.create.schema.json", + "title": "Author.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "bio": { + "type": "string", + "maxLength": 1000 + }, + "createdAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + } + }, + "required": [ + "name", + "createdAt" + ] +} diff --git a/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.schema.json b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.schema.json new file mode 100644 index 0000000000..15e9f54982 --- /dev/null +++ b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/blog/Author.schema.json", + "title": "Author", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "bio": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "bio", + "createdAt", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt" + ] +} diff --git a/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.strict.schema.json b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.strict.schema.json new file mode 100644 index 0000000000..58723ebbb6 --- /dev/null +++ b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.strict.schema.json @@ -0,0 +1,35 @@ +{ + "title": "Author", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1. Maximum length: 100.", + "type": "string" + }, + "bio": { + "description": "Maximum length: 1000.", + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "description": "An instant. A value with no offset is read as UTC. Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$.", + "type": "string" + }, + "issuedCurrency": { + "description": "Maximum length: 3.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "bio", + "createdAt", + "issuedCurrency" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.update.schema.json b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.update.schema.json new file mode 100644 index 0000000000..3f3c050341 --- /dev/null +++ b/fixtures/contract-conformance/blog/expected/schemas/acme/blog/Author.update.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/blog/Author.update.schema.json", + "title": "Author.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "bio": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "createdAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + } + } +} diff --git a/fixtures/contract-conformance/decimal/case.json b/fixtures/contract-conformance/decimal/case.json new file mode 100644 index 0000000000..a3c6c54562 --- /dev/null +++ b/fixtures/contract-conformance/decimal/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/decimal/meta.json" } diff --git a/fixtures/contract-conformance/decimal/expected/openapi.json b/fixtures/contract-conformance/decimal/expected/openapi.json new file mode 100644 index 0000000000..b596813ec9 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/openapi.json @@ -0,0 +1,1893 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "decimal", + "version": "1.0.0" + }, + "paths": { + "/api/entries": { + "get": { + "operationId": "listEntry", + "tags": [ + "Entry" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[bucket]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[bucket][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[amount]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[amount][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[amount][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "bucket", + "bucket:asc", + "bucket:desc", + "amount", + "amount:asc", + "amount:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/EntryList" + }, + { + "$ref": "#/components/schemas/EntryPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createEntry", + "tags": [ + "Entry" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntryCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entry" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/entries/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getEntry", + "tags": [ + "Entry" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entry" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateEntry", + "tags": [ + "Entry" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntryUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entry" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putEntry", + "tags": [ + "Entry" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntryUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entry" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteEntry", + "tags": [ + "Entry" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/entry_totals": { + "get": { + "operationId": "listEntryTotals", + "tags": [ + "EntryTotals" + ], + "parameters": [ + { + "name": "filter[bucket]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[bucket][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[bucket][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[entries]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[entries][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[entries][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[total]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[total][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[total][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[totalOrZero]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[totalOrZero][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[totalOrZero][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[mean]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[mean][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[mean][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[top]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[top][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[top][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[flaggedShare]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[flaggedShare][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[flaggedShare][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[perFlagged]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[perFlagged][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[perFlagged][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "bucket", + "bucket:asc", + "bucket:desc", + "entries", + "entries:asc", + "entries:desc", + "total", + "total:asc", + "total:desc", + "totalOrZero", + "totalOrZero:asc", + "totalOrZero:desc", + "mean", + "mean:asc", + "mean:desc", + "top", + "top:asc", + "top:desc", + "flaggedShare", + "flaggedShare:asc", + "flaggedShare:desc", + "perFlagged", + "perFlagged:asc", + "perFlagged:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/EntryTotalsList" + }, + { + "$ref": "#/components/schemas/EntryTotalsPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/samples": { + "get": { + "operationId": "listSample", + "tags": [ + "Sample" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SampleList" + }, + { + "$ref": "#/components/schemas/SamplePage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createSample", + "tags": [ + "Sample" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/samples/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getSample", + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateSample", + "tags": [ + "Sample" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putSample", + "tags": [ + "Sample" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteSample", + "tags": [ + "Sample" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Entry": { + "title": "Entry", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "bucket": { + "type": "string", + "maxLength": 8 + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + }, + "flagged": { + "type": "boolean" + } + }, + "required": [ + "id", + "bucket", + "amount", + "flagged" + ] + }, + "EntryCreate": { + "title": "EntryCreate", + "type": "object", + "properties": { + "bucket": { + "type": "string", + "minLength": 1, + "maxLength": 8 + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 28, scale 10." + }, + "flagged": { + "type": "boolean" + } + }, + "required": [ + "bucket", + "flagged" + ] + }, + "EntryList": { + "title": "EntryList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Entry" + } + }, + "EntryPage": { + "title": "EntryPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entry" + } + }, + "total": { + "type": "integer" + } + } + }, + "EntryTotals": { + "title": "EntryTotals", + "type": "object", + "properties": { + "bucket": { + "type": "string", + "maxLength": 8 + }, + "entries": { + "type": "integer", + "format": "int64" + }, + "total": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "totalOrZero": { + "type": [ + "string", + "number" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "mean": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "top": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + }, + "flaggedShare": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "perFlagged": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + } + }, + "required": [ + "bucket", + "entries", + "total", + "totalOrZero", + "mean", + "top", + "flaggedShare", + "perFlagged" + ] + }, + "EntryTotalsList": { + "title": "EntryTotalsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/EntryTotals" + } + }, + "EntryTotalsPage": { + "title": "EntryTotalsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntryTotals" + } + }, + "total": { + "type": "integer" + } + } + }, + "EntryUpdate": { + "title": "EntryUpdate", + "type": "object", + "properties": { + "bucket": { + "type": "string", + "minLength": 1, + "maxLength": 8 + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 28, scale 10." + }, + "flagged": { + "type": "boolean" + } + } + }, + "Sample": { + "title": "Sample", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "readings": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "number" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 9, scale 4." + } + }, + "free": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + } + }, + "required": [ + "id", + "readings", + "free" + ] + }, + "SampleCreate": { + "title": "SampleCreate", + "type": "object", + "properties": { + "readings": { + "type": "array", + "items": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 4." + } + }, + "free": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number." + } + } + }, + "SampleList": { + "title": "SampleList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Sample" + } + }, + "SamplePage": { + "title": "SamplePage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sample" + } + }, + "total": { + "type": "integer" + } + } + }, + "SampleUpdate": { + "title": "SampleUpdate", + "type": "object", + "properties": { + "readings": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 4." + } + }, + "free": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number." + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.create.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.create.schema.json new file mode 100644 index 0000000000..9641cb4632 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.create.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/Entry.create.schema.json", + "title": "Entry.create", + "type": "object", + "properties": { + "bucket": { + "type": "string", + "minLength": 1, + "maxLength": 8 + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 28, scale 10." + }, + "flagged": { + "type": "boolean" + } + }, + "required": [ + "bucket", + "flagged" + ] +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json new file mode 100644 index 0000000000..10b241eec9 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/Entry.schema.json", + "title": "Entry", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "bucket": { + "type": "string", + "maxLength": 8 + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + }, + "flagged": { + "type": "boolean" + } + }, + "required": [ + "id", + "bucket", + "amount", + "flagged" + ] +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.strict.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.strict.schema.json new file mode 100644 index 0000000000..56c14d33c8 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.strict.schema.json @@ -0,0 +1,26 @@ +{ + "title": "Entry", + "type": "object", + "properties": { + "bucket": { + "description": "Minimum length: 1. Maximum length: 8.", + "type": "string" + }, + "amount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 28, scale 10. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "flagged": { + "type": "boolean" + } + }, + "required": [ + "bucket", + "amount", + "flagged" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.update.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.update.schema.json new file mode 100644 index 0000000000..d496367f08 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.update.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/Entry.update.schema.json", + "title": "Entry.update", + "type": "object", + "properties": { + "bucket": { + "type": "string", + "minLength": 1, + "maxLength": 8 + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 28, scale 10." + }, + "flagged": { + "type": "boolean" + } + } +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json new file mode 100644 index 0000000000..4bdeacec70 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/EntryTotals.schema.json", + "title": "EntryTotals", + "type": "object", + "properties": { + "bucket": { + "type": "string", + "maxLength": 8 + }, + "entries": { + "type": "integer", + "format": "int64" + }, + "total": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "totalOrZero": { + "type": [ + "string", + "number" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "mean": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "top": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + }, + "flaggedShare": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + }, + "perFlagged": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + } + }, + "required": [ + "bucket", + "entries", + "total", + "totalOrZero", + "mean", + "top", + "flaggedShare", + "perFlagged" + ] +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json new file mode 100644 index 0000000000..8431c36d0d --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json @@ -0,0 +1,63 @@ +{ + "title": "EntryTotals", + "type": "object", + "properties": { + "bucket": { + "description": "Maximum length: 8.", + "type": "string" + }, + "entries": { + "type": "integer" + }, + "total": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "type": [ + "string", + "null" + ] + }, + "totalOrZero": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "type": "string" + }, + "mean": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "type": [ + "string", + "null" + ] + }, + "top": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10.", + "type": [ + "string", + "null" + ] + }, + "flaggedShare": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "type": [ + "string", + "null" + ] + }, + "perFlagged": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "bucket", + "entries", + "total", + "totalOrZero", + "mean", + "top", + "flaggedShare", + "perFlagged" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.create.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.create.schema.json new file mode 100644 index 0000000000..74e3590229 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.create.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/Sample.create.schema.json", + "title": "Sample.create", + "type": "object", + "properties": { + "readings": { + "type": "array", + "items": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 4." + } + }, + "free": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number." + } + } +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json new file mode 100644 index 0000000000..f51a204783 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/Sample.schema.json", + "title": "Sample", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "readings": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "number" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 9, scale 4." + } + }, + "free": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + } + }, + "required": [ + "id", + "readings", + "free" + ] +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.strict.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.strict.schema.json new file mode 100644 index 0000000000..e094dc6e27 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.strict.schema.json @@ -0,0 +1,28 @@ +{ + "title": "Sample", + "type": "object", + "properties": { + "readings": { + "type": [ + "array", + "null" + ], + "items": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 4. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": "string" + } + }, + "free": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "readings", + "free" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.update.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.update.schema.json new file mode 100644 index 0000000000..1b38404d01 --- /dev/null +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.update.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/ledger/Sample.update.schema.json", + "title": "Sample.update", + "type": "object", + "properties": { + "readings": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 9, scale 4." + } + }, + "free": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number." + } + } +} diff --git a/fixtures/contract-conformance/error-name-collision-list/expected-error.txt b/fixtures/contract-conformance/error-name-collision-list/expected-error.txt new file mode 100644 index 0000000000..e881459f75 --- /dev/null +++ b/fixtures/contract-conformance/error-name-collision-list/expected-error.txt @@ -0,0 +1 @@ +[ERR_CONTRACT_NAME_COLLISION] the OpenAPI component 'OrderList' is both 'acme::contract::OrderList' (read shape) and the list response of 'acme::contract::Order'. Rename one, or leave one out with the generator's filter option. diff --git a/fixtures/contract-conformance/error-name-collision-list/meta.json b/fixtures/contract-conformance/error-name-collision-list/meta.json new file mode 100644 index 0000000000..e02c81b7be --- /dev/null +++ b/fixtures/contract-conformance/error-name-collision-list/meta.json @@ -0,0 +1,30 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { "object.entity": { + "name": "Order", + "children": [ + { "source.rdb": { "@table": "orders" } }, + { "field.long": { "name": "id" } }, + { "identity.primary": { "name": "pk", "@fields": "id", "@generation": "increment" } } + ] + }}, + { "object.value": { + "name": "OrderList", + "children": [ + { "field.string": { "name": "label", "@required": true } } + ] + }}, + { "object.entity": { + "name": "Basket", + "children": [ + { "source.rdb": { "@table": "baskets" } }, + { "field.long": { "name": "id" } }, + { "field.object": { "name": "orders", "@objectRef": "OrderList", "@storage": "jsonb" } }, + { "identity.primary": { "name": "pk", "@fields": "id", "@generation": "increment" } } + ] + }} + ] + } +} diff --git a/fixtures/contract-conformance/error-name-collision/expected-error.txt b/fixtures/contract-conformance/error-name-collision/expected-error.txt new file mode 100644 index 0000000000..fee36d2138 --- /dev/null +++ b/fixtures/contract-conformance/error-name-collision/expected-error.txt @@ -0,0 +1 @@ +[ERR_CONTRACT_NAME_COLLISION] the OpenAPI component 'OrderCreate' is both 'acme::contract::Order' (create shape) and 'acme::contract::OrderCreate' (read shape). Rename one, or leave one out with the generator's filter option. diff --git a/fixtures/contract-conformance/error-name-collision/meta.json b/fixtures/contract-conformance/error-name-collision/meta.json new file mode 100644 index 0000000000..b10842a234 --- /dev/null +++ b/fixtures/contract-conformance/error-name-collision/meta.json @@ -0,0 +1,19 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { "object.entity": { + "name": "Order", + "children": [ + { "source.rdb": { "@table": "orders" } }, + { "field.long": { "name": "id" } }, + { "identity.primary": { "name": "pk", "@fields": "id", "@generation": "increment" } } + ] + }}, + { "object.value": { + "name": "OrderCreate", + "children": [ { "field.string": { "name": "note" } } ] + }} + ] + } +} diff --git a/fixtures/contract-conformance/error-strict-map/expected-error.txt b/fixtures/contract-conformance/error-strict-map/expected-error.txt new file mode 100644 index 0000000000..c356fe3987 --- /dev/null +++ b/fixtures/contract-conformance/error-strict-map/expected-error.txt @@ -0,0 +1 @@ +[ERR_CONTRACT_STRICT_UNREPRESENTABLE] 'acme::contract::Counters' has no strict schema: a map has keys no schema can list (at 'byName'). Leave it out with the generator's filter option. diff --git a/fixtures/contract-conformance/error-strict-map/meta.json b/fixtures/contract-conformance/error-strict-map/meta.json new file mode 100644 index 0000000000..a1d05e98de --- /dev/null +++ b/fixtures/contract-conformance/error-strict-map/meta.json @@ -0,0 +1,11 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { "object.value": { + "name": "Counters", + "children": [ { "field.map": { "name": "byName", "@valueType": "int" } } ] + }} + ] + } +} diff --git a/fixtures/contract-conformance/error-strict-recursive/expected-error.txt b/fixtures/contract-conformance/error-strict-recursive/expected-error.txt new file mode 100644 index 0000000000..1f76f26692 --- /dev/null +++ b/fixtures/contract-conformance/error-strict-recursive/expected-error.txt @@ -0,0 +1 @@ +[ERR_CONTRACT_STRICT_UNREPRESENTABLE] 'acme::contract::Tree' has no strict schema: its shape is recursive (Tree -> Tree) (at 'children'). Leave it out with the generator's filter option. diff --git a/fixtures/contract-conformance/error-strict-recursive/meta.json b/fixtures/contract-conformance/error-strict-recursive/meta.json new file mode 100644 index 0000000000..36ae99dfe4 --- /dev/null +++ b/fixtures/contract-conformance/error-strict-recursive/meta.json @@ -0,0 +1,14 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { "object.value": { + "name": "Tree", + "children": [ + { "field.string": { "name": "label", "@required": true } }, + { "field.object": { "name": "children", "@objectRef": "Tree", "isArray": true } } + ] + }} + ] + } +} diff --git a/fixtures/contract-conformance/jsonb/case.json b/fixtures/contract-conformance/jsonb/case.json new file mode 100644 index 0000000000..948b352712 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/jsonb/meta.json", "strictExclude": ["Document", "Pricing"] } diff --git a/fixtures/contract-conformance/jsonb/expected/openapi.json b/fixtures/contract-conformance/jsonb/expected/openapi.json new file mode 100644 index 0000000000..bcc6621edd --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/openapi.json @@ -0,0 +1,918 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "jsonb", + "version": "1.0.0" + }, + "paths": { + "/api/documents": { + "get": { + "operationId": "listDocument", + "tags": [ + "Document" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[title]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[title][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[title][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "title", + "title:asc", + "title:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentList" + }, + { + "$ref": "#/components/schemas/DocumentPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createDocument", + "tags": [ + "Document" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Document" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/documents/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getDocument", + "tags": [ + "Document" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Document" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateDocument", + "tags": [ + "Document" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Document" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putDocument", + "tags": [ + "Document" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Document" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteDocument", + "tags": [ + "Document" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Document": { + "title": "Document", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string", + "maxLength": 200 + }, + "payload": {}, + "primaryMarker": { + "$ref": "#/components/schemas/Marker" + }, + "optionalMarker": { + "anyOf": [ + { + "$ref": "#/components/schemas/Marker" + }, + { + "type": "null" + } + ] + }, + "markers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Marker" + } + }, + "measure": { + "anyOf": [ + { + "$ref": "#/components/schemas/Measure" + }, + { + "type": "null" + } + ] + }, + "measures": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/Measure" + } + }, + "telemetry": { + "anyOf": [ + { + "$ref": "#/components/schemas/Telemetry" + }, + { + "type": "null" + } + ] + }, + "tiers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/Tier" + } + }, + "pricing": { + "anyOf": [ + { + "$ref": "#/components/schemas/Pricing" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "payload", + "primaryMarker", + "optionalMarker", + "markers", + "measure", + "measures", + "telemetry", + "tiers", + "pricing" + ] + }, + "DocumentCreate": { + "title": "DocumentCreate", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "payload": {}, + "primaryMarker": { + "$ref": "#/components/schemas/MarkerCreate" + }, + "optionalMarker": { + "$ref": "#/components/schemas/MarkerCreate" + }, + "markers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MarkerCreate" + } + }, + "measure": { + "$ref": "#/components/schemas/MeasureCreate" + }, + "measures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MeasureCreate" + } + }, + "telemetry": { + "$ref": "#/components/schemas/TelemetryCreate" + }, + "tiers": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TierCreate" + } + }, + "pricing": { + "$ref": "#/components/schemas/PricingCreate" + } + }, + "required": [ + "title", + "primaryMarker" + ] + }, + "DocumentList": { + "title": "DocumentList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Document" + } + }, + "DocumentPage": { + "title": "DocumentPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Document" + } + }, + "total": { + "type": "integer" + } + } + }, + "DocumentUpdate": { + "title": "DocumentUpdate", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "payload": {}, + "primaryMarker": { + "$ref": "#/components/schemas/MarkerCreate" + }, + "optionalMarker": { + "anyOf": [ + { + "$ref": "#/components/schemas/MarkerCreate" + }, + { + "type": "null" + } + ] + }, + "markers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MarkerCreate" + } + }, + "measure": { + "anyOf": [ + { + "$ref": "#/components/schemas/MeasureCreate" + }, + { + "type": "null" + } + ] + }, + "measures": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MeasureCreate" + } + }, + "telemetry": { + "anyOf": [ + { + "$ref": "#/components/schemas/TelemetryCreate" + }, + { + "type": "null" + } + ] + }, + "tiers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/TierCreate" + } + }, + "pricing": { + "anyOf": [ + { + "$ref": "#/components/schemas/PricingCreate" + }, + { + "type": "null" + } + ] + } + } + }, + "Marker": { + "title": "Marker", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 40 + }, + "score": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "label" + ] + }, + "MarkerCreate": { + "title": "MarkerCreate", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "score": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "label" + ] + }, + "Measure": { + "title": "Measure", + "type": "object", + "properties": { + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "samples": { + "type": [ + "array", + "null" + ], + "items": { + "type": "number", + "format": "double" + } + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + } + } + }, + "MeasureCreate": { + "title": "MeasureCreate", + "type": "object", + "properties": { + "ratio": { + "type": "number", + "format": "double" + }, + "samples": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 12, scale 4." + } + } + }, + "Pricing": { + "title": "Pricing", + "type": "object", + "properties": { + "tiers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/Tier" + } + } + } + }, + "PricingCreate": { + "title": "PricingCreate", + "type": "object", + "properties": { + "tiers": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TierCreate" + } + } + } + }, + "Telemetry": { + "title": "Telemetry", + "type": "object", + "properties": { + "capturedAt": { + "type": "string", + "format": "date-time" + }, + "latitude": { + "type": "number", + "format": "double" + }, + "tempC": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "capturedAt", + "latitude" + ] + }, + "TelemetryCreate": { + "title": "TelemetryCreate", + "type": "object", + "properties": { + "capturedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "latitude": { + "type": "number", + "format": "double" + }, + "tempC": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "capturedAt", + "latitude" + ] + }, + "Tier": { + "title": "Tier", + "type": "object", + "properties": { + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + } + } + }, + "TierCreate": { + "title": "TierCreate", + "type": "object", + "properties": { + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 12, scale 4." + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.create.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.create.schema.json new file mode 100644 index 0000000000..d19e71ddd2 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.create.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Document.create.schema.json", + "title": "Document.create", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "payload": {}, + "primaryMarker": { + "$ref": "./Marker.create.schema.json" + }, + "optionalMarker": { + "$ref": "./Marker.create.schema.json" + }, + "markers": { + "type": "array", + "items": { + "$ref": "./Marker.create.schema.json" + } + }, + "measure": { + "$ref": "./Measure.create.schema.json" + }, + "measures": { + "type": "array", + "items": { + "$ref": "./Measure.create.schema.json" + } + }, + "telemetry": { + "$ref": "./Telemetry.create.schema.json" + }, + "tiers": { + "type": "object", + "additionalProperties": { + "$ref": "./Tier.create.schema.json" + } + }, + "pricing": { + "$ref": "./Pricing.create.schema.json" + } + }, + "required": [ + "title", + "primaryMarker" + ] +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.schema.json new file mode 100644 index 0000000000..969384ab5b --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.schema.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Document.schema.json", + "title": "Document", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string", + "maxLength": 200 + }, + "payload": {}, + "primaryMarker": { + "$ref": "./Marker.schema.json" + }, + "optionalMarker": { + "anyOf": [ + { + "$ref": "./Marker.schema.json" + }, + { + "type": "null" + } + ] + }, + "markers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Marker.schema.json" + } + }, + "measure": { + "anyOf": [ + { + "$ref": "./Measure.schema.json" + }, + { + "type": "null" + } + ] + }, + "measures": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Measure.schema.json" + } + }, + "telemetry": { + "anyOf": [ + { + "$ref": "./Telemetry.schema.json" + }, + { + "type": "null" + } + ] + }, + "tiers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Tier.schema.json" + } + }, + "pricing": { + "anyOf": [ + { + "$ref": "./Pricing.schema.json" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "payload", + "primaryMarker", + "optionalMarker", + "markers", + "measure", + "measures", + "telemetry", + "tiers", + "pricing" + ] +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.update.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.update.schema.json new file mode 100644 index 0000000000..efe49110e2 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Document.update.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Document.update.schema.json", + "title": "Document.update", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "payload": {}, + "primaryMarker": { + "$ref": "./Marker.create.schema.json" + }, + "optionalMarker": { + "anyOf": [ + { + "$ref": "./Marker.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "markers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Marker.create.schema.json" + } + }, + "measure": { + "anyOf": [ + { + "$ref": "./Measure.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "measures": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "./Measure.create.schema.json" + } + }, + "telemetry": { + "anyOf": [ + { + "$ref": "./Telemetry.create.schema.json" + }, + { + "type": "null" + } + ] + }, + "tiers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Tier.create.schema.json" + } + }, + "pricing": { + "anyOf": [ + { + "$ref": "./Pricing.create.schema.json" + }, + { + "type": "null" + } + ] + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.create.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.create.schema.json new file mode 100644 index 0000000000..30c0d7a77b --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Marker.create.schema.json", + "title": "Marker.create", + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "score": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json new file mode 100644 index 0000000000..e3cec584ae --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Marker.schema.json", + "title": "Marker", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 40 + }, + "score": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "label" + ] +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.strict.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.strict.schema.json new file mode 100644 index 0000000000..86dcf39c87 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.strict.schema.json @@ -0,0 +1,21 @@ +{ + "title": "Marker", + "type": "object", + "properties": { + "label": { + "description": "Minimum length: 1. Maximum length: 40.", + "type": "string" + }, + "score": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "label", + "score" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.create.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.create.schema.json new file mode 100644 index 0000000000..8e7661c3e9 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.create.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Measure.create.schema.json", + "title": "Measure.create", + "type": "object", + "properties": { + "ratio": { + "type": "number", + "format": "double" + }, + "samples": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + }, + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 12, scale 4." + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json new file mode 100644 index 0000000000..f9bd343db8 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Measure.schema.json", + "title": "Measure", + "type": "object", + "properties": { + "ratio": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "samples": { + "type": [ + "array", + "null" + ], + "items": { + "type": "number", + "format": "double" + } + }, + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.strict.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.strict.schema.json new file mode 100644 index 0000000000..468adc1cb2 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.strict.schema.json @@ -0,0 +1,34 @@ +{ + "title": "Measure", + "type": "object", + "properties": { + "ratio": { + "type": [ + "number", + "null" + ] + }, + "samples": { + "type": [ + "array", + "null" + ], + "items": { + "type": "number" + } + }, + "amount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 12, scale 4. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "ratio", + "samples", + "amount" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.create.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.create.schema.json new file mode 100644 index 0000000000..e20a6f9ccd --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.create.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Pricing.create.schema.json", + "title": "Pricing.create", + "type": "object", + "properties": { + "tiers": { + "type": "object", + "additionalProperties": { + "$ref": "./Tier.create.schema.json" + } + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json new file mode 100644 index 0000000000..9b59d930f5 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Pricing.schema.json", + "title": "Pricing", + "type": "object", + "properties": { + "tiers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "./Tier.schema.json" + } + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.create.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.create.schema.json new file mode 100644 index 0000000000..9a07c573a7 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.create.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Telemetry.create.schema.json", + "title": "Telemetry.create", + "type": "object", + "properties": { + "capturedAt": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "description": "An instant. A value with no offset is read as UTC." + }, + "latitude": { + "type": "number", + "format": "double" + }, + "tempC": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "capturedAt", + "latitude" + ] +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json new file mode 100644 index 0000000000..2f4a8ca706 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Telemetry.schema.json", + "title": "Telemetry", + "type": "object", + "properties": { + "capturedAt": { + "type": "string", + "format": "date-time" + }, + "latitude": { + "type": "number", + "format": "double" + }, + "tempC": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "capturedAt", + "latitude" + ] +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.strict.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.strict.schema.json new file mode 100644 index 0000000000..48abfcdea5 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.strict.schema.json @@ -0,0 +1,25 @@ +{ + "title": "Telemetry", + "type": "object", + "properties": { + "capturedAt": { + "description": "An instant. A value with no offset is read as UTC. Pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$.", + "type": "string" + }, + "latitude": { + "type": "number" + }, + "tempC": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "capturedAt", + "latitude", + "tempC" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.create.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.create.schema.json new file mode 100644 index 0000000000..814383275d --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Tier.create.schema.json", + "title": "Tier.create", + "type": "object", + "properties": { + "amount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 12, scale 4." + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json new file mode 100644 index 0000000000..23a18d7b1a --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/store/Tier.schema.json", + "title": "Tier", + "type": "object", + "properties": { + "amount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + } + } +} diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.strict.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.strict.schema.json new file mode 100644 index 0000000000..5b6c91c542 --- /dev/null +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "Tier", + "type": "object", + "properties": { + "amount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 12, scale 4. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "amount" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/case.json b/fixtures/contract-conformance/m2m/case.json new file mode 100644 index 0000000000..efacd86bf5 --- /dev/null +++ b/fixtures/contract-conformance/m2m/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/m2m/meta.json" } diff --git a/fixtures/contract-conformance/m2m/expected/openapi.json b/fixtures/contract-conformance/m2m/expected/openapi.json new file mode 100644 index 0000000000..28d59f6255 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/openapi.json @@ -0,0 +1,5982 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "m2m", + "version": "1.0.0" + }, + "paths": { + "/api/accounts": { + "get": { + "operationId": "listAccount", + "tags": [ + "Account" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[kind]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "Member", + "Guest" + ] + } + }, + { + "name": "filter[kind][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "Member", + "Guest" + ] + } + }, + { + "name": "filter[kind][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "Member", + "Guest" + ] + } + }, + { + "name": "filter[kind][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[kind][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[kind][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[handle]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[handle][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "kind", + "kind:asc", + "kind:desc", + "handle", + "handle:asc", + "handle:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AccountList" + }, + { + "$ref": "#/components/schemas/AccountPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getAccount", + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/{id}/badges": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listAccountBadges", + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + }, + "/api/accounts/guest": { + "get": { + "operationId": "listGuestAccount", + "tags": [ + "GuestAccount" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[handle]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[handle][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[invitedBy]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[invitedBy][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[invitedBy][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[invitedBy][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[invitedBy][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[invitedBy][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "handle", + "handle:asc", + "handle:desc", + "invitedBy", + "invitedBy:asc", + "invitedBy:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuestAccountList" + }, + { + "$ref": "#/components/schemas/GuestAccountPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createGuestAccount", + "tags": [ + "GuestAccount" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccountCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccount" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/guest/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getGuestAccount", + "tags": [ + "GuestAccount" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccount" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateGuestAccount", + "tags": [ + "GuestAccount" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccountUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccount" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putGuestAccount", + "tags": [ + "GuestAccount" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccountUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestAccount" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteGuestAccount", + "tags": [ + "GuestAccount" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/guest/{id}/badges": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listGuestAccountBadges", + "tags": [ + "GuestAccount" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + }, + "/api/accounts/member": { + "get": { + "operationId": "listMemberAccount", + "tags": [ + "MemberAccount" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[handle]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[handle][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[handle][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[karma]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[karma][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[karma][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "handle", + "handle:asc", + "handle:desc", + "karma", + "karma:asc", + "karma:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberAccountList" + }, + { + "$ref": "#/components/schemas/MemberAccountPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createMemberAccount", + "tags": [ + "MemberAccount" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccount" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/member/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getMemberAccount", + "tags": [ + "MemberAccount" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccount" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateMemberAccount", + "tags": [ + "MemberAccount" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccount" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putMemberAccount", + "tags": [ + "MemberAccount" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccount" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteMemberAccount", + "tags": [ + "MemberAccount" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/member/{id}/badges": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listMemberAccountBadges", + "tags": [ + "MemberAccount" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + }, + "/api/accounts/member/{id}/scopes": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listMemberAccountScopes", + "tags": [ + "MemberAccount" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + }, + "/api/accounts/member/{id}/interests": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listMemberAccountInterests", + "tags": [ + "MemberAccount" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + }, + "/api/account_tags": { + "get": { + "operationId": "listAccountTag", + "tags": [ + "AccountTag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AccountTagList" + }, + { + "$ref": "#/components/schemas/AccountTagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createAccountTag", + "tags": [ + "AccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/account_tags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getAccountTag", + "tags": [ + "AccountTag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAccountTag", + "tags": [ + "AccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putAccountTag", + "tags": [ + "AccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAccountTag", + "tags": [ + "AccountTag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/follows": { + "get": { + "operationId": "listFollow", + "tags": [ + "Follow" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FollowList" + }, + { + "$ref": "#/components/schemas/FollowPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createFollow", + "tags": [ + "Follow" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FollowCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Follow" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/follows/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getFollow", + "tags": [ + "Follow" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Follow" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateFollow", + "tags": [ + "Follow" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FollowUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Follow" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putFollow", + "tags": [ + "Follow" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FollowUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Follow" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteFollow", + "tags": [ + "Follow" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/friendships": { + "get": { + "operationId": "listFriendship", + "tags": [ + "Friendship" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FriendshipList" + }, + { + "$ref": "#/components/schemas/FriendshipPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createFriendship", + "tags": [ + "Friendship" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FriendshipCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Friendship" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/friendships/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getFriendship", + "tags": [ + "Friendship" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Friendship" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateFriendship", + "tags": [ + "Friendship" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FriendshipUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Friendship" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putFriendship", + "tags": [ + "Friendship" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FriendshipUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Friendship" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteFriendship", + "tags": [ + "Friendship" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/member_account_tags": { + "get": { + "operationId": "listMemberAccountTag", + "tags": [ + "MemberAccountTag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberAccountTagList" + }, + { + "$ref": "#/components/schemas/MemberAccountTagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createMemberAccountTag", + "tags": [ + "MemberAccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/member_account_tags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getMemberAccountTag", + "tags": [ + "MemberAccountTag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateMemberAccountTag", + "tags": [ + "MemberAccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putMemberAccountTag", + "tags": [ + "MemberAccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteMemberAccountTag", + "tags": [ + "MemberAccountTag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/persons": { + "get": { + "operationId": "listPerson", + "tags": [ + "Person" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PersonList" + }, + { + "$ref": "#/components/schemas/PersonPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createPerson", + "tags": [ + "Person" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/persons/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getPerson", + "tags": [ + "Person" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updatePerson", + "tags": [ + "Person" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putPerson", + "tags": [ + "Person" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deletePerson", + "tags": [ + "Person" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/persons/{id}/following": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listPersonFollowing", + "tags": [ + "Person" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + } + } + } + } + } + } + }, + "/api/persons/{id}/friends": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listPersonFriends", + "tags": [ + "Person" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + } + } + } + } + } + } + }, + "/api/posts": { + "get": { + "operationId": "listPost", + "tags": [ + "Post" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PostList" + }, + { + "$ref": "#/components/schemas/PostPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createPost", + "tags": [ + "Post" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/posts/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getPost", + "tags": [ + "Post" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updatePost", + "tags": [ + "Post" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putPost", + "tags": [ + "Post" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deletePost", + "tags": [ + "Post" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/posts/{id}/tags": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listPostTags", + "tags": [ + "Post" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + } + } + } + } + }, + "/api/posts/{id}/reviewers": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "listPostReviewers", + "tags": [ + "Post" + ], + "responses": { + "200": { + "description": "The related rows.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberAccount" + } + } + } + } + } + } + } + }, + "/api/post_categories": { + "get": { + "operationId": "listPostCategory", + "tags": [ + "PostCategory" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PostCategoryList" + }, + { + "$ref": "#/components/schemas/PostCategoryPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createPostCategory", + "tags": [ + "PostCategory" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategoryCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategory" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/post_categories/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getPostCategory", + "tags": [ + "PostCategory" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategory" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updatePostCategory", + "tags": [ + "PostCategory" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategoryUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategory" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putPostCategory", + "tags": [ + "PostCategory" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategoryUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCategory" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deletePostCategory", + "tags": [ + "PostCategory" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/post_reviewers": { + "get": { + "operationId": "listPostReviewer", + "tags": [ + "PostReviewer" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PostReviewerList" + }, + { + "$ref": "#/components/schemas/PostReviewerPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createPostReviewer", + "tags": [ + "PostReviewer" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewerCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewer" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/post_reviewers/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getPostReviewer", + "tags": [ + "PostReviewer" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewer" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updatePostReviewer", + "tags": [ + "PostReviewer" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewerUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewer" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putPostReviewer", + "tags": [ + "PostReviewer" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewerUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostReviewer" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deletePostReviewer", + "tags": [ + "PostReviewer" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/post_tags": { + "get": { + "operationId": "listPostTag", + "tags": [ + "PostTag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PostTagList" + }, + { + "$ref": "#/components/schemas/PostTagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createPostTag", + "tags": [ + "PostTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/post_tags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getPostTag", + "tags": [ + "PostTag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updatePostTag", + "tags": [ + "PostTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putPostTag", + "tags": [ + "PostTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deletePostTag", + "tags": [ + "PostTag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/scoped_account_tags": { + "get": { + "operationId": "listScopedAccountTag", + "tags": [ + "ScopedAccountTag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ScopedAccountTagList" + }, + { + "$ref": "#/components/schemas/ScopedAccountTagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createScopedAccountTag", + "tags": [ + "ScopedAccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/scoped_account_tags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getScopedAccountTag", + "tags": [ + "ScopedAccountTag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateScopedAccountTag", + "tags": [ + "ScopedAccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putScopedAccountTag", + "tags": [ + "ScopedAccountTag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopedAccountTag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteScopedAccountTag", + "tags": [ + "ScopedAccountTag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/tags": { + "get": { + "operationId": "listTag", + "tags": [ + "Tag" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/TagList" + }, + { + "$ref": "#/components/schemas/TagPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createTag", + "tags": [ + "Tag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/tags/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getTag", + "tags": [ + "Tag" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateTag", + "tags": [ + "Tag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putTag", + "tags": [ + "Tag" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteTag", + "tags": [ + "Tag" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Account": { + "title": "Account", + "oneOf": [ + { + "$ref": "#/components/schemas/GuestAccount" + }, + { + "$ref": "#/components/schemas/MemberAccount" + } + ], + "discriminator": { + "propertyName": "kind", + "mapping": { + "Guest": "#/components/schemas/GuestAccount", + "Member": "#/components/schemas/MemberAccount" + } + } + }, + "AccountList": { + "title": "AccountList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Account" + } + }, + "AccountPage": { + "title": "AccountPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Account" + } + }, + "total": { + "type": "integer" + } + } + }, + "AccountTag": { + "title": "AccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] + }, + "AccountTagCreate": { + "title": "AccountTagCreate", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] + }, + "AccountTagList": { + "title": "AccountTagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/AccountTag" + } + }, + "AccountTagPage": { + "title": "AccountTagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AccountTag" + } + }, + "total": { + "type": "integer" + } + } + }, + "AccountTagUpdate": { + "title": "AccountTagUpdate", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } + }, + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Follow": { + "title": "Follow", + "type": "object", + "properties": { + "followerId": { + "type": "integer", + "format": "int64" + }, + "followeeId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "followerId", + "followeeId" + ] + }, + "FollowCreate": { + "title": "FollowCreate", + "type": "object", + "properties": { + "followerId": { + "type": "integer", + "format": "int64" + }, + "followeeId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "followerId", + "followeeId" + ] + }, + "FollowList": { + "title": "FollowList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Follow" + } + }, + "FollowPage": { + "title": "FollowPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Follow" + } + }, + "total": { + "type": "integer" + } + } + }, + "FollowUpdate": { + "title": "FollowUpdate", + "type": "object", + "properties": { + "followerId": { + "type": "integer", + "format": "int64" + }, + "followeeId": { + "type": "integer", + "format": "int64" + } + } + }, + "Friendship": { + "title": "Friendship", + "type": "object", + "properties": { + "personAId": { + "type": "integer", + "format": "int64" + }, + "personBId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "personAId", + "personBId" + ] + }, + "FriendshipCreate": { + "title": "FriendshipCreate", + "type": "object", + "properties": { + "personAId": { + "type": "integer", + "format": "int64" + }, + "personBId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "personAId", + "personBId" + ] + }, + "FriendshipList": { + "title": "FriendshipList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Friendship" + } + }, + "FriendshipPage": { + "title": "FriendshipPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Friendship" + } + }, + "total": { + "type": "integer" + } + } + }, + "FriendshipUpdate": { + "title": "FriendshipUpdate", + "type": "object", + "properties": { + "personAId": { + "type": "integer", + "format": "int64" + }, + "personBId": { + "type": "integer", + "format": "int64" + } + } + }, + "GuestAccount": { + "title": "GuestAccount", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "const": "Guest" + }, + "handle": { + "type": "string", + "maxLength": 80 + }, + "invitedBy": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + }, + "required": [ + "id", + "kind", + "handle", + "invitedBy" + ] + }, + "GuestAccountCreate": { + "title": "GuestAccountCreate", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "invitedBy": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "handle" + ] + }, + "GuestAccountList": { + "title": "GuestAccountList", + "type": "array", + "items": { + "$ref": "#/components/schemas/GuestAccount" + } + }, + "GuestAccountPage": { + "title": "GuestAccountPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuestAccount" + } + }, + "total": { + "type": "integer" + } + } + }, + "GuestAccountUpdate": { + "title": "GuestAccountUpdate", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "invitedBy": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + } + }, + "MemberAccount": { + "title": "MemberAccount", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "const": "Member" + }, + "handle": { + "type": "string", + "maxLength": 80 + }, + "karma": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id", + "kind", + "handle", + "karma" + ] + }, + "MemberAccountCreate": { + "title": "MemberAccountCreate", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "karma": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "handle" + ] + }, + "MemberAccountList": { + "title": "MemberAccountList", + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberAccount" + } + }, + "MemberAccountPage": { + "title": "MemberAccountPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberAccount" + } + }, + "total": { + "type": "integer" + } + } + }, + "MemberAccountTag": { + "title": "MemberAccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] + }, + "MemberAccountTagCreate": { + "title": "MemberAccountTagCreate", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] + }, + "MemberAccountTagList": { + "title": "MemberAccountTagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberAccountTag" + } + }, + "MemberAccountTagPage": { + "title": "MemberAccountTagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberAccountTag" + } + }, + "total": { + "type": "integer" + } + } + }, + "MemberAccountTagUpdate": { + "title": "MemberAccountTagUpdate", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } + }, + "MemberAccountUpdate": { + "title": "MemberAccountUpdate", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "karma": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "Person": { + "title": "Person", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] + }, + "PersonCreate": { + "title": "PersonCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] + }, + "PersonList": { + "title": "PersonList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + }, + "PersonPage": { + "title": "PersonPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Person" + } + }, + "total": { + "type": "integer" + } + } + }, + "PersonUpdate": { + "title": "PersonUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "Post": { + "title": "Post", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string", + "maxLength": 200 + } + }, + "required": [ + "id", + "title" + ] + }, + "PostCategory": { + "title": "PostCategory", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] + }, + "PostCategoryCreate": { + "title": "PostCategoryCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] + }, + "PostCategoryList": { + "title": "PostCategoryList", + "type": "array", + "items": { + "$ref": "#/components/schemas/PostCategory" + } + }, + "PostCategoryPage": { + "title": "PostCategoryPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PostCategory" + } + }, + "total": { + "type": "integer" + } + } + }, + "PostCategoryUpdate": { + "title": "PostCategoryUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "PostCreate": { + "title": "PostCreate", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "title" + ] + }, + "PostList": { + "title": "PostList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + }, + "PostPage": { + "title": "PostPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + }, + "total": { + "type": "integer" + } + } + }, + "PostReviewer": { + "title": "PostReviewer", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "accountId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "accountId" + ] + }, + "PostReviewerCreate": { + "title": "PostReviewerCreate", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "accountId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "accountId" + ] + }, + "PostReviewerList": { + "title": "PostReviewerList", + "type": "array", + "items": { + "$ref": "#/components/schemas/PostReviewer" + } + }, + "PostReviewerPage": { + "title": "PostReviewerPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PostReviewer" + } + }, + "total": { + "type": "integer" + } + } + }, + "PostReviewerUpdate": { + "title": "PostReviewerUpdate", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "accountId": { + "type": "integer", + "format": "int64" + } + } + }, + "PostTag": { + "title": "PostTag", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "tagId" + ] + }, + "PostTagCreate": { + "title": "PostTagCreate", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "tagId" + ] + }, + "PostTagList": { + "title": "PostTagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/PostTag" + } + }, + "PostTagPage": { + "title": "PostTagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PostTag" + } + }, + "total": { + "type": "integer" + } + } + }, + "PostTagUpdate": { + "title": "PostTagUpdate", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } + }, + "PostUpdate": { + "title": "PostUpdate", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + } + }, + "ScopedAccountTag": { + "title": "ScopedAccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] + }, + "ScopedAccountTagCreate": { + "title": "ScopedAccountTagCreate", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] + }, + "ScopedAccountTagList": { + "title": "ScopedAccountTagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopedAccountTag" + } + }, + "ScopedAccountTagPage": { + "title": "ScopedAccountTagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopedAccountTag" + } + }, + "total": { + "type": "integer" + } + } + }, + "ScopedAccountTagUpdate": { + "title": "ScopedAccountTagUpdate", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } + }, + "Tag": { + "title": "Tag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] + }, + "TagCreate": { + "title": "TagCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] + }, + "TagList": { + "title": "TagList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "TagPage": { + "title": "TagPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "total": { + "type": "integer" + } + } + }, + "TagUpdate": { + "title": "TagUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Account.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Account.schema.json new file mode 100644 index 0000000000..1a72582faf --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Account.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Account.schema.json", + "title": "Account", + "oneOf": [ + { + "$ref": "./GuestAccount.schema.json" + }, + { + "$ref": "./MemberAccount.schema.json" + } + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.create.schema.json new file mode 100644 index 0000000000..68caec2824 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/AccountTag.create.schema.json", + "title": "AccountTag.create", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.schema.json new file mode 100644 index 0000000000..8263275a0e --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/AccountTag.schema.json", + "title": "AccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.strict.schema.json new file mode 100644 index 0000000000..d5ff03d80e --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "AccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer" + }, + "tagId": { + "type": "integer" + } + }, + "required": [ + "accountId", + "tagId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.update.schema.json new file mode 100644 index 0000000000..8b7cec3207 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/AccountTag.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/AccountTag.update.schema.json", + "title": "AccountTag.update", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.create.schema.json new file mode 100644 index 0000000000..81b4ebdf84 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Follow.create.schema.json", + "title": "Follow.create", + "type": "object", + "properties": { + "followerId": { + "type": "integer", + "format": "int64" + }, + "followeeId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "followerId", + "followeeId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.schema.json new file mode 100644 index 0000000000..b00855c87d --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Follow.schema.json", + "title": "Follow", + "type": "object", + "properties": { + "followerId": { + "type": "integer", + "format": "int64" + }, + "followeeId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "followerId", + "followeeId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.strict.schema.json new file mode 100644 index 0000000000..5ef26d5a30 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "Follow", + "type": "object", + "properties": { + "followerId": { + "type": "integer" + }, + "followeeId": { + "type": "integer" + } + }, + "required": [ + "followerId", + "followeeId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.update.schema.json new file mode 100644 index 0000000000..bafa370de5 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Follow.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Follow.update.schema.json", + "title": "Follow.update", + "type": "object", + "properties": { + "followerId": { + "type": "integer", + "format": "int64" + }, + "followeeId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.create.schema.json new file mode 100644 index 0000000000..4de5177e5f --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Friendship.create.schema.json", + "title": "Friendship.create", + "type": "object", + "properties": { + "personAId": { + "type": "integer", + "format": "int64" + }, + "personBId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "personAId", + "personBId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.schema.json new file mode 100644 index 0000000000..6ebf68e320 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Friendship.schema.json", + "title": "Friendship", + "type": "object", + "properties": { + "personAId": { + "type": "integer", + "format": "int64" + }, + "personBId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "personAId", + "personBId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.strict.schema.json new file mode 100644 index 0000000000..9f6c7a6f6f --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "Friendship", + "type": "object", + "properties": { + "personAId": { + "type": "integer" + }, + "personBId": { + "type": "integer" + } + }, + "required": [ + "personAId", + "personBId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.update.schema.json new file mode 100644 index 0000000000..b7afbad306 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Friendship.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Friendship.update.schema.json", + "title": "Friendship.update", + "type": "object", + "properties": { + "personAId": { + "type": "integer", + "format": "int64" + }, + "personBId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.create.schema.json new file mode 100644 index 0000000000..9e9db782d0 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/GuestAccount.create.schema.json", + "title": "GuestAccount.create", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "invitedBy": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "handle" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.schema.json new file mode 100644 index 0000000000..ba22d7c217 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/GuestAccount.schema.json", + "title": "GuestAccount", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "const": "Guest" + }, + "handle": { + "type": "string", + "maxLength": 80 + }, + "invitedBy": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + }, + "required": [ + "id", + "kind", + "handle", + "invitedBy" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.strict.schema.json new file mode 100644 index 0000000000..442b38218e --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.strict.schema.json @@ -0,0 +1,22 @@ +{ + "title": "GuestAccount", + "type": "object", + "properties": { + "handle": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "invitedBy": { + "description": "Maximum length: 80.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "handle", + "invitedBy" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.update.schema.json new file mode 100644 index 0000000000..2da941bd83 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/GuestAccount.update.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/GuestAccount.update.schema.json", + "title": "GuestAccount.update", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "invitedBy": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.create.schema.json new file mode 100644 index 0000000000..c798a6c833 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/MemberAccount.create.schema.json", + "title": "MemberAccount.create", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "karma": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "handle" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.schema.json new file mode 100644 index 0000000000..ea0829a295 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/MemberAccount.schema.json", + "title": "MemberAccount", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "const": "Member" + }, + "handle": { + "type": "string", + "maxLength": 80 + }, + "karma": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id", + "kind", + "handle", + "karma" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.strict.schema.json new file mode 100644 index 0000000000..8a3099bd83 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.strict.schema.json @@ -0,0 +1,21 @@ +{ + "title": "MemberAccount", + "type": "object", + "properties": { + "handle": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "karma": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "handle", + "karma" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.update.schema.json new file mode 100644 index 0000000000..f4ddaf2a8f --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccount.update.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/MemberAccount.update.schema.json", + "title": "MemberAccount.update", + "type": "object", + "properties": { + "handle": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "karma": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.create.schema.json new file mode 100644 index 0000000000..61b0e77e2b --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/MemberAccountTag.create.schema.json", + "title": "MemberAccountTag.create", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.schema.json new file mode 100644 index 0000000000..300bbebff3 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/MemberAccountTag.schema.json", + "title": "MemberAccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.strict.schema.json new file mode 100644 index 0000000000..78a6f3108b --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "MemberAccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer" + }, + "tagId": { + "type": "integer" + } + }, + "required": [ + "accountId", + "tagId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.update.schema.json new file mode 100644 index 0000000000..3ab529a36e --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/MemberAccountTag.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/MemberAccountTag.update.schema.json", + "title": "MemberAccountTag.update", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.create.schema.json new file mode 100644 index 0000000000..3c71142901 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Person.create.schema.json", + "title": "Person.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.schema.json new file mode 100644 index 0000000000..c3ffa2dffa --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Person.schema.json", + "title": "Person", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.strict.schema.json new file mode 100644 index 0000000000..fbf640aad6 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Person", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.update.schema.json new file mode 100644 index 0000000000..5eb9c2a321 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Person.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Person.update.schema.json", + "title": "Person.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.create.schema.json new file mode 100644 index 0000000000..cfbe5538e3 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Post.create.schema.json", + "title": "Post.create", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "title" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.schema.json new file mode 100644 index 0000000000..b9a8f611fa --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Post.schema.json", + "title": "Post", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string", + "maxLength": 200 + } + }, + "required": [ + "id", + "title" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.strict.schema.json new file mode 100644 index 0000000000..99e47c2d22 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Post", + "type": "object", + "properties": { + "title": { + "description": "Minimum length: 1. Maximum length: 200.", + "type": "string" + } + }, + "required": [ + "title" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.update.schema.json new file mode 100644 index 0000000000..9f8df2f24f --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Post.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Post.update.schema.json", + "title": "Post.update", + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.create.schema.json new file mode 100644 index 0000000000..b08148aa57 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostCategory.create.schema.json", + "title": "PostCategory.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.schema.json new file mode 100644 index 0000000000..7267e64719 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostCategory.schema.json", + "title": "PostCategory", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.strict.schema.json new file mode 100644 index 0000000000..db240ee7fb --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "PostCategory", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.update.schema.json new file mode 100644 index 0000000000..efed50f13c --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostCategory.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostCategory.update.schema.json", + "title": "PostCategory.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.create.schema.json new file mode 100644 index 0000000000..9dcf2b8d4d --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostReviewer.create.schema.json", + "title": "PostReviewer.create", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "accountId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "accountId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.schema.json new file mode 100644 index 0000000000..36cbf726d5 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostReviewer.schema.json", + "title": "PostReviewer", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "accountId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "accountId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.strict.schema.json new file mode 100644 index 0000000000..21a3feacf7 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "PostReviewer", + "type": "object", + "properties": { + "postId": { + "type": "integer" + }, + "accountId": { + "type": "integer" + } + }, + "required": [ + "postId", + "accountId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.update.schema.json new file mode 100644 index 0000000000..851406cef3 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostReviewer.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostReviewer.update.schema.json", + "title": "PostReviewer.update", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "accountId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.create.schema.json new file mode 100644 index 0000000000..ead0a2cd3f --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostTag.create.schema.json", + "title": "PostTag.create", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.schema.json new file mode 100644 index 0000000000..38a8ec2eaf --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostTag.schema.json", + "title": "PostTag", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "postId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.strict.schema.json new file mode 100644 index 0000000000..707c3a5aed --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "PostTag", + "type": "object", + "properties": { + "postId": { + "type": "integer" + }, + "tagId": { + "type": "integer" + } + }, + "required": [ + "postId", + "tagId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.update.schema.json new file mode 100644 index 0000000000..1eaac8f6fc --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/PostTag.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/PostTag.update.schema.json", + "title": "PostTag.update", + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.create.schema.json new file mode 100644 index 0000000000..734a614589 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/ScopedAccountTag.create.schema.json", + "title": "ScopedAccountTag.create", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.schema.json new file mode 100644 index 0000000000..26b9f3e507 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/ScopedAccountTag.schema.json", + "title": "ScopedAccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "accountId", + "tagId" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.strict.schema.json new file mode 100644 index 0000000000..e527d9ea27 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "ScopedAccountTag", + "type": "object", + "properties": { + "accountId": { + "type": "integer" + }, + "tagId": { + "type": "integer" + } + }, + "required": [ + "accountId", + "tagId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.update.schema.json new file mode 100644 index 0000000000..28422b417b --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/ScopedAccountTag.update.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/ScopedAccountTag.update.schema.json", + "title": "ScopedAccountTag.update", + "type": "object", + "properties": { + "accountId": { + "type": "integer", + "format": "int64" + }, + "tagId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.create.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.create.schema.json new file mode 100644 index 0000000000..3c9272884a --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Tag.create.schema.json", + "title": "Tag.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.schema.json new file mode 100644 index 0000000000..3dfa8b0831 --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Tag.schema.json", + "title": "Tag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.strict.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.strict.schema.json new file mode 100644 index 0000000000..e6bbb1c59c --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Tag", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.update.schema.json b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.update.schema.json new file mode 100644 index 0000000000..7e5707512b --- /dev/null +++ b/fixtures/contract-conformance/m2m/expected/schemas/acme/social/Tag.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/social/Tag.update.schema.json", + "title": "Tag.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/projection/case.json b/fixtures/contract-conformance/projection/case.json new file mode 100644 index 0000000000..afc8ee3f88 --- /dev/null +++ b/fixtures/contract-conformance/projection/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/projection/meta.json" } diff --git a/fixtures/contract-conformance/projection/expected/openapi.json b/fixtures/contract-conformance/projection/expected/openapi.json new file mode 100644 index 0000000000..013f3d895c --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/openapi.json @@ -0,0 +1,1868 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "projection", + "version": "1.0.0" + }, + "paths": { + "/api/invoices": { + "get": { + "operationId": "listInvoice", + "tags": [ + "Invoice" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceList" + }, + { + "$ref": "#/components/schemas/InvoicePage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createInvoice", + "tags": [ + "Invoice" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoices/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getInvoice", + "tags": [ + "Invoice" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateInvoice", + "tags": [ + "Invoice" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putInvoice", + "tags": [ + "Invoice" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteInvoice", + "tags": [ + "Invoice" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_ledgers": { + "get": { + "operationId": "listInvoiceLedger", + "tags": [ + "InvoiceLedger" + ], + "parameters": [ + { + "name": "filter[number]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[number][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[number][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[discount]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[discount][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[discount][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[weight]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][eq]", + "in": "query", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][ne]", + "in": "query", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][gt]", + "in": "query", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][gte]", + "in": "query", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][lt]", + "in": "query", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][lte]", + "in": "query", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "filter[weight][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[weight][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "number", + "number:asc", + "number:desc", + "reference", + "reference:asc", + "reference:desc", + "discount", + "discount:asc", + "discount:desc", + "weight", + "weight:asc", + "weight:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceLedgerList" + }, + { + "$ref": "#/components/schemas/InvoiceLedgerPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_ledgers/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getInvoiceLedger", + "tags": [ + "InvoiceLedger" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceLedger" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_registers": { + "get": { + "operationId": "listInvoiceRegister", + "tags": [ + "InvoiceRegister" + ], + "parameters": [ + { + "name": "filter[regNo]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[regNo][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[regNo][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "regNo", + "regNo:asc", + "regNo:desc", + "reference", + "reference:asc", + "reference:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceRegisterList" + }, + { + "$ref": "#/components/schemas/InvoiceRegisterPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_registers/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getInvoiceRegister", + "tags": [ + "InvoiceRegister" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRegister" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_stubs": { + "get": { + "operationId": "listInvoiceStub", + "tags": [ + "InvoiceStub" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "reference", + "reference:asc", + "reference:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceStubList" + }, + { + "$ref": "#/components/schemas/InvoiceStubPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_summaries": { + "get": { + "operationId": "listInvoiceSummary", + "tags": [ + "InvoiceSummary" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[status]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[status][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[amountCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[amountCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[amountCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "reference", + "reference:asc", + "reference:desc", + "amountCents", + "amountCents:asc", + "amountCents:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceSummaryList" + }, + { + "$ref": "#/components/schemas/InvoiceSummaryPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_summaries/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getInvoiceSummary", + "tags": [ + "InvoiceSummary" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceSummary" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Invoice": { + "title": "Invoice", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "status": { + "type": "string", + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + }, + "weight": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents", + "discount", + "weight" + ] + }, + "InvoiceCreate": { + "title": "InvoiceCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "weight": { + "type": "number", + "format": "float" + } + }, + "required": [ + "reference", + "status", + "amountCents" + ] + }, + "InvoiceLedger": { + "title": "InvoiceLedger", + "type": "object", + "properties": { + "number": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "discount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + }, + "weight": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "required": [ + "number", + "reference", + "discount", + "weight" + ] + }, + "InvoiceLedgerList": { + "title": "InvoiceLedgerList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceLedger" + } + }, + "InvoiceLedgerPage": { + "title": "InvoiceLedgerPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceLedger" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceList": { + "title": "InvoiceList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice" + } + }, + "InvoicePage": { + "title": "InvoicePage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceRegister": { + "title": "InvoiceRegister", + "type": "object", + "properties": { + "regNo": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + } + }, + "required": [ + "regNo", + "reference" + ] + }, + "InvoiceRegisterList": { + "title": "InvoiceRegisterList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceRegister" + } + }, + "InvoiceRegisterPage": { + "title": "InvoiceRegisterPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceRegister" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceStub": { + "title": "InvoiceStub", + "type": "object", + "properties": { + "id": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + } + }, + "required": [ + "id", + "reference" + ] + }, + "InvoiceStubList": { + "title": "InvoiceStubList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceStub" + } + }, + "InvoiceStubPage": { + "title": "InvoiceStubPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceStub" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceSummary": { + "title": "InvoiceSummary", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "status": { + "type": "string", + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents" + ] + }, + "InvoiceSummaryList": { + "title": "InvoiceSummaryList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceSummary" + } + }, + "InvoiceSummaryPage": { + "title": "InvoiceSummaryPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceSummary" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceUpdate": { + "title": "InvoiceUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "weight": { + "type": [ + "number", + "null" + ], + "format": "float" + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.create.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.create.schema.json new file mode 100644 index 0000000000..ccf79705a5 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.create.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Invoice.create.schema.json", + "title": "Invoice.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "weight": { + "type": "number", + "format": "float" + } + }, + "required": [ + "reference", + "status", + "amountCents" + ] +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json new file mode 100644 index 0000000000..e4e9380bdc --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Invoice.schema.json", + "title": "Invoice", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "status": { + "type": "string", + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + }, + "weight": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents", + "discount", + "weight" + ] +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.strict.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.strict.schema.json new file mode 100644 index 0000000000..291e8cc292 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.strict.schema.json @@ -0,0 +1,38 @@ +{ + "title": "Invoice", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 40.", + "type": "string" + }, + "status": { + "description": "Minimum length: 1. Maximum length: 20.", + "type": "string" + }, + "amountCents": { + "type": "integer" + }, + "discount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + }, + "weight": { + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "reference", + "status", + "amountCents", + "discount", + "weight" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.update.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.update.schema.json new file mode 100644 index 0000000000..ffa88eb464 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.update.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Invoice.update.schema.json", + "title": "Invoice.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "discount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + }, + "weight": { + "type": [ + "number", + "null" + ], + "format": "float" + } + } +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json new file mode 100644 index 0000000000..6f287783a6 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceLedger.schema.json", + "title": "InvoiceLedger", + "type": "object", + "properties": { + "number": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "discount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + }, + "weight": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "required": [ + "number", + "reference", + "discount", + "weight" + ] +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json new file mode 100644 index 0000000000..8fa0079fe1 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json @@ -0,0 +1,33 @@ +{ + "title": "InvoiceLedger", + "type": "object", + "properties": { + "number": { + "type": "integer" + }, + "reference": { + "description": "Maximum length: 40.", + "type": "string" + }, + "discount": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2.", + "type": [ + "string", + "null" + ] + }, + "weight": { + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "number", + "reference", + "discount", + "weight" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.schema.json new file mode 100644 index 0000000000..d4b8672c44 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceRegister.schema.json", + "title": "InvoiceRegister", + "type": "object", + "properties": { + "regNo": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + } + }, + "required": [ + "regNo", + "reference" + ] +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.strict.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.strict.schema.json new file mode 100644 index 0000000000..8c060fb88c --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceRegister.strict.schema.json @@ -0,0 +1,21 @@ +{ + "title": "InvoiceRegister", + "type": "object", + "properties": { + "regNo": { + "type": [ + "integer", + "null" + ] + }, + "reference": { + "description": "Maximum length: 40.", + "type": "string" + } + }, + "required": [ + "regNo", + "reference" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.schema.json new file mode 100644 index 0000000000..c86910ae12 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceStub.schema.json", + "title": "InvoiceStub", + "type": "object", + "properties": { + "id": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + } + }, + "required": [ + "id", + "reference" + ] +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.strict.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.strict.schema.json new file mode 100644 index 0000000000..a36ade7312 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceStub.strict.schema.json @@ -0,0 +1,21 @@ +{ + "title": "InvoiceStub", + "type": "object", + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "reference": { + "description": "Maximum length: 40.", + "type": "string" + } + }, + "required": [ + "id", + "reference" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.schema.json new file mode 100644 index 0000000000..b983dd713c --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceSummary.schema.json", + "title": "InvoiceSummary", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "status": { + "type": "string", + "maxLength": 20 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents" + ] +} diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.strict.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.strict.schema.json new file mode 100644 index 0000000000..8398322ca0 --- /dev/null +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceSummary.strict.schema.json @@ -0,0 +1,27 @@ +{ + "title": "InvoiceSummary", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "reference": { + "description": "Maximum length: 40.", + "type": "string" + }, + "status": { + "description": "Maximum length: 20.", + "type": "string" + }, + "amountCents": { + "type": "integer" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/case.json b/fixtures/contract-conformance/report/case.json new file mode 100644 index 0000000000..70ec7ff1aa --- /dev/null +++ b/fixtures/contract-conformance/report/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/report/meta.json", "strictExclude": ["Ticket", "TicketsByTags"], "reportExport": true } diff --git a/fixtures/contract-conformance/report/expected/openapi.json b/fixtures/contract-conformance/report/expected/openapi.json new file mode 100644 index 0000000000..e014146cf1 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/openapi.json @@ -0,0 +1,7411 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "report", + "version": "1.0.0" + }, + "paths": { + "/api/categories": { + "get": { + "operationId": "listCategory", + "tags": [ + "Category" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CategoryList" + }, + { + "$ref": "#/components/schemas/CategoryPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createCategory", + "tags": [ + "Category" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/categories/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getCategory", + "tags": [ + "Category" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateCategory", + "tags": [ + "Category" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putCategory", + "tags": [ + "Category" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCategory", + "tags": [ + "Category" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/export_cells": { + "get": { + "operationId": "listExportCells", + "tags": [ + "ExportCells" + ], + "parameters": [ + { + "name": "filter[label]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[label][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[label][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[label][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[label][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[label][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[cells]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[cells][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[cells][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[totalCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[totalCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "label", + "label:asc", + "label:desc", + "cells", + "cells:asc", + "cells:desc", + "totalCents", + "totalCents:asc", + "totalCents:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExportCellsList" + }, + { + "$ref": "#/components/schemas/ExportCellsPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/export_lines": { + "get": { + "operationId": "listExportLine", + "tags": [ + "ExportLine" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExportLineList" + }, + { + "$ref": "#/components/schemas/ExportLinePage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createExportLine", + "tags": [ + "ExportLine" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLineCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLine" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/export_lines/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getExportLine", + "tags": [ + "ExportLine" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLine" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateExportLine", + "tags": [ + "ExportLine" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLineUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLine" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putExportLine", + "tags": [ + "ExportLine" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLineUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportLine" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteExportLine", + "tags": [ + "ExportLine" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoices": { + "get": { + "operationId": "listInvoice", + "tags": [ + "Invoice" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceList" + }, + { + "$ref": "#/components/schemas/InvoicePage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createInvoice", + "tags": [ + "Invoice" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoices/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getInvoice", + "tags": [ + "Invoice" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateInvoice", + "tags": [ + "Invoice" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putInvoice", + "tags": [ + "Invoice" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteInvoice", + "tags": [ + "Invoice" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_calendars": { + "get": { + "operationId": "listInvoiceCalendar", + "tags": [ + "InvoiceCalendar" + ], + "parameters": [ + { + "name": "filter[issuedOnDay]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][gt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][gte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][lt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][lte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnDay][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[issuedOnDay][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[issuedOnMonth]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][gt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][gte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][lt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][lte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[issuedOnMonth][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[invoices]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[invoices][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[totalCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[totalCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "issuedOnDay", + "issuedOnDay:asc", + "issuedOnDay:desc", + "issuedOnMonth", + "issuedOnMonth:asc", + "issuedOnMonth:desc", + "invoices", + "invoices:asc", + "invoices:desc", + "totalCents", + "totalCents:asc", + "totalCents:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceCalendarList" + }, + { + "$ref": "#/components/schemas/InvoiceCalendarPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_disputes": { + "get": { + "operationId": "listInvoiceDisputes", + "tags": [ + "InvoiceDisputes" + ], + "parameters": [ + { + "name": "filter[disputed]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[disputed][eq]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[disputed][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[invoices]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[invoices][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[bigInvoices]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[bigInvoices][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[bigInvoices][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "disputed", + "disputed:asc", + "disputed:desc", + "invoices", + "invoices:asc", + "invoices:desc", + "bigInvoices", + "bigInvoices:asc", + "bigInvoices:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceDisputesList" + }, + { + "$ref": "#/components/schemas/InvoiceDisputesPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_status_totals": { + "get": { + "operationId": "listInvoiceStatusTotals", + "tags": [ + "InvoiceStatusTotals" + ], + "parameters": [ + { + "name": "filter[status]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + } + }, + { + "name": "filter[status][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + } + }, + { + "name": "filter[status][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + } + }, + { + "name": "filter[status][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[status][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[status][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[invoices]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[invoices][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[totalCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[totalCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[paidCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[paidCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[paidCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "status", + "status:asc", + "status:desc", + "invoices", + "invoices:asc", + "invoices:desc", + "totalCents", + "totalCents:asc", + "totalCents:desc", + "paidCents", + "paidCents:asc", + "paidCents:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceStatusTotalsList" + }, + { + "$ref": "#/components/schemas/InvoiceStatusTotalsPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoice_totals": { + "get": { + "operationId": "listInvoiceTotals", + "tags": [ + "InvoiceTotals" + ], + "parameters": [ + { + "name": "filter[invoices]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[invoices][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[totalCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[totalCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[paidShare]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[paidShare][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[paidShare][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "invoices", + "invoices:asc", + "invoices:desc", + "totalCents", + "totalCents:asc", + "totalCents:desc", + "paidShare", + "paidShare:asc", + "paidShare:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoiceTotalsList" + }, + { + "$ref": "#/components/schemas/InvoiceTotalsPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/invoices_by_months": { + "get": { + "operationId": "listInvoicesByMonth", + "tags": [ + "InvoicesByMonth" + ], + "parameters": [ + { + "name": "filter[issuedOnMonth]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][gt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][gte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][lt]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][lte]", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "filter[issuedOnMonth][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[issuedOnMonth][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[invoices]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[invoices][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[invoices][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[totalCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[totalCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[totalCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "issuedOnMonth", + "issuedOnMonth:asc", + "issuedOnMonth:desc", + "invoices", + "invoices:asc", + "invoices:desc", + "totalCents", + "totalCents:asc", + "totalCents:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InvoicesByMonthList" + }, + { + "$ref": "#/components/schemas/InvoicesByMonthPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/products": { + "get": { + "operationId": "listProduct", + "tags": [ + "Product" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProductList" + }, + { + "$ref": "#/components/schemas/ProductPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createProduct", + "tags": [ + "Product" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/products/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getProduct", + "tags": [ + "Product" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateProduct", + "tags": [ + "Product" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putProduct", + "tags": [ + "Product" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteProduct", + "tags": [ + "Product" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/product_refunds": { + "get": { + "operationId": "listProductRefunds", + "tags": [ + "ProductRefunds" + ], + "parameters": [ + { + "name": "filter[productId]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[productId][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[productName]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[productName][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[sales]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[sales][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[hasRefund]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[hasRefund][eq]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[hasRefund][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[hasRefundOrFalse]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[hasRefundOrFalse][eq]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[hasRefundOrFalse][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "productId", + "productId:asc", + "productId:desc", + "productName", + "productName:asc", + "productName:desc", + "sales", + "sales:asc", + "sales:desc", + "hasRefund", + "hasRefund:asc", + "hasRefund:desc", + "hasRefundOrFalse", + "hasRefundOrFalse:asc", + "hasRefundOrFalse:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProductRefundsList" + }, + { + "$ref": "#/components/schemas/ProductRefundsPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/product_revenues": { + "get": { + "operationId": "listProductRevenue", + "tags": [ + "ProductRevenue" + ], + "parameters": [ + { + "name": "filter[productId]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[productId][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[productId][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[productName]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[productName][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[productName][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[sales]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[sales][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[revenueCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[revenueCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[revenueOrZero]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueOrZero][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[revenueOrZero][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "productId", + "productId:asc", + "productId:desc", + "productName", + "productName:asc", + "productName:desc", + "sales", + "sales:asc", + "sales:desc", + "revenueCents", + "revenueCents:asc", + "revenueCents:desc", + "revenueOrZero", + "revenueOrZero:asc", + "revenueOrZero:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProductRevenueList" + }, + { + "$ref": "#/components/schemas/ProductRevenuePage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/readings": { + "get": { + "operationId": "listReading", + "tags": [ + "Reading" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReadingList" + }, + { + "$ref": "#/components/schemas/ReadingPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createReading", + "tags": [ + "Reading" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadingCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reading" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/readings/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getReading", + "tags": [ + "Reading" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reading" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateReading", + "tags": [ + "Reading" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadingUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reading" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putReading", + "tags": [ + "Reading" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadingUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reading" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteReading", + "tags": [ + "Reading" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/reading_logs": { + "get": { + "operationId": "listReadingLog", + "tags": [ + "ReadingLog" + ], + "parameters": [ + { + "name": "filter[seq]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[seq][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[seq][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[readings]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[readings][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[readings][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "seq", + "seq:asc", + "seq:desc", + "readings", + "readings:asc", + "readings:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReadingLogList" + }, + { + "$ref": "#/components/schemas/ReadingLogPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sales": { + "get": { + "operationId": "listSale", + "tags": [ + "Sale" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SaleList" + }, + { + "$ref": "#/components/schemas/SalePage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createSale", + "tags": [ + "Sale" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaleCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sale" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sales/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getSale", + "tags": [ + "Sale" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sale" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateSale", + "tags": [ + "Sale" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sale" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putSale", + "tags": [ + "Sale" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sale" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteSale", + "tags": [ + "Sale" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sales_by_categories": { + "get": { + "operationId": "listSalesByCategory", + "tags": [ + "SalesByCategory" + ], + "parameters": [ + { + "name": "filter[categoryName]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[categoryName][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[categoryName][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[categoryName][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[categoryName][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[categoryName][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[sales]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[sales][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[revenueCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[revenueCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[maxCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[maxCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[maxCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[largeSales]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[largeSales][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[largeSales][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "categoryName", + "categoryName:asc", + "categoryName:desc", + "sales", + "sales:asc", + "sales:desc", + "revenueCents", + "revenueCents:asc", + "revenueCents:desc", + "maxCents", + "maxCents:asc", + "maxCents:desc", + "largeSales", + "largeSales:asc", + "largeSales:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SalesByCategoryList" + }, + { + "$ref": "#/components/schemas/SalesByCategoryPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sales_by_skus": { + "get": { + "operationId": "listSalesBySku", + "tags": [ + "SalesBySku" + ], + "parameters": [ + { + "name": "filter[productSku]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[productSku][eq]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[productSku][ne]", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter[productSku][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[productSku][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[sales]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[sales][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[sales][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[revenueCents]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[revenueCents][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[revenueCents][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "productSku", + "productSku:asc", + "productSku:desc", + "sales", + "sales:asc", + "sales:desc", + "revenueCents", + "revenueCents:asc", + "revenueCents:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SalesBySkuList" + }, + { + "$ref": "#/components/schemas/SalesBySkuPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/tickets": { + "get": { + "operationId": "listTicket", + "tags": [ + "Ticket" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/TicketList" + }, + { + "$ref": "#/components/schemas/TicketPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createTicket", + "tags": [ + "Ticket" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ticket" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/tickets/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getTicket", + "tags": [ + "Ticket" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ticket" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateTicket", + "tags": [ + "Ticket" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ticket" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putTicket", + "tags": [ + "Ticket" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ticket" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteTicket", + "tags": [ + "Ticket" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/tickets_by_tags": { + "get": { + "operationId": "listTicketsByTags", + "tags": [ + "TicketsByTags" + ], + "parameters": [ + { + "name": "filter[tickets]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[tickets][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[tickets][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "tickets", + "tickets:asc", + "tickets:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "csv", + "json", + "xml" + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/TicketsByTagsList" + }, + { + "$ref": "#/components/schemas/TicketsByTagsPage" + } + ] + } + }, + "text/csv": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Category": { + "title": "Category", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "CategoryCreate": { + "title": "CategoryCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "name" + ] + }, + "CategoryList": { + "title": "CategoryList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Category" + } + }, + "CategoryPage": { + "title": "CategoryPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Category" + } + }, + "total": { + "type": "integer" + } + } + }, + "CategoryUpdate": { + "title": "CategoryUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } + }, + "ExportCells": { + "title": "ExportCells", + "type": "object", + "properties": { + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 40 + }, + "cells": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "label", + "cells", + "totalCents" + ] + }, + "ExportCellsList": { + "title": "ExportCellsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExportCells" + } + }, + "ExportCellsPage": { + "title": "ExportCellsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExportCells" + } + }, + "total": { + "type": "integer" + } + } + }, + "ExportLine": { + "title": "ExportLine", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 40 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "label", + "amountCents" + ] + }, + "ExportLineCreate": { + "title": "ExportLineCreate", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 40 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "amountCents" + ] + }, + "ExportLineList": { + "title": "ExportLineList", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExportLine" + } + }, + "ExportLinePage": { + "title": "ExportLinePage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExportLine" + } + }, + "total": { + "type": "integer" + } + } + }, + "ExportLineUpdate": { + "title": "ExportLineUpdate", + "type": "object", + "properties": { + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 40 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + } + }, + "Invoice": { + "title": "Invoice", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "issuedOn": { + "type": "string", + "format": "date" + }, + "disputed": { + "type": "boolean" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents", + "issuedOn", + "disputed" + ] + }, + "InvoiceCalendar": { + "title": "InvoiceCalendar", + "type": "object", + "properties": { + "issuedOnDay": { + "type": "string", + "format": "date" + }, + "issuedOnMonth": { + "type": "string", + "format": "date" + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "issuedOnDay", + "issuedOnMonth", + "invoices", + "totalCents" + ] + }, + "InvoiceCalendarList": { + "title": "InvoiceCalendarList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceCalendar" + } + }, + "InvoiceCalendarPage": { + "title": "InvoiceCalendarPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceCalendar" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceCreate": { + "title": "InvoiceCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "issuedOn": { + "type": "string", + "format": "date" + }, + "disputed": { + "type": "boolean" + } + }, + "required": [ + "reference", + "status", + "amountCents", + "issuedOn", + "disputed" + ] + }, + "InvoiceDisputes": { + "title": "InvoiceDisputes", + "type": "object", + "properties": { + "disputed": { + "type": "boolean" + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "bigInvoices": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "disputed", + "invoices", + "bigInvoices" + ] + }, + "InvoiceDisputesList": { + "title": "InvoiceDisputesList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceDisputes" + } + }, + "InvoiceDisputesPage": { + "title": "InvoiceDisputesPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceDisputes" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceList": { + "title": "InvoiceList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice" + } + }, + "InvoicePage": { + "title": "InvoicePage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceStatusTotals": { + "title": "InvoiceStatusTotals", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "paidCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "status", + "invoices", + "totalCents", + "paidCents" + ] + }, + "InvoiceStatusTotalsList": { + "title": "InvoiceStatusTotalsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceStatusTotals" + } + }, + "InvoiceStatusTotalsPage": { + "title": "InvoiceStatusTotalsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceStatusTotals" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceTotals": { + "title": "InvoiceTotals", + "type": "object", + "properties": { + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "paidShare": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + } + }, + "required": [ + "invoices", + "totalCents", + "paidShare" + ] + }, + "InvoiceTotalsList": { + "title": "InvoiceTotalsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceTotals" + } + }, + "InvoiceTotalsPage": { + "title": "InvoiceTotalsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoiceTotals" + } + }, + "total": { + "type": "integer" + } + } + }, + "InvoiceUpdate": { + "title": "InvoiceUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "issuedOn": { + "type": "string", + "format": "date" + }, + "disputed": { + "type": "boolean" + } + } + }, + "InvoicesByMonth": { + "title": "InvoicesByMonth", + "type": "object", + "properties": { + "issuedOnMonth": { + "type": "string", + "format": "date" + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "issuedOnMonth", + "invoices", + "totalCents" + ] + }, + "InvoicesByMonthList": { + "title": "InvoicesByMonthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoicesByMonth" + } + }, + "InvoicesByMonthPage": { + "title": "InvoicesByMonthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvoicesByMonth" + } + }, + "total": { + "type": "integer" + } + } + }, + "Product": { + "title": "Product", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "sku": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "categoryId": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "id", + "name", + "sku", + "categoryId" + ] + }, + "ProductCreate": { + "title": "ProductCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": "string", + "format": "uuid" + }, + "categoryId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "name" + ] + }, + "ProductList": { + "title": "ProductList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + }, + "ProductPage": { + "title": "ProductPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + }, + "total": { + "type": "integer" + } + } + }, + "ProductRefunds": { + "title": "ProductRefunds", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "productName": { + "type": "string" + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "hasRefund": { + "type": [ + "boolean", + "null" + ] + }, + "hasRefundOrFalse": { + "type": "boolean" + } + }, + "required": [ + "productId", + "productName", + "sales", + "hasRefund", + "hasRefundOrFalse" + ] + }, + "ProductRefundsList": { + "title": "ProductRefundsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductRefunds" + } + }, + "ProductRefundsPage": { + "title": "ProductRefundsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductRefunds" + } + }, + "total": { + "type": "integer" + } + } + }, + "ProductRevenue": { + "title": "ProductRevenue", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "productName": { + "type": "string" + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "revenueOrZero": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "productId", + "productName", + "sales", + "revenueCents", + "revenueOrZero" + ] + }, + "ProductRevenueList": { + "title": "ProductRevenueList", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductRevenue" + } + }, + "ProductRevenuePage": { + "title": "ProductRevenuePage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductRevenue" + } + }, + "total": { + "type": "integer" + } + } + }, + "ProductUpdate": { + "title": "ProductUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "categoryId": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "Reading": { + "title": "Reading", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "seq": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "seq" + ] + }, + "ReadingCreate": { + "title": "ReadingCreate", + "type": "object", + "properties": { + "seq": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "seq" + ] + }, + "ReadingList": { + "title": "ReadingList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Reading" + } + }, + "ReadingLog": { + "title": "ReadingLog", + "type": "object", + "properties": { + "seq": { + "type": "integer", + "format": "int32" + }, + "readings": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "seq", + "readings" + ] + }, + "ReadingLogList": { + "title": "ReadingLogList", + "type": "array", + "items": { + "$ref": "#/components/schemas/ReadingLog" + } + }, + "ReadingLogPage": { + "title": "ReadingLogPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReadingLog" + } + }, + "total": { + "type": "integer" + } + } + }, + "ReadingPage": { + "title": "ReadingPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reading" + } + }, + "total": { + "type": "integer" + } + } + }, + "ReadingUpdate": { + "title": "ReadingUpdate", + "type": "object", + "properties": { + "seq": { + "type": "integer", + "format": "int32" + } + } + }, + "Sale": { + "title": "Sale", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "productId": { + "type": "integer", + "format": "int64" + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "refunded": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id", + "productId", + "amountCents", + "refunded" + ] + }, + "SaleCreate": { + "title": "SaleCreate", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "refunded": { + "type": "boolean" + } + }, + "required": [ + "productId", + "amountCents" + ] + }, + "SaleList": { + "title": "SaleList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Sale" + } + }, + "SalePage": { + "title": "SalePage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sale" + } + }, + "total": { + "type": "integer" + } + } + }, + "SaleUpdate": { + "title": "SaleUpdate", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "refunded": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "SalesByCategory": { + "title": "SalesByCategory", + "type": "object", + "properties": { + "categoryName": { + "type": [ + "string", + "null" + ] + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "maxCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "largeSales": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "categoryName", + "sales", + "revenueCents", + "maxCents", + "largeSales" + ] + }, + "SalesByCategoryList": { + "title": "SalesByCategoryList", + "type": "array", + "items": { + "$ref": "#/components/schemas/SalesByCategory" + } + }, + "SalesByCategoryPage": { + "title": "SalesByCategoryPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SalesByCategory" + } + }, + "total": { + "type": "integer" + } + } + }, + "SalesBySku": { + "title": "SalesBySku", + "type": "object", + "properties": { + "productSku": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "productSku", + "sales", + "revenueCents" + ] + }, + "SalesBySkuList": { + "title": "SalesBySkuList", + "type": "array", + "items": { + "$ref": "#/components/schemas/SalesBySku" + } + }, + "SalesBySkuPage": { + "title": "SalesBySkuPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SalesBySku" + } + }, + "total": { + "type": "integer" + } + } + }, + "Ticket": { + "title": "Ticket", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "attrs": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "id", + "tags", + "attrs" + ] + }, + "TicketCreate": { + "title": "TicketCreate", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "attrs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "TicketList": { + "title": "TicketList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ticket" + } + }, + "TicketPage": { + "title": "TicketPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Ticket" + } + }, + "total": { + "type": "integer" + } + } + }, + "TicketUpdate": { + "title": "TicketUpdate", + "type": "object", + "properties": { + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "attrs": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + } + } + }, + "TicketsByTags": { + "title": "TicketsByTags", + "type": "object", + "properties": { + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "attrs": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "tickets": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "tags", + "attrs", + "tickets" + ] + }, + "TicketsByTagsList": { + "title": "TicketsByTagsList", + "type": "array", + "items": { + "$ref": "#/components/schemas/TicketsByTags" + } + }, + "TicketsByTagsPage": { + "title": "TicketsByTagsPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TicketsByTags" + } + }, + "total": { + "type": "integer" + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.create.schema.json new file mode 100644 index 0000000000..60876477d1 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.create.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Category.create.schema.json", + "title": "Category.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.schema.json new file mode 100644 index 0000000000..cb997aa0d3 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Category.schema.json", + "title": "Category", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.strict.schema.json new file mode 100644 index 0000000000..1c77630357 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Category", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.update.schema.json new file mode 100644 index 0000000000..f92cdf2586 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Category.update.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Category.update.schema.json", + "title": "Category.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.schema.json new file mode 100644 index 0000000000..70311d6c60 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ExportCells.schema.json", + "title": "ExportCells", + "type": "object", + "properties": { + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 40 + }, + "cells": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "label", + "cells", + "totalCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.strict.schema.json new file mode 100644 index 0000000000..1cf6733004 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportCells.strict.schema.json @@ -0,0 +1,28 @@ +{ + "title": "ExportCells", + "type": "object", + "properties": { + "label": { + "description": "Maximum length: 40.", + "type": [ + "string", + "null" + ] + }, + "cells": { + "type": "integer" + }, + "totalCents": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "label", + "cells", + "totalCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.create.schema.json new file mode 100644 index 0000000000..45f230d9f1 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.create.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ExportLine.create.schema.json", + "title": "ExportLine.create", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 40 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "amountCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.schema.json new file mode 100644 index 0000000000..087476bc4b --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ExportLine.schema.json", + "title": "ExportLine", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 40 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "label", + "amountCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.strict.schema.json new file mode 100644 index 0000000000..e1a63e1200 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.strict.schema.json @@ -0,0 +1,21 @@ +{ + "title": "ExportLine", + "type": "object", + "properties": { + "label": { + "description": "Maximum length: 40.", + "type": [ + "string", + "null" + ] + }, + "amountCents": { + "type": "integer" + } + }, + "required": [ + "label", + "amountCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.update.schema.json new file mode 100644 index 0000000000..3d9fc8fc87 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ExportLine.update.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ExportLine.update.schema.json", + "title": "ExportLine.update", + "type": "object", + "properties": { + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 40 + }, + "amountCents": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.create.schema.json new file mode 100644 index 0000000000..4e5d6fa656 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.create.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Invoice.create.schema.json", + "title": "Invoice.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "issuedOn": { + "type": "string", + "format": "date" + }, + "disputed": { + "type": "boolean" + } + }, + "required": [ + "reference", + "status", + "amountCents", + "issuedOn", + "disputed" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.schema.json new file mode 100644 index 0000000000..b79626b13e --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Invoice.schema.json", + "title": "Invoice", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reference": { + "type": "string", + "maxLength": 40 + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "issuedOn": { + "type": "string", + "format": "date" + }, + "disputed": { + "type": "boolean" + } + }, + "required": [ + "id", + "reference", + "status", + "amountCents", + "issuedOn", + "disputed" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.strict.schema.json new file mode 100644 index 0000000000..84ff43045c --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.strict.schema.json @@ -0,0 +1,36 @@ +{ + "title": "Invoice", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 40.", + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer" + }, + "issuedOn": { + "description": "Format: date.", + "type": "string" + }, + "disputed": { + "type": "boolean" + } + }, + "required": [ + "reference", + "status", + "amountCents", + "issuedOn", + "disputed" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.update.schema.json new file mode 100644 index 0000000000..4ef8971388 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Invoice.update.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Invoice.update.schema.json", + "title": "Invoice.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "issuedOn": { + "type": "string", + "format": "date" + }, + "disputed": { + "type": "boolean" + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.schema.json new file mode 100644 index 0000000000..4a1ff0c5ed --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceCalendar.schema.json", + "title": "InvoiceCalendar", + "type": "object", + "properties": { + "issuedOnDay": { + "type": "string", + "format": "date" + }, + "issuedOnMonth": { + "type": "string", + "format": "date" + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "issuedOnDay", + "issuedOnMonth", + "invoices", + "totalCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.strict.schema.json new file mode 100644 index 0000000000..60fa19ffe5 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceCalendar.strict.schema.json @@ -0,0 +1,30 @@ +{ + "title": "InvoiceCalendar", + "type": "object", + "properties": { + "issuedOnDay": { + "description": "Format: date.", + "type": "string" + }, + "issuedOnMonth": { + "description": "Format: date.", + "type": "string" + }, + "invoices": { + "type": "integer" + }, + "totalCents": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "issuedOnDay", + "issuedOnMonth", + "invoices", + "totalCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.schema.json new file mode 100644 index 0000000000..4afac77e8f --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceDisputes.schema.json", + "title": "InvoiceDisputes", + "type": "object", + "properties": { + "disputed": { + "type": "boolean" + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "bigInvoices": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "disputed", + "invoices", + "bigInvoices" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.strict.schema.json new file mode 100644 index 0000000000..4aa59a10d0 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceDisputes.strict.schema.json @@ -0,0 +1,21 @@ +{ + "title": "InvoiceDisputes", + "type": "object", + "properties": { + "disputed": { + "type": "boolean" + }, + "invoices": { + "type": "integer" + }, + "bigInvoices": { + "type": "integer" + } + }, + "required": [ + "disputed", + "invoices", + "bigInvoices" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.schema.json new file mode 100644 index 0000000000..fd9b3c8824 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceStatusTotals.schema.json", + "title": "InvoiceStatusTotals", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "paidCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "status", + "invoices", + "totalCents", + "paidCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.strict.schema.json new file mode 100644 index 0000000000..1a053a8250 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceStatusTotals.strict.schema.json @@ -0,0 +1,36 @@ +{ + "title": "InvoiceStatusTotals", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "OPEN", + "PAID", + "VOID" + ] + }, + "invoices": { + "type": "integer" + }, + "totalCents": { + "type": [ + "integer", + "null" + ] + }, + "paidCents": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "status", + "invoices", + "totalCents", + "paidCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json new file mode 100644 index 0000000000..1e840b66ed --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoiceTotals.schema.json", + "title": "InvoiceTotals", + "type": "object", + "properties": { + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "paidShare": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + } + }, + "required": [ + "invoices", + "totalCents", + "paidShare" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json new file mode 100644 index 0000000000..64e94ce688 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json @@ -0,0 +1,28 @@ +{ + "title": "InvoiceTotals", + "type": "object", + "properties": { + "invoices": { + "type": "integer" + }, + "totalCents": { + "type": [ + "integer", + "null" + ] + }, + "paidShare": { + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "invoices", + "totalCents", + "paidShare" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.schema.json new file mode 100644 index 0000000000..2b9f27164d --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/InvoicesByMonth.schema.json", + "title": "InvoicesByMonth", + "type": "object", + "properties": { + "issuedOnMonth": { + "type": "string", + "format": "date" + }, + "invoices": { + "type": "integer", + "format": "int64" + }, + "totalCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "issuedOnMonth", + "invoices", + "totalCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.strict.schema.json new file mode 100644 index 0000000000..376ce3a0ca --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoicesByMonth.strict.schema.json @@ -0,0 +1,25 @@ +{ + "title": "InvoicesByMonth", + "type": "object", + "properties": { + "issuedOnMonth": { + "description": "Format: date.", + "type": "string" + }, + "invoices": { + "type": "integer" + }, + "totalCents": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "issuedOnMonth", + "invoices", + "totalCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.create.schema.json new file mode 100644 index 0000000000..4ab46812ab --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.create.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Product.create.schema.json", + "title": "Product.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": "string", + "format": "uuid" + }, + "categoryId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.schema.json new file mode 100644 index 0000000000..5b308ce166 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Product.schema.json", + "title": "Product", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "sku": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "categoryId": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "id", + "name", + "sku", + "categoryId" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.strict.schema.json new file mode 100644 index 0000000000..cf2ad3572b --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.strict.schema.json @@ -0,0 +1,29 @@ +{ + "title": "Product", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1.", + "type": "string" + }, + "sku": { + "description": "Format: uuid.", + "type": [ + "string", + "null" + ] + }, + "categoryId": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "name", + "sku", + "categoryId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.update.schema.json new file mode 100644 index 0000000000..9bdeba3e1e --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Product.update.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Product.update.schema.json", + "title": "Product.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "categoryId": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.schema.json new file mode 100644 index 0000000000..4f61f08968 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ProductRefunds.schema.json", + "title": "ProductRefunds", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "productName": { + "type": "string" + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "hasRefund": { + "type": [ + "boolean", + "null" + ] + }, + "hasRefundOrFalse": { + "type": "boolean" + } + }, + "required": [ + "productId", + "productName", + "sales", + "hasRefund", + "hasRefundOrFalse" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.strict.schema.json new file mode 100644 index 0000000000..fc8d8e5682 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRefunds.strict.schema.json @@ -0,0 +1,32 @@ +{ + "title": "ProductRefunds", + "type": "object", + "properties": { + "productId": { + "type": "integer" + }, + "productName": { + "type": "string" + }, + "sales": { + "type": "integer" + }, + "hasRefund": { + "type": [ + "boolean", + "null" + ] + }, + "hasRefundOrFalse": { + "type": "boolean" + } + }, + "required": [ + "productId", + "productName", + "sales", + "hasRefund", + "hasRefundOrFalse" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.schema.json new file mode 100644 index 0000000000..bb967ddbe7 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ProductRevenue.schema.json", + "title": "ProductRevenue", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "productName": { + "type": "string" + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "revenueOrZero": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "productId", + "productName", + "sales", + "revenueCents", + "revenueOrZero" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.strict.schema.json new file mode 100644 index 0000000000..6bbbe356bc --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ProductRevenue.strict.schema.json @@ -0,0 +1,32 @@ +{ + "title": "ProductRevenue", + "type": "object", + "properties": { + "productId": { + "type": "integer" + }, + "productName": { + "type": "string" + }, + "sales": { + "type": "integer" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ] + }, + "revenueOrZero": { + "type": "integer" + } + }, + "required": [ + "productId", + "productName", + "sales", + "revenueCents", + "revenueOrZero" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.create.schema.json new file mode 100644 index 0000000000..4f7a3eb2fa --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.create.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Reading.create.schema.json", + "title": "Reading.create", + "type": "object", + "properties": { + "seq": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "seq" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.schema.json new file mode 100644 index 0000000000..4d3ff3a703 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Reading.schema.json", + "title": "Reading", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "seq": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "seq" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.strict.schema.json new file mode 100644 index 0000000000..552ea5a9a1 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.strict.schema.json @@ -0,0 +1,13 @@ +{ + "title": "Reading", + "type": "object", + "properties": { + "seq": { + "type": "integer" + } + }, + "required": [ + "seq" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.update.schema.json new file mode 100644 index 0000000000..a2b68e42f7 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Reading.update.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Reading.update.schema.json", + "title": "Reading.update", + "type": "object", + "properties": { + "seq": { + "type": "integer", + "format": "int32" + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.schema.json new file mode 100644 index 0000000000..13f9f93c79 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/ReadingLog.schema.json", + "title": "ReadingLog", + "type": "object", + "properties": { + "seq": { + "type": "integer", + "format": "int32" + }, + "readings": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "seq", + "readings" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.strict.schema.json new file mode 100644 index 0000000000..8bc0381166 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/ReadingLog.strict.schema.json @@ -0,0 +1,17 @@ +{ + "title": "ReadingLog", + "type": "object", + "properties": { + "seq": { + "type": "integer" + }, + "readings": { + "type": "integer" + } + }, + "required": [ + "seq", + "readings" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.create.schema.json new file mode 100644 index 0000000000..90976efa80 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.create.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Sale.create.schema.json", + "title": "Sale.create", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "refunded": { + "type": "boolean" + } + }, + "required": [ + "productId", + "amountCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.schema.json new file mode 100644 index 0000000000..c23b5f37aa --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Sale.schema.json", + "title": "Sale", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "productId": { + "type": "integer", + "format": "int64" + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "refunded": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id", + "productId", + "amountCents", + "refunded" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.strict.schema.json new file mode 100644 index 0000000000..4b923634c6 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.strict.schema.json @@ -0,0 +1,24 @@ +{ + "title": "Sale", + "type": "object", + "properties": { + "productId": { + "type": "integer" + }, + "amountCents": { + "type": "integer" + }, + "refunded": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "productId", + "amountCents", + "refunded" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.update.schema.json new file mode 100644 index 0000000000..a25b77be1b --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Sale.update.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Sale.update.schema.json", + "title": "Sale.update", + "type": "object", + "properties": { + "productId": { + "type": "integer", + "format": "int64" + }, + "amountCents": { + "type": "integer", + "format": "int64" + }, + "refunded": { + "type": [ + "boolean", + "null" + ] + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.schema.json new file mode 100644 index 0000000000..d5f04f35eb --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/SalesByCategory.schema.json", + "title": "SalesByCategory", + "type": "object", + "properties": { + "categoryName": { + "type": [ + "string", + "null" + ] + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "maxCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "largeSales": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "categoryName", + "sales", + "revenueCents", + "maxCents", + "largeSales" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.strict.schema.json new file mode 100644 index 0000000000..2e325d5018 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesByCategory.strict.schema.json @@ -0,0 +1,38 @@ +{ + "title": "SalesByCategory", + "type": "object", + "properties": { + "categoryName": { + "type": [ + "string", + "null" + ] + }, + "sales": { + "type": "integer" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ] + }, + "maxCents": { + "type": [ + "integer", + "null" + ] + }, + "largeSales": { + "type": "integer" + } + }, + "required": [ + "categoryName", + "sales", + "revenueCents", + "maxCents", + "largeSales" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.schema.json new file mode 100644 index 0000000000..2a5df3c5ce --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/SalesBySku.schema.json", + "title": "SalesBySku", + "type": "object", + "properties": { + "productSku": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "sales": { + "type": "integer", + "format": "int64" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "productSku", + "sales", + "revenueCents" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.strict.schema.json new file mode 100644 index 0000000000..01b83e2a7f --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/SalesBySku.strict.schema.json @@ -0,0 +1,28 @@ +{ + "title": "SalesBySku", + "type": "object", + "properties": { + "productSku": { + "description": "Format: uuid.", + "type": [ + "string", + "null" + ] + }, + "sales": { + "type": "integer" + }, + "revenueCents": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "productSku", + "sales", + "revenueCents" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.create.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.create.schema.json new file mode 100644 index 0000000000..839d4aeae1 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.create.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Ticket.create.schema.json", + "title": "Ticket.create", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "attrs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.schema.json new file mode 100644 index 0000000000..5f27bb9d8c --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Ticket.schema.json", + "title": "Ticket", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "attrs": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "id", + "tags", + "attrs" + ] +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.update.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.update.schema.json new file mode 100644 index 0000000000..fd7c87c404 --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/Ticket.update.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/Ticket.update.schema.json", + "title": "Ticket.update", + "type": "object", + "properties": { + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "attrs": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/TicketsByTags.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/TicketsByTags.schema.json new file mode 100644 index 0000000000..ee1a168e7f --- /dev/null +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/TicketsByTags.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/sales/TicketsByTags.schema.json", + "title": "TicketsByTags", + "type": "object", + "properties": { + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "attrs": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "tickets": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "tags", + "attrs", + "tickets" + ] +} diff --git a/fixtures/contract-conformance/secret-fields/case.json b/fixtures/contract-conformance/secret-fields/case.json new file mode 100644 index 0000000000..e998108cca --- /dev/null +++ b/fixtures/contract-conformance/secret-fields/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/secret-fields/meta.json" } diff --git a/fixtures/contract-conformance/secret-fields/expected/openapi.json b/fixtures/contract-conformance/secret-fields/expected/openapi.json new file mode 100644 index 0000000000..b2cd08f362 --- /dev/null +++ b/fixtures/contract-conformance/secret-fields/expected/openapi.json @@ -0,0 +1,543 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "secret-fields", + "version": "1.0.0" + }, + "paths": { + "/api/accounts": { + "get": { + "operationId": "listAccount", + "tags": [ + "Account" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[name]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[name][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[name][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "name", + "name:asc", + "name:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AccountList" + }, + { + "$ref": "#/components/schemas/AccountPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createAccount", + "tags": [ + "Account" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/accounts/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getAccount", + "tags": [ + "Account" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateAccount", + "tags": [ + "Account" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putAccount", + "tags": [ + "Account" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteAccount", + "tags": [ + "Account" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Account": { + "title": "Account", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "tokenHash": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + }, + "required": [ + "id", + "name", + "tokenHash" + ] + }, + "AccountCreate": { + "title": "AccountCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + }, + "required": [ + "name" + ] + }, + "AccountList": { + "title": "AccountList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Account" + } + }, + "AccountPage": { + "title": "AccountPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Account" + } + }, + "total": { + "type": "integer" + } + } + }, + "AccountUpdate": { + "title": "AccountUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + }, + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.create.schema.json b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.create.schema.json new file mode 100644 index 0000000000..5e0344176e --- /dev/null +++ b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/secrets/Account.create.schema.json", + "title": "Account.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.schema.json b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.schema.json new file mode 100644 index 0000000000..e66a8f1d5b --- /dev/null +++ b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/secrets/Account.schema.json", + "title": "Account", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "tokenHash": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + }, + "required": [ + "id", + "name", + "tokenHash" + ] +} diff --git a/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.strict.schema.json b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.strict.schema.json new file mode 100644 index 0000000000..5b1ec9e974 --- /dev/null +++ b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Account", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1. Maximum length: 100.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.update.schema.json b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.update.schema.json new file mode 100644 index 0000000000..df13f1ec3d --- /dev/null +++ b/fixtures/contract-conformance/secret-fields/expected/schemas/acme/secrets/Account.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/secrets/Account.update.schema.json", + "title": "Account.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } +} diff --git a/fixtures/contract-conformance/tph/case.json b/fixtures/contract-conformance/tph/case.json new file mode 100644 index 0000000000..302701ba16 --- /dev/null +++ b/fixtures/contract-conformance/tph/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/tph/meta.json" } diff --git a/fixtures/contract-conformance/tph/expected/openapi.json b/fixtures/contract-conformance/tph/expected/openapi.json new file mode 100644 index 0000000000..3693e1e4f8 --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/openapi.json @@ -0,0 +1,2239 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "tph", + "version": "1.0.0" + }, + "paths": { + "/api/auths": { + "get": { + "operationId": "listAuth", + "tags": [ + "Auth" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[type]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "enum": [ + "Bridge", + "Copay", + "PriorAuth" + ] + } + }, + { + "name": "filter[type][eq]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "Bridge", + "Copay", + "PriorAuth" + ] + } + }, + { + "name": "filter[type][ne]", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "Bridge", + "Copay", + "PriorAuth" + ] + } + }, + { + "name": "filter[type][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[type][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[type][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "type", + "type:asc", + "type:desc", + "reference", + "reference:asc", + "reference:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AuthList" + }, + { + "$ref": "#/components/schemas/AuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getAuth", + "tags": [ + "Auth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Auth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/bridge": { + "get": { + "operationId": "listBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[quantity]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "filter[quantity][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[quantity][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "reference", + "reference:asc", + "reference:desc", + "quantity", + "quantity:asc", + "quantity:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/BridgeAuthList" + }, + { + "$ref": "#/components/schemas/BridgeAuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuthCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/bridge/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/copay": { + "get": { + "operationId": "listCopayAuth", + "tags": [ + "CopayAuth" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[copayAmount]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][eq]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][ne]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][gt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][gte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][lt]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][lte]", + "in": "query", + "schema": { + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" + } + }, + { + "name": "filter[copayAmount][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[copayAmount][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "reference", + "reference:asc", + "reference:desc", + "copayAmount", + "copayAmount:asc", + "copayAmount:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CopayAuthList" + }, + { + "$ref": "#/components/schemas/CopayAuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createCopayAuth", + "tags": [ + "CopayAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuthCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/copay/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getCopayAuth", + "tags": [ + "CopayAuth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateCopayAuth", + "tags": [ + "CopayAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putCopayAuth", + "tags": [ + "CopayAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCopayAuth", + "tags": [ + "CopayAuth" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/priorauth": { + "get": { + "operationId": "listPriorAuthAuth", + "tags": [ + "PriorAuthAuth" + ], + "parameters": [ + { + "name": "filter[id]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][eq]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][ne]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][gte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lt]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][lte]", + "in": "query", + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "filter[id][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[id][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[reference]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[reference][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[reference][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[approver]", + "in": "query", + "description": "Sugar for the eq operator.", + "schema": { + "type": "string" + } + }, + { + "name": "filter[approver][eq]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[approver][ne]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[approver][in]", + "in": "query", + "schema": { + "type": "string", + "description": "Comma-separated values." + } + }, + { + "name": "filter[approver][like]", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filter[approver][isNull]", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "id:asc", + "id:desc", + "reference", + "reference:asc", + "reference:desc", + "approver", + "approver:asc", + "approver:desc" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PriorAuthAuthList" + }, + { + "$ref": "#/components/schemas/PriorAuthAuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createPriorAuthAuth", + "tags": [ + "PriorAuthAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuthCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/priorauth/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getPriorAuthAuth", + "tags": [ + "PriorAuthAuth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updatePriorAuthAuth", + "tags": [ + "PriorAuthAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putPriorAuthAuth", + "tags": [ + "PriorAuthAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriorAuthAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deletePriorAuthAuth", + "tags": [ + "PriorAuthAuth" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Auth": { + "title": "Auth", + "oneOf": [ + { + "$ref": "#/components/schemas/BridgeAuth" + }, + { + "$ref": "#/components/schemas/CopayAuth" + }, + { + "$ref": "#/components/schemas/PriorAuthAuth" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "Bridge": "#/components/schemas/BridgeAuth", + "Copay": "#/components/schemas/CopayAuth", + "PriorAuth": "#/components/schemas/PriorAuthAuth" + } + } + }, + "AuthList": { + "title": "AuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Auth" + } + }, + "AuthPage": { + "title": "AuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Auth" + } + }, + "total": { + "type": "integer" + } + } + }, + "BridgeAuth": { + "title": "BridgeAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Bridge" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "quantity": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "type", + "reference", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt", + "quantity" + ] + }, + "BridgeAuthCreate": { + "title": "BridgeAuthCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + }, + "quantity": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "reference", + "quantity" + ] + }, + "BridgeAuthList": { + "title": "BridgeAuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/BridgeAuth" + } + }, + "BridgeAuthPage": { + "title": "BridgeAuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BridgeAuth" + } + }, + "total": { + "type": "integer" + } + } + }, + "BridgeAuthUpdate": { + "title": "BridgeAuthUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "quantity": { + "type": "integer", + "format": "int32" + } + } + }, + "CopayAuth": { + "title": "CopayAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Copay" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "copayAmount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + } + }, + "required": [ + "id", + "type", + "reference", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt", + "copayAmount" + ] + }, + "CopayAuthCreate": { + "title": "CopayAuthCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + }, + "copayAmount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + } + }, + "required": [ + "reference" + ] + }, + "CopayAuthList": { + "title": "CopayAuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/CopayAuth" + } + }, + "CopayAuthPage": { + "title": "CopayAuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CopayAuth" + } + }, + "total": { + "type": "integer" + } + } + }, + "CopayAuthUpdate": { + "title": "CopayAuthUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "copayAmount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + } + } + }, + "PriorAuthAuth": { + "title": "PriorAuthAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "PriorAuth" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "approver": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + }, + "required": [ + "id", + "type", + "reference", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt", + "approver" + ] + }, + "PriorAuthAuthCreate": { + "title": "PriorAuthAuthCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + }, + "approver": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "reference" + ] + }, + "PriorAuthAuthList": { + "title": "PriorAuthAuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/PriorAuthAuth" + } + }, + "PriorAuthAuthPage": { + "title": "PriorAuthAuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PriorAuthAuth" + } + }, + "total": { + "type": "integer" + } + } + }, + "PriorAuthAuthUpdate": { + "title": "PriorAuthAuthUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "approver": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/Auth.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/Auth.schema.json new file mode 100644 index 0000000000..2ee81ea91e --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/Auth.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/Auth.schema.json", + "title": "Auth", + "oneOf": [ + { + "$ref": "./BridgeAuth.schema.json" + }, + { + "$ref": "./CopayAuth.schema.json" + }, + { + "$ref": "./PriorAuthAuth.schema.json" + } + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.create.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.create.schema.json new file mode 100644 index 0000000000..c508de35cb --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.create.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/BridgeAuth.create.schema.json", + "title": "BridgeAuth.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + }, + "quantity": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "reference", + "quantity" + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.schema.json new file mode 100644 index 0000000000..ddc3ff7232 --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/BridgeAuth.schema.json", + "title": "BridgeAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Bridge" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "quantity": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "type", + "reference", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt", + "quantity" + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.strict.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.strict.schema.json new file mode 100644 index 0000000000..c2a0e81cc8 --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.strict.schema.json @@ -0,0 +1,26 @@ +{ + "title": "BridgeAuth", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "issuedCurrency": { + "description": "Maximum length: 3.", + "type": [ + "string", + "null" + ] + }, + "quantity": { + "type": "integer" + } + }, + "required": [ + "reference", + "issuedCurrency", + "quantity" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.update.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.update.schema.json new file mode 100644 index 0000000000..85e0b051b1 --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/BridgeAuth.update.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/BridgeAuth.update.schema.json", + "title": "BridgeAuth.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "quantity": { + "type": "integer", + "format": "int32" + } + } +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.create.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.create.schema.json new file mode 100644 index 0000000000..d669f9f28a --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.create.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/CopayAuth.create.schema.json", + "title": "CopayAuth.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + }, + "copayAmount": { + "type": [ + "string", + "number" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + } + }, + "required": [ + "reference" + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json new file mode 100644 index 0000000000..a53fcc5ceb --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/CopayAuth.schema.json", + "title": "CopayAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Copay" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "copayAmount": { + "type": [ + "string", + "number", + "null" + ], + "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + } + }, + "required": [ + "id", + "type", + "reference", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt", + "copayAmount" + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.strict.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.strict.schema.json new file mode 100644 index 0000000000..8aeeb3445b --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.strict.schema.json @@ -0,0 +1,30 @@ +{ + "title": "CopayAuth", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "issuedCurrency": { + "description": "Maximum length: 3.", + "type": [ + "string", + "null" + ] + }, + "copayAmount": { + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2. Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "reference", + "issuedCurrency", + "copayAmount" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.update.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.update.schema.json new file mode 100644 index 0000000000..30d0ed1ddb --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.update.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/CopayAuth.update.schema.json", + "title": "CopayAuth.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "copayAmount": { + "type": [ + "string", + "number", + "null" + ], + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "description": "A decimal, as a string in JSON-number grammar or a JSON number. Precision 10, scale 2." + } + } +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.create.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.create.schema.json new file mode 100644 index 0000000000..c33766e807 --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.create.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/PriorAuthAuth.create.schema.json", + "title": "PriorAuthAuth.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "issuedCurrency": { + "type": "string", + "maxLength": 3 + }, + "approver": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "reference" + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.schema.json new file mode 100644 index 0000000000..d53d846f5d --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/PriorAuthAuth.schema.json", + "title": "PriorAuthAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "PriorAuth" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "issuedCurrency": { + "type": [ + "string", + "null" + ], + "maxLength": 3 + }, + "autoCreatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "autoUpdatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "approver": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + }, + "required": [ + "id", + "type", + "reference", + "issuedCurrency", + "autoCreatedAt", + "autoUpdatedAt", + "approver" + ] +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.strict.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.strict.schema.json new file mode 100644 index 0000000000..96db0afeba --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.strict.schema.json @@ -0,0 +1,30 @@ +{ + "title": "PriorAuthAuth", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + }, + "issuedCurrency": { + "description": "Maximum length: 3.", + "type": [ + "string", + "null" + ] + }, + "approver": { + "description": "Maximum length: 80.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "reference", + "issuedCurrency", + "approver" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.update.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.update.schema.json new file mode 100644 index 0000000000..81d8db395c --- /dev/null +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/PriorAuthAuth.update.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/PriorAuthAuth.update.schema.json", + "title": "PriorAuthAuth.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "approver": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/write-through/case.json b/fixtures/contract-conformance/write-through/case.json new file mode 100644 index 0000000000..e616937046 --- /dev/null +++ b/fixtures/contract-conformance/write-through/case.json @@ -0,0 +1 @@ +{ "meta": "fixtures/api-contract-conformance/write-through/meta.json" } diff --git a/fixtures/contract-conformance/write-through/expected/openapi.json b/fixtures/contract-conformance/write-through/expected/openapi.json new file mode 100644 index 0000000000..567483d952 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/openapi.json @@ -0,0 +1,780 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "write-through", + "version": "1.0.0" + }, + "paths": { + "/api/customers": { + "get": { + "operationId": "listCustomer", + "tags": [ + "Customer" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CustomerList" + }, + { + "$ref": "#/components/schemas/CustomerPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createCustomer", + "tags": [ + "Customer" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/customers/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getCustomer", + "tags": [ + "Customer" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateCustomer", + "tags": [ + "Customer" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putCustomer", + "tags": [ + "Customer" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCustomer", + "tags": [ + "Customer" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/orders": { + "get": { + "operationId": "listOrder", + "tags": [ + "Order" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/OrderList" + }, + { + "$ref": "#/components/schemas/OrderPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createOrder", + "tags": [ + "Order" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/orders/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getOrder", + "tags": [ + "Order" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateOrder", + "tags": [ + "Order" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putOrder", + "tags": [ + "Order" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteOrder", + "tags": [ + "Order" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Customer": { + "title": "Customer", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] + }, + "CustomerCreate": { + "title": "CustomerCreate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] + }, + "CustomerList": { + "title": "CustomerList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Customer" + } + }, + "CustomerPage": { + "title": "CustomerPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Customer" + } + }, + "total": { + "type": "integer" + } + } + }, + "CustomerUpdate": { + "title": "CustomerUpdate", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "Order": { + "title": "Order", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "customerId": { + "type": "integer", + "format": "int64" + }, + "customerName": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "customerId", + "customerName" + ] + }, + "OrderCreate": { + "title": "OrderCreate", + "type": "object", + "properties": { + "customerId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "customerId" + ] + }, + "OrderList": { + "title": "OrderList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "OrderPage": { + "title": "OrderPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "total": { + "type": "integer" + } + } + }, + "OrderUpdate": { + "title": "OrderUpdate", + "type": "object", + "properties": { + "customerId": { + "type": "integer", + "format": "int64" + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.create.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.create.schema.json new file mode 100644 index 0000000000..a81a9826b3 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/orders/Customer.create.schema.json", + "title": "Customer.create", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "name" + ] +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.schema.json new file mode 100644 index 0000000000..0b85cae030 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/orders/Customer.schema.json", + "title": "Customer", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "maxLength": 80 + } + }, + "required": [ + "id", + "name" + ] +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.strict.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.strict.schema.json new file mode 100644 index 0000000000..781e1e41d2 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Customer", + "type": "object", + "properties": { + "name": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.update.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.update.schema.json new file mode 100644 index 0000000000..5a598e11e2 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Customer.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/orders/Customer.update.schema.json", + "title": "Customer.update", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.create.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.create.schema.json new file mode 100644 index 0000000000..26a9cade7b --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.create.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/orders/Order.create.schema.json", + "title": "Order.create", + "type": "object", + "properties": { + "customerId": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "customerId" + ] +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.schema.json new file mode 100644 index 0000000000..109288b421 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/orders/Order.schema.json", + "title": "Order", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "customerId": { + "type": "integer", + "format": "int64" + }, + "customerName": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "customerId", + "customerName" + ] +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.strict.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.strict.schema.json new file mode 100644 index 0000000000..ea8f521505 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.strict.schema.json @@ -0,0 +1,13 @@ +{ + "title": "Order", + "type": "object", + "properties": { + "customerId": { + "type": "integer" + } + }, + "required": [ + "customerId" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.update.schema.json b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.update.schema.json new file mode 100644 index 0000000000..1ac3659052 --- /dev/null +++ b/fixtures/contract-conformance/write-through/expected/schemas/acme/orders/Order.update.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/orders/Order.update.schema.json", + "title": "Order.update", + "type": "object", + "properties": { + "customerId": { + "type": "integer", + "format": "int64" + } + } +} diff --git a/fixtures/generator-registry-conformance/README.md b/fixtures/generator-registry-conformance/README.md index 28265ef407..9249a51881 100644 --- a/fixtures/generator-registry-conformance/README.md +++ b/fixtures/generator-registry-conformance/README.md @@ -34,7 +34,7 @@ to make the choice cheap, and `layer` is the axis it is cheap along. |---|---|---| | `model` | entity, names, barrel, dto, value-object | app shape | | `persistence` | queries, prisma-schema, db-context, repository, exposed-table, relations, stored-proc | app shape | -| `api` | routes, routes-hono, filter-allowlist, validator, spring-config | app shape | +| `api` | routes, routes-hono, filter-allowlist, validator, spring-config, contract-schema, openapi | app shape | | `client` | form, hooks, grid, grid-hook | app shape | | `docs` | docs, api-docs, requirements, agent-docs, metamodel-docs, docs-site, plantuml, html-docs, ai-docs | declared in the project's generator list like any other; nothing is generated that the list does not declare (ADR-0034 Amendment 9) | | `capability` | prompt-render, output-parser, output-prompt, extractor, render-helper, trace-helper, requirement-tests, cube-model, shared-model, template, callable, json-schema, mustache | `meta gen --list --probe` | diff --git a/fixtures/generator-registry-conformance/registry.json b/fixtures/generator-registry-conformance/registry.json index 8a17d9368e..44db6ebcf4 100644 --- a/fixtures/generator-registry-conformance/registry.json +++ b/fixtures/generator-registry-conformance/registry.json @@ -239,6 +239,22 @@ "eject": "full", "ports": ["typescript", "csharp", "java", "kotlin", "python"] }, + "contract-schema": { + "concept": "JSON Schema 2020-12 for the model's wire shapes (read, create, update), and a strict structured-output profile. A different concept from json-schema, which describes metadata files.", + "tier": "neutral", + "layer": "api", + "note": "TYPESCRIPT ONLY by design (ADR-0034 Amendment 10): a neutral artifact with one engine, so an adopter on any port runs it through the Node `meta` CLI, and the output depends on the model and the wire setting, never on the port.", + "eject": "full", + "ports": ["typescript"] + }, + "openapi": { + "concept": "One OpenAPI 3.1 document for the generated REST surface, whose components are the contract-schema shapes.", + "tier": "neutral", + "layer": "api", + "note": "TYPESCRIPT ONLY by design (ADR-0034 Amendment 10): every port's routes answer the same REST contract, so one engine states it for all of them.", + "eject": "full", + "ports": ["typescript"] + }, "cube-model": { "concept": "Cube data model files (model/cubes/*.yml, model/views/*.yml) exported from the reporting vocabulary.", "tier": "native", diff --git a/server/typescript/packages/cli/README.md b/server/typescript/packages/cli/README.md index 629d6e6848..cae4c2ba87 100644 --- a/server/typescript/packages/cli/README.md +++ b/server/typescript/packages/cli/README.md @@ -79,9 +79,9 @@ meta gen --list --format json # to swap in, the entry to wire, and what to install. meta eject entity -# Need an output none of them emits (OpenAPI, JSON Schema, a client, docs)? Write your -# own: this scaffolds a working, commented generator and wires it into the config. -meta generator new openapi --scope model +# Need an output none of them emits (a client, a data dictionary, a search mapping)? Write +# your own: this scaffolds a working, commented generator and wires it into the config. +meta generator new data-dictionary --scope model # 5. Generate TS code (config-driven via metaobjects.config.ts) meta gen @@ -131,14 +131,14 @@ On a real write run (not `--dry-run`), `meta gen` also runs an **advisory anti-p ### `meta generator new ` -Writes a generator of your own — the primary way to get an output MetaObjects does not ship (OpenAPI, JSON Schema, a client, a service layer, docs). `codegen/generators/.ts` is a small, commented generator that already runs: it emits one JSON file per unit describing the model, read through the resolving accessors. The command also adds the import and the `Generator()` entry to `metaobjects.config.ts` when the config has one literal `generators: [...]`, and otherwise prints the two lines to add. `meta gen` runs it and `meta verify --codegen` gates it with nothing to register; change the emit from there. +Writes a generator of your own — the primary way to get an output MetaObjects does not ship (a client, a service layer, a data dictionary). `codegen/generators/.ts` is a small, commented generator that already runs: it emits one JSON file per unit describing the model, read through the resolving accessors. The command also adds the import and the `Generator()` entry to `metaobjects.config.ts` when the config has one literal `generators: [...]`, and otherwise prints the two lines to add. `meta gen` runs it and `meta verify --codegen` gates it with nothing to register; change the emit from there. Flags: - `--scope entity|package|model` — one file per object (default), per metadata package, or for the whole model - `--force` — overwrite an existing `codegen/generators/.ts` - `--no-wire` — write the file only; print the import and entry to add -A reference generator's name is refused — eject that one instead. Every port's shape, and JSON Schema / OpenAPI 3.1 examples to copy, are in the repository guide `docs/recipes/write-your-own-generator.md`. +A reference generator's name is refused — eject that one instead. Every port's shape, and worked examples to copy, are in the repository guide `docs/recipes/write-your-own-generator.md`. ### `meta types []` diff --git a/server/typescript/packages/cli/src/commands/generator-help.ts b/server/typescript/packages/cli/src/commands/generator-help.ts index c167df6257..dcd3e59351 100644 --- a/server/typescript/packages/cli/src/commands/generator-help.ts +++ b/server/typescript/packages/cli/src/commands/generator-help.ts @@ -23,14 +23,14 @@ USAGE: Writes ${OWNED_GENERATORS_DIR}/.ts — a small, commented, WORKING generator — and adds its import and entry to ${CONFIG_FILE}, so \`meta gen\` runs it and \`meta verify --codegen\` gates its output straight away. Then edit the emit to produce -what you need: OpenAPI, JSON Schema, a client, DTOs, docs — anything the model describes. +what you need: a client, DTOs, a data dictionary — anything the model describes. This is the primary way to get an output MetaObjects does not ship. Reach for \`meta eject \` instead only when a reference generator already emits something close to what you want (\`meta gen --list\` is the catalog). - is kebab-case (json-schema, openapi, service-layer). It names the file, the -generator's \`name\` in diagnostics, and the exported factory (jsonSchemaGenerator). + is kebab-case (data-dictionary, service-layer). It names the file, the +generator's \`name\` in diagnostics, and the exported factory (dataDictionaryGenerator). FLAGS: --scope What the generator walks, one file per unit (default: entity): @@ -41,7 +41,7 @@ FLAGS: --no-wire Write the file only; print the import and entry to add yourself --help, -h Print this help -Every port's shape, and JSON Schema + OpenAPI 3.1 examples to copy: the guide +Every port's shape, and worked examples to copy: the guide docs/recipes/write-your-own-generator.md in the MetaObjects repository (github.com/metaobjectsdev), and the metaobjects-codegen skill \`meta init\` installs. `; diff --git a/server/typescript/packages/cli/src/commands/generator.ts b/server/typescript/packages/cli/src/commands/generator.ts index 8c4fc89990..6e874021ad 100644 --- a/server/typescript/packages/cli/src/commands/generator.ts +++ b/server/typescript/packages/cli/src/commands/generator.ts @@ -1,8 +1,8 @@ // `meta generator new ` — scaffold a generator of YOUR OWN (ADR-0034 Amendment 4). // // MetaObjects' product is the core — the model, loader, `verify`, `migrate`, render and -// extract. Every output an application needs beyond the reference generators (OpenAPI, JSON -// Schema, a client, a DTO layer, docs) is a generator the adopter writes. This command makes +// extract. Every output an application needs beyond the reference generators (a client, a DTO +// layer, a data dictionary) is a generator the adopter writes. This command makes // the first minute of that cheap: it writes a working, commented generator into // codegen/generators/ and wires it into metaobjects.config.ts, so `meta gen` runs it and // `meta verify --codegen` gates it before the author has changed a line. diff --git a/server/typescript/packages/cli/src/commands/init.ts b/server/typescript/packages/cli/src/commands/init.ts index 06ef368fb1..0b3f7f04f8 100644 --- a/server/typescript/packages/cli/src/commands/init.ts +++ b/server/typescript/packages/cli/src/commands/init.ts @@ -142,7 +142,7 @@ export default defineConfig({ // Need an output nothing in that catalog emits — OpenAPI, JSON Schema, a client, a // service layer? Write your own generator; that is the normal path: // - // meta generator new openapi --scope model + // meta generator new data-dictionary --scope model // // writes a working, commented generator into ./codegen/generators/ and adds it here. ${docsTarget} generators: ${generators}, diff --git a/server/typescript/packages/cli/src/index.ts b/server/typescript/packages/cli/src/index.ts index 8c4dba3bc5..a294da47d0 100644 --- a/server/typescript/packages/cli/src/index.ts +++ b/server/typescript/packages/cli/src/index.ts @@ -34,7 +34,7 @@ COMMANDS: agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports gen [...] Codegen TS targets from your declared metadata generator new Write a generator of your own into codegen/generators/ and wire it — the - way to get any output MetaObjects does not ship (OpenAPI, JSON Schema, …) + way to get any output MetaObjects does not ship (Zod, a client, …) eject Copy a reference generator into codegen/generators/ to own it (any time after init) eject --list List every ejectable generator name, grouped by package eject --undo Remove owned generator copies that are the shipped reference diff --git a/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap b/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap index 7e4af6bf54..06921804fb 100644 --- a/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap +++ b/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap @@ -14,7 +14,7 @@ COMMANDS: agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports gen [...] Codegen TS targets from your declared metadata generator new Write a generator of your own into codegen/generators/ and wire it — the - way to get any output MetaObjects does not ship (OpenAPI, JSON Schema, …) + way to get any output MetaObjects does not ship (Zod, a client, …) eject Copy a reference generator into codegen/generators/ to own it (any time after init) eject --list List every ejectable generator name, grouped by package eject --undo Remove owned generator copies that are the shipped reference diff --git a/server/typescript/packages/cli/test/unit/reporting-inert.test.ts b/server/typescript/packages/cli/test/unit/reporting-inert.test.ts index 8f99963740..0294324565 100644 --- a/server/typescript/packages/cli/test/unit/reporting-inert.test.ts +++ b/server/typescript/packages/cli/test/unit/reporting-inert.test.ts @@ -24,6 +24,10 @@ // a cube only the rollup of a served one (here StoreTotals), and a selection that matches no // entity with that vocabulary gets no cube at all. // +// The contract emitters (FR-022) follow the served line in their own shapes: `contract-schema` +// adds the report's one schema file, and `openapi`, one document for the whole model, gains the +// report's one list path and its three schemas, which the test reads from the parsed document. +// // The model pair lives in fixtures/codegen-noop/reporting/ and is shared with the other // four ports' copies of this test. `with/` carries a report that declares a read-only // `source.rdb @kind: view` (R5 allows one): that is the case that once leaked in C#, where @@ -143,6 +147,7 @@ const SERVED_REPORT_FILES: Readonly> = { hooks: [`${OUT}/StoreTotals.hooks.ts`, `${OUT}/StoreTotals.meta.ts`], // The export spec for the report's list route, and the project's one shared writer module. "report-export": [`${OUT}/StoreTotals.export.ts`, `${OUT}/report-export-writers.ts`], + "contract-schema": [`${OUT}/schemas/acme/shop/StoreTotals.schema.json`], }; /** * FR-044 Plan 4: the files `cube-model` writes for `with/`, and none for `without/`. It writes @@ -171,6 +176,8 @@ const BARREL = `${OUT}/index.ts`; /** The agent UI page, which `agent-docs` writes when the run also wires the client tier. It * is one page for the whole model, so a served report adds its section to it. */ const AGENT_UI_PAGE = `${OUT}/agent/ui.md`; +/** The OpenAPI document, one for the whole model, so a served report adds to it. */ +const OPENAPI_DOC = `${OUT}/openapi.json`; /** The client UI generators that emit NOTHING for a report: a report has a list hook and no * grid, grid hook or form. */ const UI_TIER = ["grid", "grid-hook", "form"] as const; @@ -198,6 +205,26 @@ function expectOnlyAdds( } } +interface OpenApiDoc { + paths: Record; + components: { schemas: Record }; +} + +/** The OpenAPI document moves by the served report's list path and its three schemas alone: + * everything else in it is unchanged, and a sourceless report appears nowhere. */ +function expectOpenApiAddsOnlyTheServedReport(before: string, after: string): void { + const was = JSON.parse(before) as OpenApiDoc; + const now = JSON.parse(after) as OpenApiDoc; + expect(Object.keys(now.paths).filter((p) => !(p in was.paths))).toEqual(["/store_totals"]); + expect(Object.keys(now.components.schemas).filter((s) => !(s in was.components.schemas)).sort()) + .toEqual(["StoreTotals", "StoreTotalsList", "StoreTotalsPage"]); + for (const [path, item] of Object.entries(was.paths)) expect(now.paths[path]).toEqual(item); + for (const [name, schema] of Object.entries(was.components.schemas)) { + expect(now.components.schemas[name]).toEqual(schema); + } + for (const name of SOURCELESS_REPORTS) expect(after).not.toContain(name); +} + describe("FR-044 a sourceless report is inert; a served report emits exactly its read-only files", () => { test("the with-model really carries the vocabulary (else every check below is vacuous)", () => { const reports = withReporting.objects().filter((o) => o.subType === OBJECT_SUBTYPE_REPORT); @@ -229,7 +256,8 @@ describe("FR-044 a sourceless report is inert; a served report emits exactly its expect(actual).toEqual(expected); return; } - expectOnlyAdds(expected, actual, ADDED_FOR_WITH[name] ?? []); + expectOnlyAdds(expected, actual, ADDED_FOR_WITH[name] ?? [], [OPENAPI_DOC]); + if (name === "openapi") expectOpenApiAddsOnlyTheServedReport(expected[OPENAPI_DOC]!, actual[OPENAPI_DOC]!); if (name === "barrel") { // The barrel is the one shared file that moves, and it moves by the report's // export alone: every line it had is still there, in order. @@ -349,7 +377,7 @@ describe("FR-044 a sourceless report is inert; a served report emits exactly its expect(expected[""]).toBeUndefined(); expect(actual[""]).toBeUndefined(); expect(Object.keys(expected).length).toBeGreaterThan(10); - expectOnlyAdds(expected, actual, Object.values(ADDED_FOR_WITH).flat(), [AGENT_UI_PAGE]); + expectOnlyAdds(expected, actual, Object.values(ADDED_FOR_WITH).flat(), [AGENT_UI_PAGE, OPENAPI_DOC]); // The agent UI page moves by the served report's section alone: every line it had is // still there, in order, and the section names the report. const before = expected[AGENT_UI_PAGE]!.split("\n"); diff --git a/server/typescript/packages/codegen-ts/package.json b/server/typescript/packages/codegen-ts/package.json index cb52ec1a11..c793c6de09 100644 --- a/server/typescript/packages/codegen-ts/package.json +++ b/server/typescript/packages/codegen-ts/package.json @@ -112,7 +112,8 @@ "scripts": { "build": "tsc -p .", "typecheck": "tsc -p tsconfig.typecheck.json", - "gen:cube-canonical": "bun scripts/gen-cube-model-canonical.ts" + "gen:cube-canonical": "bun scripts/gen-cube-model-canonical.ts", + "gen:contract-conformance": "bun scripts/gen-contract-conformance.ts" }, "license": "Apache-2.0", "author": "Doug Mealing ", @@ -154,6 +155,9 @@ "@biomejs/biome": "^1.9.0", "@metaobjectsdev/codegen-ts-react": "workspace:*", "@metaobjectsdev/runtime-ts": "workspace:*", + "@seriousme/openapi-schema-validator": "^2.11.0", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "bun-types": "latest", "drizzle-orm": "^0.38.0", "hono": "^4.6.0", diff --git a/server/typescript/packages/codegen-ts/scripts/gen-contract-conformance.ts b/server/typescript/packages/codegen-ts/scripts/gen-contract-conformance.ts new file mode 100644 index 0000000000..3bdd0e4e96 --- /dev/null +++ b/server/typescript/packages/codegen-ts/scripts/gen-contract-conformance.ts @@ -0,0 +1,138 @@ +// gen-contract-conformance.ts — (re)write the contract emitters' golden trees (FR-022). +// +// Run: `bun run gen:contract-conformance` (from server/typescript/packages/codegen-ts). Pure +// metadata to JSON, no database. Each case under fixtures/contract-conformance/ runs jsonSchemaFile() +// in both profiles and openApiFile() through runGen, and its `expected/` is replaced with what they +// write. test/contract/contract-corpus.test.ts regenerates in a temp directory and fails on any +// difference. An `error-*` case's `expected-error.txt` is written by hand. +// +// Also exports the harness the corpus test uses. + +import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { loadUris, type MetaObject, type MetaRoot, type WireFormat, wireFormatOf } from "@metaobjectsdev/metadata"; +import { defineConfig, type Generator, runGen } from "../src/index.js"; +import { jsonSchemaFile, openApiFile } from "../src/generators/index.js"; +import { readTree } from "./gen-cube-model-canonical.js"; + +// scripts → codegen-ts → packages → typescript → server → repo root +const REPO_ROOT = resolve(import.meta.dir, "..", "..", "..", "..", ".."); + +export const CONTRACT_CORPUS_DIR = join(REPO_ROOT, "fixtures", "contract-conformance"); + +/** This script, as a failing test names it. */ +export const CONTRACT_SCRIPT = "server/typescript/packages/codegen-ts/scripts/gen-contract-conformance.ts (bun run gen:contract-conformance)"; + +/** Every case serves under this prefix, the one the api-contract corpus requests. */ +const API_PREFIX = "/api"; + +export interface ContractCase { + readonly name: string; + readonly dir: string; + /** The metadata file: the case's own `meta.json`, or a shared one `case.json` names. */ + readonly meta: string; + readonly wire?: Partial; + /** Objects the strict profile leaves out, because the strict subset cannot state them. */ + readonly strictExclude: readonly string[]; + /** The project wires `report-export`, so a report's list route takes `format`. */ + readonly reportExport: boolean; +} + +interface CaseJson { + /** Repository-relative metadata file. Default: `meta.json` beside it. */ + meta?: string; + wire?: Partial; + strictExclude?: string[]; + reportExport?: boolean; +} + +function readCase(name: string): ContractCase { + const dir = join(CONTRACT_CORPUS_DIR, name); + const path = join(dir, "case.json"); + const json = existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as CaseJson) : {}; + return { + name, + dir, + meta: json.meta === undefined ? join(dir, "meta.json") : join(REPO_ROOT, json.meta), + ...(json.wire !== undefined && { wire: json.wire }), + strictExclude: json.strictExclude ?? [], + reportExport: json.reportExport ?? false, + }; +} + +/** Every case, in name order. */ +export function contractCases(): ContractCase[] { + return readdirSync(CONTRACT_CORPUS_DIR) + .filter((name) => statSync(join(CONTRACT_CORPUS_DIR, name)).isDirectory()) + .sort() + .map(readCase); +} + +export async function loadCaseModel(c: ContractCase): Promise { + const result = await loadUris([pathToFileURL(c.meta).href]); + if (result.errors.length > 0) { + throw new Error(`${c.meta}: metadata did not load cleanly: ${result.errors.map((e) => e.message).join("; ")}`); + } + if (c.wire !== undefined) result.root.wireFormat = { ...wireFormatOf(result.root), ...c.wire }; + return result.root; +} + +/** The generators a case runs. A caller passes its own factories to run the reference copies. */ +export interface ContractFactories { + readonly jsonSchemaFile: typeof jsonSchemaFile; + readonly openApiFile: typeof openApiFile; +} + +/** What marks `report-export` for the runner. The generator's own files are not this corpus's subject. */ +const REPORT_EXPORT_MARKER: Generator = { name: "report-export", emitsReportExport: true, generate: () => [] }; + +function caseGenerators(c: ContractCase, factories: ContractFactories): Generator[] { + const inStrict = (obj: MetaObject): boolean => !c.strictExclude.includes(obj.name); + return [ + factories.jsonSchemaFile(), + factories.jsonSchemaFile({ profile: "strict", filter: inStrict }), + factories.openApiFile({ title: c.name, version: "1.0.0" }), + ...(c.reportExport ? [REPORT_EXPORT_MARKER] : []), + ]; +} + +/** Run a case's generators into a fresh temp directory and read back what they wrote. */ +export async function contractTree(c: ContractCase, factories: ContractFactories = { jsonSchemaFile, openApiFile }): Promise> { + const root = await loadCaseModel(c); + const dir = mkdtempSync(join(tmpdir(), "contract-conformance-")); + try { + const outDir = join(dir, "out"); + await runGen({ + config: defineConfig({ outDir, dialect: "postgres", apiPrefix: API_PREFIX, generators: caseGenerators(c, factories) }), + metadata: root, + genStateDir: join(dir, ".gen-state"), + }); + return readTree(outDir); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +async function main(): Promise { + for (const c of contractCases()) { + if (c.name.startsWith("error-")) continue; + const fresh = mkdtempSync(join(tmpdir(), "contract-expected-")); + try { + for (const [path, content] of await contractTree(c)) { + const full = join(fresh, path); + mkdirSync(join(full, ".."), { recursive: true }); + await Bun.write(full, content); + } + const expected = join(c.dir, "expected"); + rmSync(expected, { recursive: true, force: true }); + cpSync(fresh, expected, { recursive: true }); + console.log(`${c.name}: wrote ${readdirSync(fresh, { recursive: true }).length} entries`); + } finally { + rmSync(fresh, { recursive: true, force: true }); + } + } +} + +if (import.meta.main) await main(); diff --git a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts new file mode 100644 index 0000000000..ded2866d30 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts @@ -0,0 +1,401 @@ +// FR-022 — the OpenAPI 3.1 document of the generated REST surface: the contract in +// docs/features/api-contract.md, which every port's routes answer, narrowed to the verbs a project +// mounts. `components.schemas` is the canonical JSON Schema (an OpenAPI 3.1 schema IS JSON Schema +// 2020-12), so the two artifacts cannot disagree. +// +// It states the cross-port contract only. The TS-only filter extensions (`?search`, `filter[or]`) +// are left out, and so are the `405` refusals on a read-only surface, which are not operations a +// caller can use (the api docs omit them for the same reason). + +import { type MetaField, type MetaObject, type MetaRoot, opsForField } from "@metaobjectsdev/metadata"; +import { FIELD_ATTR_FILTERABLE, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_TIMESTAMP, FILTER_OP_IN, FILTER_OP_IS_NULL, FILTER_OP_LIKE } from "@metaobjectsdev/metadata/constants"; +import { hasItemRoute, itemRouteField, servedPath, servesReadApi } from "../api-surface.js"; +import { enumValues } from "../enum-meta.js"; +import { isProjection } from "../projection/projection-detector.js"; +import type { RelationMap } from "../relation-resolver.js"; +import { CRUD_VERBS, type CrudVerb, type ExposeOption, intersectExpose, resolveExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js"; +import { isReport } from "../source-detect.js"; +import { sortableFields } from "../templates/filter-shared.js"; +import { tphPlan } from "../templates/tph-discriminator.js"; +import { isTphSubtype } from "../tph.js"; +import { ContractEmitError, ERR_CONTRACT_NAME_COLLISION } from "./contract-errors.js"; +import { type ContractShape, DECIMAL_INPUT_PATTERN, type JsonSchema, scalarSchema, timestampInputSchema } from "./field-schema.js"; +import { collectContractEntries } from "./object-schema.js"; + +export const OPENAPI_VERSION = "3.1.0"; + +/** The error envelope's component. A collision with a real object is refused like any other. */ +const ERROR_COMPONENT = "ApiError"; + +const SHAPE_SUFFIX: Record = { read: "", create: "Create", update: "Update" }; + +const SORT_DIRECTIONS = ["asc", "desc"] as const; + +export interface OpenApiDocumentOptions { + /** `info.title`. */ + readonly title?: string; + /** `info.version`: the version of YOUR API, not of MetaObjects. */ + readonly version?: string; + /** `info.description`. */ + readonly description?: string; + /** `servers`, as OpenAPI spells it. A deployment fact: metadata does not hold it. */ + readonly servers?: readonly Record[]; + /** `components.securitySchemes`, as OpenAPI spells it. Authentication is the application's. */ + readonly securitySchemes?: Record; + /** The top-level `security` requirement, as OpenAPI spells it. */ + readonly security?: readonly Record[]; +} + +export interface OpenApiBuildInput extends OpenApiDocumentOptions { + readonly root: MetaRoot; + /** The objects the document covers. */ + readonly objects: readonly MetaObject[]; + readonly apiPrefix: string; + readonly relationMap: RelationMap; + /** The verbs the routes generators mount. Undefined mounts every verb. */ + readonly expose?: ExposeOption; + /** Whether an object has routes at all, when a routes generator's `filter` narrows them. */ + readonly mounted?: (obj: MetaObject) => boolean; + /** Whether `report-export` is wired, which adds `?format=` to a served report. */ + readonly reportExport: boolean; +} + +function byName(a: MetaObject, b: MetaObject): number { + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; +} + +function componentName(obj: MetaObject, shape: ContractShape): string { + return `${obj.name}${SHAPE_SUFFIX[shape]}`; +} + +function componentRef(name: string): string { + return `#/components/schemas/${name}`; +} + +function shapeRef(obj: MetaObject, shape: ContractShape): JsonSchema { + return { $ref: componentRef(componentName(obj, shape)) }; +} + +function jsonContent(schema: JsonSchema): JsonSchema { + return { "application/json": { schema } }; +} + +function errorResponse(description: string): JsonSchema { + return { description, content: jsonContent({ $ref: componentRef(ERROR_COMPONENT) }) }; +} + +const BAD_REQUEST = errorResponse("The request was refused."); +const NOT_FOUND = errorResponse("No such row."); +const CONFLICT = errorResponse("A constraint refused the write."); + +/** The value of one filter operator, in the query string. */ +function filterValueSchema(field: MetaField, op: string): JsonSchema { + if (op === FILTER_OP_IS_NULL) return { type: "boolean" }; + if (op === FILTER_OP_IN) return { type: "string", description: "Comma-separated values." }; + if (op === FILTER_OP_LIKE) return { type: "string" }; + if (field.subType === FIELD_SUBTYPE_DECIMAL) return { type: "string", pattern: DECIMAL_INPUT_PATTERN }; + if (field.subType === FIELD_SUBTYPE_TIMESTAMP) return timestampInputSchema(field); + if (field.subType === FIELD_SUBTYPE_ENUM) return { type: "string", enum: enumValues(field) ?? [] }; + return scalarSchema(field.subType); +} + +/** One parameter per filterable field and operator, so removing either is a visible change. */ +function filterParameters(obj: MetaObject, excludeField: string | undefined): JsonSchema[] { + const params: JsonSchema[] = []; + const filterable = obj.fields().filter((f) => f.attr(FIELD_ATTR_FILTERABLE) === true && f.name !== excludeField); + for (const field of filterable) { + const ops = opsForField(field); + if (ops.length === 0) continue; + params.push({ name: `filter[${field.name}]`, in: "query", description: "Sugar for the eq operator.", schema: filterValueSchema(field, ops[0] ?? "") }); + for (const op of ops) { + params.push({ name: `filter[${field.name}][${op}]`, in: "query", schema: filterValueSchema(field, op) }); + } + } + return params; +} + +function sortParameter(obj: MetaObject, excludeField: string | undefined): JsonSchema[] { + const fields = sortableFields(obj).filter((f) => f.name !== excludeField); + if (fields.length === 0) return []; + const values = fields.flatMap((f) => [f.name, ...SORT_DIRECTIONS.map((d) => `${f.name}:${d}`)]); + return [{ name: "sort", in: "query", schema: { type: "string", enum: values } }]; +} + +function pageParameters(minLimit: number): JsonSchema[] { + return [ + { name: "limit", in: "query", schema: { type: "integer", minimum: minLimit } }, + { name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }, + { name: "withCount", in: "query", description: "Answer { rows, total } instead of the array.", schema: { type: "integer", enum: [1] } }, + ]; +} + +function listComponent(row: MetaObject): string { + return `${row.name}List`; +} + +function pageComponent(row: MetaObject): string { + return `${row.name}Page`; +} + +/** + * A list answers an array, or `{ rows, total }` under `withCount=1`. Each is a named component + * because a diff tool pairs a `oneOf` by name: inline, adding a field to the row reads as two + * subschemas replaced, and oasdiff reports a compatible change as a breaking one. + */ +function listSchema(row: MetaObject): JsonSchema { + return { oneOf: [{ $ref: componentRef(listComponent(row)) }, { $ref: componentRef(pageComponent(row)) }] }; +} + +function listComponents(row: MetaObject): Record { + const rows = { type: "array", items: shapeRef(row, "read") }; + return { + [listComponent(row)]: { title: listComponent(row), ...rows }, + [pageComponent(row)]: { + title: pageComponent(row), + type: "object", + required: ["rows", "total"], + properties: { rows, total: { type: "integer" } }, + }, + }; +} + +function idParameter(obj: MetaObject): JsonSchema { + const name = itemRouteField(obj); + const field = obj.fields().find((f) => f.name === name); + return { name: "id", in: "path", required: true, schema: field === undefined ? { type: "string" } : scalarSchema(field.subType) }; +} + +/** The operations of one resource: its collection path and, when it has rows by key, its item path. */ +interface ResourceOps { + /** The object whose rows the routes answer, and whose name the operations carry. */ + readonly row: MetaObject; + readonly path: string; + readonly verbs: ReadonlySet; + /** Filtering and sorting on this field is refused: a per-subtype mount pins the discriminator. */ + readonly excludeField?: string; + readonly report: boolean; + readonly reportExport: boolean; + readonly item: boolean; + /** Collects each object a list route answers, for its list components. */ + readonly listed: Set; +} + +function listOperation(r: ResourceOps): JsonSchema { + const { row } = r; + const parameters = [ + ...filterParameters(row, r.excludeField), + ...sortParameter(row, r.excludeField), + ...pageParameters(r.report ? 1 : 0), + ...(r.reportExport ? [{ name: "format", in: "query", schema: { type: "string", enum: ["csv", "json", "xml"] } }] : []), + ]; + r.listed.add(row); + const content = jsonContent(listSchema(row)); + const exports = r.reportExport ? { "text/csv": { schema: { type: "string" } }, "application/xml": { schema: { type: "string" } } } : {}; + return { + operationId: `list${row.name}`, + tags: [row.name], + parameters, + responses: { "200": { description: "The rows.", content: { ...content, ...exports } }, "400": BAD_REQUEST }, + }; +} + +function writeBody(row: MetaObject, shape: ContractShape): JsonSchema { + return { required: true, content: jsonContent(shapeRef(row, shape)) }; +} + +function updateOperation(row: MetaObject, operationId: string): JsonSchema { + return { + operationId, + tags: [row.name], + requestBody: writeBody(row, "update"), + responses: { + "200": { description: "The row.", content: jsonContent(shapeRef(row, "read")) }, + "400": BAD_REQUEST, + "404": NOT_FOUND, + "409": CONFLICT, + }, + }; +} + +function itemOperations(r: ResourceOps): JsonSchema { + const { row, verbs } = r; + const tags = [row.name]; + return { + ...(verbs.has("get") && { + get: { + operationId: `get${row.name}`, + tags, + responses: { "200": { description: "The row.", content: jsonContent(shapeRef(row, "read")) }, "404": NOT_FOUND }, + }, + }), + ...(verbs.has("update") && { + patch: updateOperation(row, `update${row.name}`), + // The contract's PUT is an alias of PATCH: a partial update, not a replacement. + put: updateOperation(row, `put${row.name}`), + }), + ...(verbs.has("delete") && { + delete: { + operationId: `delete${row.name}`, + tags, + responses: { "204": { description: "Deleted." }, "404": NOT_FOUND, "409": CONFLICT }, + }, + }), + }; +} + +function collectionOperations(r: ResourceOps): JsonSchema { + const { row, verbs } = r; + return { + ...(verbs.has("list") && { get: listOperation(r) }), + ...(verbs.has("create") && { + post: { + operationId: `create${row.name}`, + tags: [row.name], + requestBody: writeBody(row, "create"), + responses: { + "201": { description: "The new row.", content: jsonContent(shapeRef(row, "read")) }, + "400": BAD_REQUEST, + "409": CONFLICT, + }, + }, + }), + }; +} + +function resourcePaths(r: ResourceOps): Record { + const paths: Record = {}; + const collection = collectionOperations(r); + if (Object.keys(collection).length > 0) paths[r.path] = collection; + const item = r.item ? itemOperations(r) : {}; + if (Object.keys(item).length > 0) paths[`${r.path}/{id}`] = { parameters: [idParameter(r.row)], ...item }; + return paths; +} + +/** `GET /{id}/`: the M:N traversal, an array of the target's rows. */ +function m2mPaths(source: MetaObject, path: string, relationMap: RelationMap): Record { + const paths: Record = {}; + for (const entry of relationMap.get(source.name) ?? []) { + if (entry.junctionEntity === undefined) continue; + paths[`${path}/{id}/${entry.name}`] = { + parameters: [idParameter(source)], + get: { + operationId: `list${source.name}${entry.name.charAt(0).toUpperCase()}${entry.name.slice(1)}`, + tags: [source.name], + responses: { + "200": { description: "The related rows.", content: jsonContent({ type: "array", items: shapeRef(entry.target, "read") }) }, + }, + }, + }; + } + return paths; +} + +function verbsOf(fixed: readonly CrudVerb[], obj: MetaObject, expose: ExposeOption | undefined): ReadonlySet { + return new Set(intersectExpose([...fixed], resolveExpose(obj, expose))); +} + +function pathsOf( + obj: MetaObject, + input: OpenApiBuildInput, + reportNames: ReadonlySet, + listed: Set, +): Record { + const { root, apiPrefix, expose, relationMap } = input; + const path = servedPath(obj, apiPrefix); + const report = reportNames.has(obj.name); + if (isProjection(obj)) { + return resourcePaths({ + row: obj, path, report, reportExport: report && input.reportExport, item: hasItemRoute(obj), listed, + verbs: new Set(["list", "get"]), + }); + } + const plan = tphPlan(obj, root); + if (plan === null) { + return { + ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, listed, verbs: verbsOf(CRUD_VERBS, obj, expose) }), + ...m2mPaths(obj, path, relationMap), + }; + } + const paths: Record = { + ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, listed, verbs: verbsOf(TPH_POLYMORPHIC_VERBS, obj, expose) }), + ...m2mPaths(obj, path, relationMap), + }; + for (const { entity, routeSegment } of plan.subtypes) { + const subPath = `${path}/${routeSegment}`; + Object.assign(paths, resourcePaths({ + row: entity, path: subPath, report: false, reportExport: false, item: true, listed, + verbs: verbsOf(CRUD_VERBS, obj, expose), excludeField: plan.discriminatorField, + }), m2mPaths(entity, subPath, relationMap)); + } + return paths; +} + +function collectSchemas(input: OpenApiBuildInput, listed: ReadonlySet): Record { + const taken = new Map([[ERROR_COMPONENT, "the error envelope"]]); + const schemas: Record = {}; + function claim(name: string, holder: string): void { + const other = taken.get(name); + if (other !== undefined) { + throw new ContractEmitError( + ERR_CONTRACT_NAME_COLLISION, + `the OpenAPI component '${name}' is both ${other} and ${holder}. Rename one, or leave one out with the generator's filter option.`, + ); + } + taken.set(name, holder); + } + const entries = collectContractEntries( + input.objects, + input.root, + (_from, target, shape) => componentRef(componentName(target, shape)), + { openApiDiscriminator: true }, + ); + for (const { obj, shape, schema } of entries) { + const name = componentName(obj, shape); + claim(name, `'${obj.resolutionKey()}' (${shape} shape)`); + schemas[name] = { title: name, ...schema }; + } + for (const row of listed) { + for (const [name, schema] of Object.entries(listComponents(row))) { + claim(name, `the list response of '${row.resolutionKey()}'`); + schemas[name] = schema; + } + } + schemas[ERROR_COMPONENT] = { + title: ERROR_COMPONENT, + description: "The error envelope. `error` is the stable code; `message` is free prose.", + type: "object", + required: ["error"], + properties: { + error: { type: "string" }, + message: { type: "string" }, + field: { type: "string", description: "The rejected filter or sort field, on the four codes that name one." }, + }, + }; + return Object.fromEntries(Object.entries(schemas).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))); +} + +/** The whole document, in the key order it is written. */ +export function buildOpenApiDocument(input: OpenApiBuildInput): JsonSchema { + const reportNames = new Set(input.root.objects().filter(isReport).map((o) => o.name)); + const objects = [...input.objects].sort(byName); + const served = objects.filter((o) => servesReadApi(o) && !isTphSubtype(o) && (input.mounted?.(o) ?? true)); + const paths: Record = {}; + const listed = new Set(); + for (const obj of served) Object.assign(paths, pathsOf(obj, input, reportNames, listed)); + return { + openapi: OPENAPI_VERSION, + info: { + title: input.title ?? "API", + version: input.version ?? "0.0.0", + ...(input.description !== undefined && { description: input.description }), + }, + ...(input.servers !== undefined && { servers: input.servers }), + ...(input.security !== undefined && { security: input.security }), + paths, + components: { + schemas: collectSchemas({ ...input, objects }, listed), + ...(input.securitySchemes !== undefined && { securitySchemes: input.securitySchemes }), + }, + }; +} + diff --git a/server/typescript/packages/codegen-ts/src/contract/contract-errors.ts b/server/typescript/packages/codegen-ts/src/contract/contract-errors.ts new file mode 100644 index 0000000000..97266b31e0 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/contract/contract-errors.ts @@ -0,0 +1,23 @@ +// FR-022 — the contract emitters' own errors. These are generator errors, printed in the message; +// they are not loader codes and are not listed in fixtures/conformance/ERROR-CODES.json. + +/** Two objects would take the same OpenAPI component name. */ +export const ERR_CONTRACT_NAME_COLLISION = "ERR_CONTRACT_NAME_COLLISION"; +/** A strict schema was asked for a shape the strict subset cannot state (a map, an open object, a cycle). */ +export const ERR_CONTRACT_STRICT_UNREPRESENTABLE = "ERR_CONTRACT_STRICT_UNREPRESENTABLE"; + +export const CONTRACT_ERROR_CODES = [ + ERR_CONTRACT_NAME_COLLISION, + ERR_CONTRACT_STRICT_UNREPRESENTABLE, +] as const; +export type ContractErrorCode = (typeof CONTRACT_ERROR_CODES)[number]; + +export class ContractEmitError extends Error { + readonly code: ContractErrorCode; + + constructor(code: ContractErrorCode, message: string) { + super(`[${code}] ${message}`); + this.name = "ContractEmitError"; + this.code = code; + } +} diff --git a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts new file mode 100644 index 0000000000..06fb2ec011 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts @@ -0,0 +1,300 @@ +// FR-022 — one field as JSON Schema 2020-12. Pure: it reads the field and returns a plain value. +// +// The encodings are the REST contract's, not the persistence corpus's comparator form +// (docs/features/api-contract.md, "Type encodings"): a long is a JSON number, a currency is integer +// minor units, a time has no offset, and a @localTime timestamp has no `Z`. A decimal is the one +// encoding that is a project setting, so it reads `wire.decimals` and never the port, which is what +// keeps these bytes neutral. +// +// The constraints mirror what the generated write validators enforce (`appendValidatorChain`): +// the same bounds on the same field shapes, so a schema neither promises more nor refuses more. + +import { + type MetaField, + type MetaObject, + WIRE_DECIMALS_STRING, + wireFormatOf, +} from "@metaobjectsdev/metadata"; +import { + DB_COLUMN_TYPE_JSONB, + DOC_ATTR_DESCRIPTION, + FIELD_ATTR_CURRENCY, + FIELD_ATTR_DB_COLUMN_TYPE, + FIELD_ATTR_LENIENT, + FIELD_ATTR_LOCAL_TIME, + FIELD_ATTR_MAX_LENGTH, + FIELD_ATTR_OBJECT_REF, + FIELD_ATTR_PRECISION, + FIELD_ATTR_SCALE, + FIELD_ATTR_STRING_FORMAT, + FIELD_ATTR_VALUE_TYPE, + FIELD_SUBTYPE_BOOLEAN, + FIELD_SUBTYPE_CURRENCY, + FIELD_SUBTYPE_DATE, + FIELD_SUBTYPE_DECIMAL, + FIELD_SUBTYPE_DOUBLE, + FIELD_SUBTYPE_ENUM, + FIELD_SUBTYPE_FLOAT, + FIELD_SUBTYPE_INET, + FIELD_SUBTYPE_INT, + FIELD_SUBTYPE_LONG, + FIELD_SUBTYPE_MAP, + FIELD_SUBTYPE_OBJECT, + FIELD_SUBTYPE_STRING, + FIELD_SUBTYPE_TIME, + FIELD_SUBTYPE_TIMESTAMP, + FIELD_SUBTYPE_URI, + FIELD_SUBTYPE_UUID, + STRING_FORMAT_EMAIL, + STRING_FORMAT_HOSTNAME, + VALIDATOR_ATTR_MAX, + VALIDATOR_ATTR_MIN, + VALIDATOR_ATTR_PATTERN, + VALIDATOR_SUBTYPE_ARRAY, + VALIDATOR_SUBTYPE_LENGTH, + VALIDATOR_SUBTYPE_NUMERIC, + VALIDATOR_SUBTYPE_REGEX, +} from "@metaobjectsdev/metadata/constants"; +import { enumValues } from "../enum-meta.js"; +import { objectRefTarget } from "../model-walk.js"; + +/** A JSON Schema, as a plain object in the key order it is written. */ +export type JsonSchema = { [key: string]: unknown }; + +/** + * The three wire shapes of an object: what a read answers, what POST takes and what PATCH takes. + * They differ (a generated id is not sent, a credential is not returned, a PATCH requires nothing), + * so one schema per object cannot state all three. + */ +export type ContractShape = "read" | "create" | "update"; + +/** How a schema points at another object's schema: a relative file, or a component. */ +export type RefFor = (target: MetaObject, shape: ContractShape) => string; + +/** The canonical decimal string (ADR-0019 Amendment 1): plain notation, no trailing fraction zeros. */ +export const DECIMAL_CANONICAL_PATTERN = "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$"; +/** A decimal as a request may send it: a string in JSON-number grammar. */ +export const DECIMAL_INPUT_PATTERN = "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$"; +/** `HH:MM:SS[.fff]`. The RFC 3339 `time` format demands an offset, which this wire form does not carry. */ +export const TIME_PATTERN = "^[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$"; +/** A @localTime timestamp: a naive wall clock, which `date-time` refuses for having no offset. */ +export const LOCAL_TIMESTAMP_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?$"; + +/** + * An instant as a request may send it. Every port's routes take a value with no offset and read it + * as UTC (the api-contract corpus sends one), which `date-time` refuses, so a write states what a + * route takes and a read states what it answers. + */ +export const TIMESTAMP_INPUT_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$"; + +const NUMERIC_SUBTYPES: ReadonlySet = new Set([ + FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, +]); + +function isJsonbBag(field: MetaField): boolean { + return field.subType === FIELD_SUBTYPE_STRING && field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB; +} + +function decimalPrecisionNote(field: MetaField): string { + const precision = field.attr(FIELD_ATTR_PRECISION); + const scale = field.attr(FIELD_ATTR_SCALE); + if (typeof precision !== "number" || typeof scale !== "number") return ""; + return ` Precision ${String(precision)}, scale ${String(scale)}.`; +} + +/** A decimal. An answer follows `wire.decimals`; a request takes either form in every mode. */ +function decimalSchema(field: MetaField, shape: ContractShape): JsonSchema { + const note = decimalPrecisionNote(field); + if (shape !== "read") { + return { + type: ["string", "number"], + pattern: DECIMAL_INPUT_PATTERN, + description: `A decimal, as a string in JSON-number grammar or a JSON number.${note}`, + }; + } + if (wireFormatOf(field).decimals === WIRE_DECIMALS_STRING) { + return { type: "string", pattern: DECIMAL_CANONICAL_PATTERN }; + } + return { + type: ["string", "number"], + description: `A decimal. Set wire.decimals to "string" to fix one form across server languages.${note}`, + }; +} + +/** A timestamp as a request or a filter operand sends it. */ +export function timestampInputSchema(field: MetaField): JsonSchema { + if (field.attr(FIELD_ATTR_LOCAL_TIME) === true) return { type: "string", pattern: LOCAL_TIMESTAMP_PATTERN }; + return { type: "string", pattern: TIMESTAMP_INPUT_PATTERN, description: "An instant. A value with no offset is read as UTC." }; +} + +/** A scalar field type by subtype name, with no constraints. A map's `@valueType` reads this too. */ +export function scalarSchema(subType: string): JsonSchema { + switch (subType) { + case FIELD_SUBTYPE_INT: return { type: "integer", format: "int32" }; + case FIELD_SUBTYPE_LONG: + case FIELD_SUBTYPE_CURRENCY: return { type: "integer", format: "int64" }; + case FIELD_SUBTYPE_DOUBLE: return { type: "number", format: "double" }; + case FIELD_SUBTYPE_FLOAT: return { type: "number", format: "float" }; + case FIELD_SUBTYPE_BOOLEAN: return { type: "boolean" }; + case FIELD_SUBTYPE_DATE: return { type: "string", format: "date" }; + case FIELD_SUBTYPE_TIME: return { type: "string", pattern: TIME_PATTERN }; + case FIELD_SUBTYPE_TIMESTAMP: return { type: "string", format: "date-time" }; + case FIELD_SUBTYPE_UUID: return { type: "string", format: "uuid" }; + case FIELD_SUBTYPE_URI: return { type: "string", format: "uri" }; + case FIELD_SUBTYPE_INET: return { type: "string", anyOf: [{ format: "ipv4" }, { format: "ipv6" }] }; + case FIELD_SUBTYPE_DECIMAL: return { type: "string", pattern: DECIMAL_INPUT_PATTERN }; + default: return { type: "string" }; + } +} + +/** One value of a field that holds no reference to another object. */ +function plainValueSchema(field: MetaField, shape: ContractShape): JsonSchema { + const lenient = field.attr(FIELD_ATTR_LENIENT) === true; + switch (field.subType) { + case FIELD_SUBTYPE_DECIMAL: + return decimalSchema(field, shape); + case FIELD_SUBTYPE_TIMESTAMP: + if (shape !== "read") return timestampInputSchema(field); + return field.attr(FIELD_ATTR_LOCAL_TIME) === true + ? { type: "string", pattern: LOCAL_TIMESTAMP_PATTERN } + : scalarSchema(field.subType); + case FIELD_SUBTYPE_URI: + case FIELD_SUBTYPE_INET: + return lenient ? { type: "string" } : scalarSchema(field.subType); + case FIELD_SUBTYPE_ENUM: + return { type: "string", enum: enumValues(field) ?? [] }; + case FIELD_SUBTYPE_CURRENCY: { + const code = field.attr(FIELD_ATTR_CURRENCY); + const unit = typeof code === "string" ? ` of ${code}` : ""; + return { ...scalarSchema(field.subType), description: `Integer minor units${unit}.` }; + } + case FIELD_SUBTYPE_STRING: { + if (isJsonbBag(field)) return {}; + const format = field.attr(FIELD_ATTR_STRING_FORMAT); + if (format === STRING_FORMAT_EMAIL) return { type: "string", format: "email" }; + if (format === STRING_FORMAT_HOSTNAME) return { type: "string", format: "hostname" }; + return { type: "string" }; + } + default: + return scalarSchema(field.subType); + } +} + +/** A nested shape is the matching shape of the target. A write takes the target's create shape. */ +function nestedShape(shape: ContractShape): ContractShape { + return shape === "read" ? "read" : "create"; +} + +function objectValueSchema(field: MetaField, shape: ContractShape, refFor: RefFor): JsonSchema { + const target = objectRefTarget(field); + return target === undefined ? { type: "object" } : { $ref: refFor(target, nestedShape(shape)) }; +} + +function mapValueSchema(field: MetaField, shape: ContractShape, refFor: RefFor): JsonSchema { + const ref = field.attr(FIELD_ATTR_OBJECT_REF); + const target = typeof ref === "string" && ref.length > 0 ? objectRefTarget(field) : undefined; + if (target !== undefined) { + return { type: "object", additionalProperties: { $ref: refFor(target, nestedShape(shape)) } }; + } + const valueType = field.attr(FIELD_ATTR_VALUE_TYPE); + return { type: "object", additionalProperties: scalarSchema(typeof valueType === "string" ? valueType : FIELD_SUBTYPE_STRING) }; +} + +interface FieldBounds { + minLength?: number; + maxLength?: number; + pattern?: string; + minimum?: number; + maximum?: number; + minItems?: number; + maxItems?: number; +} + +/** The bounds a field's validators state, folded as `appendValidatorChain` folds them. */ +function boundsOf(field: MetaField): FieldBounds { + const bounds: FieldBounds = {}; + const maxLength = field.attr(FIELD_ATTR_MAX_LENGTH); + if (typeof maxLength === "number") bounds.maxLength = maxLength; + // ADR-0039: resolving — a validator may inherit @min/@max/@pattern via extends. + for (const v of field.validators()) { + const min = v.attr(VALIDATOR_ATTR_MIN); + const max = v.attr(VALIDATOR_ATTR_MAX); + if (v.subType === VALIDATOR_SUBTYPE_LENGTH) { + if (typeof min === "number") bounds.minLength = min; + // FR-036 A3: @maxLength × validator.length @max is strictest-wins. + if (typeof max === "number") bounds.maxLength = bounds.maxLength === undefined ? max : Math.min(bounds.maxLength, max); + } else if (v.subType === VALIDATOR_SUBTYPE_REGEX) { + const pattern = v.attr(VALIDATOR_ATTR_PATTERN); + // FR-036 Pin 2: @pattern is full-match, and JSON Schema's `pattern` searches. + if (typeof pattern === "string") bounds.pattern = `^(?:${pattern})$`; + } else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) { + if (typeof min === "number") bounds.minimum = min; + if (typeof max === "number") bounds.maximum = max; + } else if (v.subType === VALIDATOR_SUBTYPE_ARRAY) { + if (typeof min === "number") bounds.minItems = min; + if (typeof max === "number") bounds.maxItems = max; + } + } + return bounds; +} + +/** Bounds on the one value of a scalar: characters of a string, the value of a number. */ +function applyScalarBounds(schema: JsonSchema, field: MetaField, shape: ContractShape, bounds: FieldBounds): void { + if (field.subType === FIELD_SUBTYPE_STRING && !isJsonbBag(field)) { + // FR-036 Pin 1: a @required string is non-empty on a write, unless an authored @min says otherwise. + const floor = shape !== "read" && field.isRequired ? 1 : 0; + const minLength = bounds.minLength ?? floor; + if (minLength > 0) schema.minLength = minLength; + if (bounds.maxLength !== undefined) schema.maxLength = bounds.maxLength; + if (bounds.pattern !== undefined) schema.pattern = bounds.pattern; + } else if (NUMERIC_SUBTYPES.has(field.subType)) { + if (bounds.minimum !== undefined) schema.minimum = bounds.minimum; + if (bounds.maximum !== undefined) schema.maximum = bounds.maximum; + } +} + +function joinDescription(first: unknown, second: unknown): string | undefined { + const parts = [first, second].filter((p): p is string => typeof p === "string" && p.length > 0); + return parts.length === 0 ? undefined : parts.join(" "); +} + +/** The JSON Schema of a field's value in one shape. Null is the caller's to add: see `withNull`. */ +export function fieldSchema(field: MetaField, shape: ContractShape, refFor: RefFor): JsonSchema { + const value: JsonSchema = field.subType === FIELD_SUBTYPE_OBJECT + ? objectValueSchema(field, shape, refFor) + : field.subType === FIELD_SUBTYPE_MAP + ? mapValueSchema(field, shape, refFor) + : plainValueSchema(field, shape); + + const bounds = boundsOf(field); + let schema = value; + // resolvedIsArray(), never the raw `isArray` flag: array-ness inherited through `extends` lives + // on the parent field. An array's bounds count elements (`appendValidatorChain`). + if (field.resolvedIsArray()) { + schema = { type: "array", items: value }; + if (bounds.minItems !== undefined) schema.minItems = bounds.minItems; + if (bounds.maxItems !== undefined) schema.maxItems = bounds.maxItems; + } else { + applyScalarBounds(schema, field, shape, bounds); + } + + const description = joinDescription(field.attr(DOC_ATTR_DESCRIPTION), schema.description); + if (description !== undefined) schema.description = description; + return schema; +} + +/** The schema that also accepts `null`. */ +export function withNull(schema: JsonSchema): JsonSchema { + if (Object.keys(schema).length === 0) return schema; // an open value already holds null + if (schema.$ref !== undefined) { + const { description, ...ref } = schema; + return { anyOf: [ref, { type: "null" }], ...(description !== undefined && { description }) }; + } + const out: JsonSchema = { ...schema }; + const type = schema.type; + if (typeof type === "string") out.type = [type, "null"]; + else if (Array.isArray(type)) out.type = [...type, "null"]; + // `enum` is checked on its own: without null in it, a nullable enum still refuses null. + if (Array.isArray(schema.enum)) out.enum = [...schema.enum, null]; + return out; +} diff --git a/server/typescript/packages/codegen-ts/src/contract/object-schema.ts b/server/typescript/packages/codegen-ts/src/contract/object-schema.ts new file mode 100644 index 0000000000..73b8555521 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/contract/object-schema.ts @@ -0,0 +1,177 @@ +// FR-022 — an object as JSON Schema, one schema per wire shape. ACCURATE BY CONSTRUCTION: the create +// and update shapes walk `insertSchemaFields` / `updateSchemaFields`, the field sets the generated +// write validators are built from, so a contract cannot name a field the route does not take. +// +// read every field but a credential, each key present (a top-level null is written `null`). +// A value object's optional members follow `wire.nullMembers` instead. +// create what POST takes. A server-set field (generated key, @autoSet, derived, read-only, a +// write-denied field) is left out, and a TPH subtype's discriminator comes from the URL. +// update what PATCH and PUT take: nothing required, and a non-@required field may be null to +// clear it (FR-035). + +import { + type MetaField, + type MetaObject, + type MetaRoot, + WIRE_NULL_MEMBERS_EMIT, + WIRE_NULL_MEMBERS_OMIT, + wireFormatOf, +} from "@metaobjectsdev/metadata"; +import { DOC_ATTR_DESCRIPTION, OBJECT_SUBTYPE_VALUE } from "@metaobjectsdev/metadata/constants"; +import { isApiWriteDenied, isCredentialField, servesWriteApi } from "../api-surface.js"; +import { isAbstract } from "../instance-artifacts.js"; +import { tphPlan } from "../templates/tph-discriminator.js"; +import { insertSchemaFields, updateSchemaFields } from "../templates/zod-validators.js"; +import { tphDiscriminatorPin } from "../tph.js"; +import { type ContractShape, fieldSchema, type JsonSchema, type RefFor, withNull } from "./field-schema.js"; + +export interface ObjectSchemaEnv { + readonly root: MetaRoot; + readonly refFor: RefFor; + /** Name a TPH base's subtypes with OpenAPI's `discriminator`, which plain JSON Schema does not define. */ + readonly openApiDiscriminator?: boolean; +} + +function isValueObject(obj: MetaObject): boolean { + return obj.subType === OBJECT_SUBTYPE_VALUE; +} + +/** Whether the object has the create and update shapes of a served, writable resource. */ +function isWritableResource(obj: MetaObject, root: MetaRoot): boolean { + return servesWriteApi(obj) && tphPlan(obj, root) === null; +} + +/** The shapes an object has: a read shape always, the write shapes where something can write it. */ +export function shapesOf(obj: MetaObject, root: MetaRoot): readonly ContractShape[] { + if (isValueObject(obj)) return ["read", "create"]; + return isWritableResource(obj, root) ? ["read", "create", "update"] : ["read"]; +} + +/** The objects a contract covers. An abstract object has no rows of its own, unless it is a TPH base. */ +export function contractObjects(objects: readonly MetaObject[], root: MetaRoot): MetaObject[] { + return objects.filter((o) => !isAbstract(o) || tphPlan(o, root) !== null); +} + +/** `{ type, properties, required }`, with `required` left out when nothing is. */ +function objectBody(properties: Record, required: readonly string[]): JsonSchema { + return { type: "object", properties, ...(required.length > 0 && { required: [...required] }) }; +} + +/** A TPH base reads as whichever subtype a row is. */ +function polymorphicSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema | undefined { + const plan = tphPlan(obj, env.root); + if (plan === null) return undefined; + const oneOf = plan.subtypes.map((s) => ({ $ref: env.refFor(s.entity, "read") })); + if (env.openApiDiscriminator !== true) return { oneOf }; + const mapping = Object.fromEntries(plan.subtypes.map((s) => [s.value, env.refFor(s.entity, "read")])); + return { oneOf, discriminator: { propertyName: plan.discriminatorField, mapping } }; +} + +function readSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { + const polymorphic = polymorphicSchema(obj, env); + if (polymorphic !== undefined) return polymorphic; + const pk = new Set(obj.primaryIdentity()?.fields ?? []); + const pin = tphDiscriminatorPin(obj); + const nullMembers = wireFormatOf(obj).nullMembers; + const properties: Record = {}; + const required: string[] = []; + // A credential is never on the wire: the key is absent, not null. + for (const field of obj.fields().filter((f) => !isCredentialField(f))) { + if (pin !== undefined && field.name === pin.fieldName) { + properties[field.name] = { const: pin.value }; + required.push(field.name); + continue; + } + const schema = fieldSchema(field, "read", env.refFor); + if (pk.has(field.name) || field.isRequired) { + properties[field.name] = schema; + required.push(field.name); + } else if (isValueObject(obj) && nullMembers === WIRE_NULL_MEMBERS_OMIT) { + properties[field.name] = schema; + } else if (isValueObject(obj) && nullMembers !== WIRE_NULL_MEMBERS_EMIT) { + properties[field.name] = withNull(schema); + } else { + properties[field.name] = withNull(schema); + required.push(field.name); + } + } + return objectBody(properties, required); +} + +/** Whether a client writes this field: not a server-filled one, and not a write-denied one. */ +function isClientWritten(field: MetaField | undefined, serverSet: boolean | undefined): field is MetaField { + return field !== undefined && serverSet !== true && !isApiWriteDenied(field); +} + +function createSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { + const fieldByName = new Map(obj.fields().map((f) => [f.name, f])); + const properties: Record = {}; + const required: string[] = []; + for (const shape of insertSchemaFields(obj)) { + const field = fieldByName.get(shape.name); + // A TPH subtype's discriminator is injected from the URL, not read from the body. + if (shape.pinnedLiteral !== undefined || !isClientWritten(field, shape.autoSet)) continue; + properties[shape.name] = fieldSchema(field, "create", env.refFor); + if (!shape.optional) required.push(shape.name); + } + return objectBody(properties, required); +} + +function updateSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { + const fieldByName = new Map(obj.fields().map((f) => [f.name, f])); + const pk = new Set(obj.primaryIdentity()?.fields ?? []); + const properties: Record = {}; + for (const shape of updateSchemaFields(obj)) { + const field = fieldByName.get(shape.name); + if (!isClientWritten(field, shape.autoSet)) continue; + const schema = fieldSchema(field, "update", env.refFor); + // FR-035: a present null clears a non-@required field. A key is never cleared. + properties[shape.name] = field.isRequired || pk.has(shape.name) ? schema : withNull(schema); + } + return objectBody(properties, []); +} + +/** The object's schema in one shape, without a document's `$schema` or `$id`. */ +export function objectSchema(obj: MetaObject, shape: ContractShape, env: ObjectSchemaEnv): JsonSchema { + const body = shape === "read" ? readSchema(obj, env) : shape === "create" ? createSchema(obj, env) : updateSchema(obj, env); + const description = obj.attr(DOC_ATTR_DESCRIPTION); + return { ...(typeof description === "string" && { description }), ...body }; +} + +export interface ContractEntry { + readonly obj: MetaObject; + readonly shape: ContractShape; + readonly schema: JsonSchema; +} + +/** How one object's schema points at another: from a file to a file, or at a component. */ +export type RefFrom = (from: MetaObject, target: MetaObject, shape: ContractShape) => string; + +/** + * Every schema a set of objects needs: their own shapes, and each shape a `$ref` reaches, so a + * reference never dangles when the generator's `filter` leaves its target out of the set. + */ +export function collectContractEntries( + objects: readonly MetaObject[], + root: MetaRoot, + refFrom: RefFrom, + options: { readonly openApiDiscriminator?: boolean } = {}, +): ContractEntry[] { + const queued = new Map(); + const queue = (obj: MetaObject, shape: ContractShape): void => { + const key = `${obj.resolutionKey()}#${shape}`; + if (!queued.has(key)) queued.set(key, { obj, shape }); + }; + for (const obj of objects) for (const shape of shapesOf(obj, root)) queue(obj, shape); + + const entries: ContractEntry[] = []; + // A Map iterates what is added while it is walked, so this visits everything `refFor` queues. + for (const { obj, shape } of queued.values()) { + const refFor: RefFor = (target, targetShape) => { + queue(target, targetShape); + return refFrom(obj, target, targetShape); + }; + entries.push({ obj, shape, schema: objectSchema(obj, shape, { root, refFor, ...options }) }); + } + return entries; +} diff --git a/server/typescript/packages/codegen-ts/src/contract/strict-profile.ts b/server/typescript/packages/codegen-ts/src/contract/strict-profile.ts new file mode 100644 index 0000000000..3ec1e550b5 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/contract/strict-profile.ts @@ -0,0 +1,161 @@ +// FR-022 — the strict profile: one schema every structured-output host accepts. It is the +// intersection of what they take, not a variant per host: the root is an object, every property is +// required and optional ones are `["T", "null"]`, every object closes with +// `additionalProperties: false`, and there is no `$ref`, no recursion and no constraint keyword. +// What a keyword said moves into `description`, where a model still reads it. +// +// The shape is the one a model would fill in: the object's create shape where it has one, else its +// read shape. A strict document has no `$schema` or `$id`, because it is pasted into a request. + +import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata"; +import { type ContractShape, type JsonSchema, withNull } from "./field-schema.js"; +import { ContractEmitError, ERR_CONTRACT_STRICT_UNREPRESENTABLE } from "./contract-errors.js"; +import { objectSchema, shapesOf } from "./object-schema.js"; +import { tphPlan } from "../templates/tph-discriminator.js"; + +/** What each dropped keyword becomes in `description`. */ +const KEYWORD_PROSE: readonly (readonly [string, string])[] = [ + ["format", "Format"], + ["pattern", "Pattern"], + ["minLength", "Minimum length"], + ["maxLength", "Maximum length"], + ["minimum", "Minimum"], + ["maximum", "Maximum"], + ["minItems", "Minimum items"], + ["maxItems", "Maximum items"], +]; + +/** Pointers handed out while building, back to the object and shape each one names. */ +type Pointers = Map; + +interface Walk { + readonly root: MetaRoot; + readonly owner: MetaObject; + readonly pointers: Pointers; + /** The objects being inlined, outermost first: meeting one again is a cycle. */ + readonly stack: readonly MetaObject[]; + /** The property path from the owner, for the refusal to name. */ + readonly at: readonly string[]; +} + +function unrepresentable(walk: Walk, why: string): ContractEmitError { + const at = walk.at.length === 0 ? "" : ` (at '${walk.at.join(".")}')`; + return new ContractEmitError( + ERR_CONTRACT_STRICT_UNREPRESENTABLE, + `'${walk.owner.resolutionKey()}' has no strict schema: ${why}${at}. Leave it out with the generator's filter option.`, + ); +} + +/** `description`, with what the dropped keywords said appended to it. */ +function proseOf(schema: JsonSchema, extra: readonly string[] = []): JsonSchema { + const parts = typeof schema.description === "string" ? [schema.description] : []; + for (const [keyword, label] of KEYWORD_PROSE) { + // `int64` and its kin size a number for a code generator. They say nothing to a model. + if (keyword === "format" && soleType(schema) !== "string") continue; + if (schema[keyword] !== undefined) parts.push(`${label}: ${String(schema[keyword])}.`); + } + parts.push(...extra); + return parts.length === 0 ? {} : { description: parts.join(" ") }; +} + +/** The first type that is not null: a strict scalar has one type, plus null when optional. */ +function soleType(schema: JsonSchema): unknown { + return Array.isArray(schema.type) ? schema.type.find((t) => t !== "null") : schema.type; +} + +function isNullable(schema: JsonSchema): boolean { + return Array.isArray(schema.type) && schema.type.includes("null"); +} + +function strictOfObject(schema: JsonSchema, walk: Walk): JsonSchema { + if (schema.additionalProperties !== undefined) throw unrepresentable(walk, "a map has keys no schema can list"); + if (schema.properties === undefined) throw unrepresentable(walk, "an open object has properties no schema can list"); + const declared = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []); + const properties: Record = {}; + for (const [name, value] of Object.entries(schema.properties as Record)) { + const strict = strictOf(value, { ...walk, at: [...walk.at, name] }); + properties[name] = declared.has(name) ? strict : withNull(strict); + } + return { + ...(schema.title !== undefined && { title: schema.title }), + ...proseOf(schema), + type: "object", + properties, + required: Object.keys(properties), + additionalProperties: false, + }; +} + +function strictOfScalar(schema: JsonSchema): JsonSchema { + const nullable = isNullable(schema); + const type = soleType(schema); + // `inet`'s anyOf is two formats on one string. + const formats = Array.isArray(schema.anyOf) ? schema.anyOf.map((f) => (f as JsonSchema).format) : []; + const prose = proseOf(schema, formats.length > 0 ? [`Format: ${formats.join(" or ")}.`] : []); + return { + ...prose, + type: nullable ? [type, "null"] : type, + ...(schema.enum !== undefined && { enum: schema.enum }), + }; +} + +function inlineRef(ref: string, walk: Walk): JsonSchema { + const pointer = walk.pointers.get(ref); + if (pointer === undefined) throw new Error(`strict profile: no object behind '${ref}'.`); + const { target, shape } = pointer; + if (walk.stack.includes(target)) { + const cycle = [...walk.stack, target].map((o) => o.name).join(" -> "); + throw unrepresentable(walk, `its shape is recursive (${cycle})`); + } + return strictOf(buildRaw(target, shape, walk.root, walk.pointers), { ...walk, stack: [...walk.stack, target] }); +} + +/** One schema, as the strict subset states it. */ +function strictOf(schema: JsonSchema, walk: Walk): JsonSchema { + if (typeof schema.$ref === "string") return inlineRef(schema.$ref, walk); + if (Array.isArray(schema.oneOf)) throw unrepresentable(walk, "a polymorphic object has no single shape"); + // `withNull` over a `$ref`: the target, nullable. + const anyOf = schema.anyOf as JsonSchema[] | undefined; + if (anyOf !== undefined && typeof anyOf[0]?.$ref === "string") { + const inlined = withNull(inlineRef(anyOf[0].$ref, walk)); + return schema.description === undefined ? inlined : { ...inlined, description: schema.description }; + } + const type = soleType(schema); + if (type === "object") { + const strict = strictOfObject(schema, walk); + return isNullable(schema) ? withNull(strict) : strict; + } + if (type === "array") { + return { + ...proseOf(schema), + type: isNullable(schema) ? ["array", "null"] : "array", + items: strictOf(schema.items as JsonSchema, walk), + }; + } + if (type === undefined) throw unrepresentable(walk, "an open value has no type to state"); + return strictOfScalar(schema); +} + +function buildRaw(obj: MetaObject, shape: ContractShape, root: MetaRoot, pointers: Pointers): JsonSchema { + return objectSchema(obj, shape, { + root, + refFor: (target, targetShape) => { + const pointer = `strict:${target.resolutionKey()}#${targetShape}`; + pointers.set(pointer, { target, shape: targetShape }); + return pointer; + }, + }); +} + +/** Whether the object has a strict schema: a TPH base is a union, and each subtype has its own. */ +export function hasStrictSchema(obj: MetaObject, root: MetaRoot): boolean { + return tphPlan(obj, root) === null; +} + +/** The object's strict schema. Throws when the strict subset cannot state it. */ +export function strictSchema(obj: MetaObject, root: MetaRoot): JsonSchema { + const shape: ContractShape = shapesOf(obj, root).includes("create") ? "create" : "read"; + const pointers: Pointers = new Map(); + const raw = buildRaw(obj, shape, root, pointers); + return strictOf({ title: obj.name, ...raw }, { root, owner: obj, pointers, stack: [obj], at: [] }); +} diff --git a/server/typescript/packages/codegen-ts/src/generator-registry.ts b/server/typescript/packages/codegen-ts/src/generator-registry.ts index f46f1b5872..a4fda8f178 100644 --- a/server/typescript/packages/codegen-ts/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts/src/generator-registry.ts @@ -53,6 +53,8 @@ import { metamodelDocsFile, docsSiteFile, cubeModel, + jsonSchemaFile, + openApiFile, prismaSchemaFile, } from "./generators/index.js"; @@ -260,6 +262,41 @@ export const generatorRegistry: Record = { }, // ----- api -------------------------------------------------------------- + "contract-schema": { + name: "contract-schema", + kind: "generator", + layer: "api", + description: + "JSON Schema 2020-12 for the model's wire shapes (.schema.json, .create, .update), or one self-contained structured-output schema per object with profile: \"strict\". Faithful to the REST contract and the wire setting.", + tier: "neutral", + factory: () => jsonSchemaFile(), + factoryExport: "jsonSchemaFile", + options: "profile?, baseUri?, filter?, target?", + // The files import nothing, and a validator is whatever the reader already has. + requires: [], + runtimePackages: [], + runtimePeers: [], + configKeys: [], + ejectable: ejectable("contract-schema"), + }, + openapi: { + name: "openapi", + kind: "generator", + layer: "api", + description: + "One OpenAPI 3.1 document for the generated REST surface: the routes, the filter, sort and paging parameters, the error envelope, and the wire shapes as components.", + tier: "neutral", + factory: () => openApiFile(), + factoryExport: "openApiFile", + options: "title?, version?, description?, servers?, securitySchemes?, security?, path?, expose?, filter?, target?", + // The document imports nothing. It reads the routes generators' `expose` when they are wired, + // and does not need them: another port's routes answer the same contract. + requires: [], + runtimePackages: [], + runtimePeers: [], + configKeys: ["apiPrefix"], + ejectable: ejectable("openapi"), + }, routes: { name: "routes", kind: "generator", diff --git a/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts b/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts new file mode 100644 index 0000000000..536caf78e5 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts @@ -0,0 +1,90 @@ +// FR-022 — jsonSchemaFile(), the contract-schema reference generator: the wire shapes of the model's +// objects as JSON Schema 2020-12. The shapes are the pure stages in src/contract/; this file decides +// which objects get a file, where it lands and which profile it writes. +// +// Canonical (the default) writes `.schema.json` for what a read answers, and +// `.create.schema.json` / `.update.schema.json` where the object has those shapes. +// Strict writes one self-contained `.strict.schema.json`, for a structured-output host. + +import { type MetaObject, type MetaRoot } from "@metaobjectsdev/metadata"; +import { effectivePackage } from "../docs-paths.js"; +import type { EmittedFile, GenContext, Generator } from "../generator.js"; +import { packageToPath } from "../import-path.js"; +import type { ContractShape, JsonSchema } from "../contract/field-schema.js"; +import { collectContractEntries, contractObjects } from "../contract/object-schema.js"; +import { hasStrictSchema, strictSchema } from "../contract/strict-profile.js"; + +/** The generator's stable name. `json-schema` is the Java generator for metadata files: a different concept. */ +const GENERATOR_NAME = "contract-schema"; + +const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"; + +const SHAPE_INFIX: Record = { read: "", create: ".create", update: ".update" }; + +export interface JsonSchemaFileOptions { + /** `canonical` is full fidelity. `strict` is the structured-output subset. Default: canonical. */ + readonly profile?: "canonical" | "strict"; + /** An absolute prefix for each `$id`, when a registry needs one. Default: the file's relative path. */ + readonly baseUri?: string; + /** Narrow which objects get a file. An object a kept one references still gets its own. */ + readonly filter?: (obj: MetaObject) => boolean; + /** Named output target: the files land under its outDir. */ + readonly target?: string; +} + +function schemaDir(obj: MetaObject): string { + const pkg = effectivePackage(obj); + return pkg === undefined ? "schemas" : `schemas/${packageToPath(pkg)}`; +} + +function schemaPath(obj: MetaObject, shape: ContractShape): string { + return `${schemaDir(obj)}/${obj.name}${SHAPE_INFIX[shape]}.schema.json`; +} + +/** The relative `$ref` from one object's directory to another's file. */ +function relativeRef(from: MetaObject, target: MetaObject, shape: ContractShape): string { + const fromDir = schemaDir(from).split("/"); + const toParts = schemaPath(target, shape).split("/"); + let shared = 0; + while (shared < fromDir.length && fromDir[shared] === toParts[shared]) shared++; + const up = fromDir.slice(shared).map(() => ".."); + const rel = [...up, ...toParts.slice(shared)].join("/"); + return up.length === 0 ? `./${rel}` : rel; +} + +function renderJson(value: JsonSchema): string { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function canonicalFiles(objects: readonly MetaObject[], root: MetaRoot, baseUri: string | undefined): EmittedFile[] { + return collectContractEntries(objects, root, relativeRef).map(({ obj, shape, schema }) => { + const path = schemaPath(obj, shape); + const title = shape === "read" ? obj.name : `${obj.name}${SHAPE_INFIX[shape]}`; + const $id = baseUri === undefined ? path : `${baseUri.replace(/\/$/, "")}/${path}`; + return { path, content: renderJson({ $schema: JSON_SCHEMA_DIALECT, $id, title, ...schema }) }; + }); +} + +function strictFiles(objects: readonly MetaObject[], root: MetaRoot): EmittedFile[] { + return objects.filter((o) => hasStrictSchema(o, root)).map((obj) => ({ + path: `${schemaDir(obj)}/${obj.name}.strict.schema.json`, + content: renderJson(strictSchema(obj, root)), + })); +} + +function generateSchemaFiles(ctx: GenContext, options: JsonSchemaFileOptions): EmittedFile[] { + const root = ctx.loadedRoot; + const objects = contractObjects(ctx.entities.filter(ctx.matches), root); + return options.profile === "strict" ? strictFiles(objects, root) : canonicalFiles(objects, root, options.baseUri); +} + +/** JSON Schema files for the model's objects, in one profile. List it twice for both. */ +export function jsonSchemaFile(options: JsonSchemaFileOptions = {}): Generator { + const generator: Generator = { + name: GENERATOR_NAME, + generate: (ctx) => generateSchemaFiles(ctx, options), + }; + if (options.filter !== undefined) generator.filter = options.filter; + if (options.target !== undefined) generator.target = options.target; + return generator; +} diff --git a/server/typescript/packages/codegen-ts/src/generators/index.ts b/server/typescript/packages/codegen-ts/src/generators/index.ts index 6ffe4f9d17..95c37d0e04 100644 --- a/server/typescript/packages/codegen-ts/src/generators/index.ts +++ b/server/typescript/packages/codegen-ts/src/generators/index.ts @@ -96,3 +96,8 @@ export { cubeModel, type CubeModelGeneratorOptions } from "./cube-model.js"; // #479 — the Prisma schema: the model's tables as `schema.prisma`, for a project that keeps Prisma // as its ORM. A reference helper; its pure stages are exported from the package root. export { prismaSchemaFile, type PrismaSchemaFileOptions } from "./prisma-schema-file.js"; + +// FR-022 — the contract emitters: the model's wire shapes as JSON Schema 2020-12 and the generated +// REST surface as one OpenAPI 3.1 document. Reference helpers; the pure stages are in src/contract/. +export { jsonSchemaFile, type JsonSchemaFileOptions } from "./contract-schema-file.js"; +export { openApiFile, type OpenApiFileOptions } from "./openapi-file.js"; diff --git a/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts b/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts new file mode 100644 index 0000000000..cc16e35b04 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts @@ -0,0 +1,66 @@ +// FR-022 — openApiFile(), the openapi reference generator: one OpenAPI 3.1 document for the generated +// REST surface. The document is built in src/contract/build-openapi.ts; this file decides which +// objects it covers, where it lands and which routes the run mounts. +// +// The document never depends on the run. It covers the whole model, narrowed by this generator's +// `filter` and the project's `scope`, so `meta gen Author` writes the same bytes as a full run. + +import type { GenContext, Generator } from "../generator.js"; +import { buildOpenApiDocument, type OpenApiDocumentOptions } from "../contract/build-openapi.js"; +import { contractObjects } from "../contract/object-schema.js"; +import type { ExposeOption } from "../routes-expose.js"; +import { generatableObjects } from "../source-detect.js"; +import type { MetaObject } from "@metaobjectsdev/metadata"; + +/** The generator's stable name. */ +const GENERATOR_NAME = "openapi"; + +const DEFAULT_PATH = "openapi.json"; + +export interface OpenApiFileOptions extends OpenApiDocumentOptions { + /** Output path relative to the target's outDir. Default: `openapi.json`. */ + readonly path?: string; + /** + * The verbs your routes mount, for a project whose routes this run does not generate (a port + * other than TypeScript). Default: what the routes generators in this run declare, else all of them. + */ + readonly expose?: ExposeOption; + /** Narrow which objects the document covers. An object a kept one references stays as a component. */ + readonly filter?: (obj: MetaObject) => boolean; + /** Named output target: the file lands under its outDir. */ + readonly target?: string; +} + +function documentObjects(ctx: GenContext): MetaObject[] { + const root = ctx.loadedRoot; + // A served report is its read model here, as it is in `ctx.entities`. + const inScope = generatableObjects(root.objects(), root).filter((o) => (ctx.select?.(o.resolutionKey()) ?? true) && ctx.matches(o)); + return contractObjects(inScope, root); +} + +/** One OpenAPI 3.1 document for the whole model. */ +export function openApiFile(options: OpenApiFileOptions = {}): Generator { + const { path, expose, filter, target, ...document } = options; + const generator: Generator = { + name: GENERATOR_NAME, + generate: (ctx) => { + const declared = ctx.config.routesExpose; + const surface = expose ?? declared?.rest ?? declared?.restHono; + const mounted = declared?.restMounted ?? declared?.restHonoMounted; + const doc = buildOpenApiDocument({ + ...document, + root: ctx.loadedRoot, + objects: documentObjects(ctx), + apiPrefix: ctx.renderContext?.apiPrefix ?? "", + relationMap: ctx.renderContext?.relationMap ?? new Map(), + reportExport: ctx.config.includeReportExport ?? false, + ...(surface !== undefined && { expose: surface }), + ...(mounted !== undefined && { mounted }), + }); + return [{ path: path ?? DEFAULT_PATH, content: `${JSON.stringify(doc, null, 2)}\n` }]; + }, + }; + if (filter !== undefined) generator.filter = filter; + if (target !== undefined) generator.target = target; + return generator; +} diff --git a/server/typescript/packages/codegen-ts/src/index.ts b/server/typescript/packages/codegen-ts/src/index.ts index b696f9f0ec..f5afab9c24 100644 --- a/server/typescript/packages/codegen-ts/src/index.ts +++ b/server/typescript/packages/codegen-ts/src/index.ts @@ -432,6 +432,18 @@ export type { SharedModel, SharedModelFiles, SharedModelInput } from "./shared-m // FR-044 Plan 4 — the Cube exporter. `cubeModel()` is the reference generator; the two pure // stages it wires are public so an owned (ejected) copy imports only this package and keeps // only the parts it owns: which entities get a cube, the file layout and the YAML text. +export { jsonSchemaFile } from "./generators/contract-schema-file.js"; +export type { JsonSchemaFileOptions } from "./generators/contract-schema-file.js"; +export { openApiFile } from "./generators/openapi-file.js"; +export type { OpenApiFileOptions } from "./generators/openapi-file.js"; +export { buildOpenApiDocument, OPENAPI_VERSION } from "./contract/build-openapi.js"; +export type { OpenApiBuildInput, OpenApiDocumentOptions } from "./contract/build-openapi.js"; +export { objectSchema, shapesOf } from "./contract/object-schema.js"; +export { fieldSchema, withNull } from "./contract/field-schema.js"; +export type { ContractShape, JsonSchema } from "./contract/field-schema.js"; +export { strictSchema } from "./contract/strict-profile.js"; +export { ContractEmitError, CONTRACT_ERROR_CODES, ERR_CONTRACT_NAME_COLLISION, ERR_CONTRACT_STRICT_UNREPRESENTABLE } from "./contract/contract-errors.js"; +export type { ContractErrorCode } from "./contract/contract-errors.js"; export { cubeModel } from "./generators/cube-model.js"; export type { CubeModelGeneratorOptions } from "./generators/cube-model.js"; export { buildCubeModel, hasReportingVocabulary } from "./cube/build-cube-model.js"; diff --git a/server/typescript/packages/codegen-ts/src/reference-templates.ts b/server/typescript/packages/codegen-ts/src/reference-templates.ts index 952fb86c18..1a2d35e0fb 100644 --- a/server/typescript/packages/codegen-ts/src/reference-templates.ts +++ b/server/typescript/packages/codegen-ts/src/reference-templates.ts @@ -32,6 +32,9 @@ export const REFERENCE_GENERATOR_NAMES = [ // The Prisma schema (#479): which objects get a model, the file layout and the blocks above // the models, with `buildPrismaSchema` and the renderers handed over beside it. "prisma-schema", + // The contract emitters (FR-022): which objects get a schema or a route in the document, and where + // the files land, with the field mapping and the document builder handed over beside them. + "contract-schema", "openapi", // The stored-procedure / table-function wrapper and the LLM-call trace helper. "callable", "trace-helper", // The publisher's shared-model artifact: which source files may contribute and which version diff --git a/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts b/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts new file mode 100644 index 0000000000..fe2cfdae30 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts @@ -0,0 +1,115 @@ +// REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/contract-schema.ts) and own it. +// Then import it LOCALLY in metaobjects.config.ts: +// import { jsonSchemaFile } from "./codegen/generators/contract-schema.js"; +// +// RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's +// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not +// reach for `Bun.*` globals here; they are undefined and take the whole run down with +// `Bun is not defined`. Use `node:` builtins instead. +// targets: JSON Schema 2020-12. The files import nothing and need no MetaObjects package: +// any validator reads them, in any language. +// use-when: something outside this repository needs the shape of what the API sends and +// takes (a validator, a schema registry, a client in a language without a port), or +// a model needs to fill one in (`profile: "strict"`: structured output, MCP). +// emits: /schemas//.schema.json for what a read answers, and +// .create.schema.json / .update.schema.json where the object has those +// shapes. `profile: "strict"` writes one self-contained .strict.schema.json +// instead. List the generator twice to get both. +// customize: which objects get a file (`generateSchemaFiles`, narrowed by the `filter` option), +// where they land (`schemaDir`, `schemaPath`) and the `$id` (`canonicalFiles`). +// Copied beside it, so yours too: `fieldSchema`, which maps each field type to its +// wire encoding; `objectSchema`, which decides what each shape holds; and +// `strictSchema`, the strict subset. Change those only if your routes differ from +// the REST contract: they state what every port's routes answer. +// composes-with: openapi, whose components are these schemas. +// +// A file is written for each object the run selects, and for each object a kept one references, +// so a `$ref` never dangles when `filter` leaves its target out. `wire.decimals` and +// `wire.nullMembers` in `.metaobjects/config.json` decide the read shape of a decimal and of a value +// object's optional member. A project that publishes this contract should set both, because the +// default is each server language's own form (docs/features/contract-emitters.md). +// +// Everything below imports the engine from `@metaobjectsdev/codegen-ts` and the contract stages from +// `../contract/`. `meta eject` copies those modules into your project, under `codegen/templates/`, +// and your copy of this file imports them from there: they are yours too. + +import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata"; +import { effectivePackage, packageToPath, type EmittedFile, type GenContext, type Generator } from "@metaobjectsdev/codegen-ts/engine"; +import type { ContractShape, JsonSchema } from "../contract/field-schema.js"; +import { collectContractEntries, contractObjects } from "../contract/object-schema.js"; +import { hasStrictSchema, strictSchema } from "../contract/strict-profile.js"; + +/** The generator's stable name. `json-schema` is the Java generator for metadata files: a different concept. */ +const GENERATOR_NAME = "contract-schema"; + +const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"; + +const SHAPE_INFIX: Record = { read: "", create: ".create", update: ".update" }; + +export interface JsonSchemaFileOptions { + /** `canonical` is full fidelity. `strict` is the structured-output subset. Default: canonical. */ + readonly profile?: "canonical" | "strict"; + /** An absolute prefix for each `$id`, when a registry needs one. Default: the file's relative path. */ + readonly baseUri?: string; + /** Narrow which objects get a file. An object a kept one references still gets its own. */ + readonly filter?: (obj: MetaObject) => boolean; + /** Named output target: the files land under its outDir. */ + readonly target?: string; +} + +function schemaDir(obj: MetaObject): string { + const pkg = effectivePackage(obj); + return pkg === undefined ? "schemas" : `schemas/${packageToPath(pkg)}`; +} + +function schemaPath(obj: MetaObject, shape: ContractShape): string { + return `${schemaDir(obj)}/${obj.name}${SHAPE_INFIX[shape]}.schema.json`; +} + +/** The relative `$ref` from one object's directory to another's file. */ +function relativeRef(from: MetaObject, target: MetaObject, shape: ContractShape): string { + const fromDir = schemaDir(from).split("/"); + const toParts = schemaPath(target, shape).split("/"); + let shared = 0; + while (shared < fromDir.length && fromDir[shared] === toParts[shared]) shared++; + const up = fromDir.slice(shared).map(() => ".."); + const rel = [...up, ...toParts.slice(shared)].join("/"); + return up.length === 0 ? `./${rel}` : rel; +} + +function renderJson(value: JsonSchema): string { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function canonicalFiles(objects: readonly MetaObject[], root: MetaRoot, baseUri: string | undefined): EmittedFile[] { + return collectContractEntries(objects, root, relativeRef).map(({ obj, shape, schema }) => { + const path = schemaPath(obj, shape); + const title = shape === "read" ? obj.name : `${obj.name}${SHAPE_INFIX[shape]}`; + const $id = baseUri === undefined ? path : `${baseUri.replace(/\/$/, "")}/${path}`; + return { path, content: renderJson({ $schema: JSON_SCHEMA_DIALECT, $id, title, ...schema }) }; + }); +} + +function strictFiles(objects: readonly MetaObject[], root: MetaRoot): EmittedFile[] { + return objects.filter((o) => hasStrictSchema(o, root)).map((obj) => ({ + path: `${schemaDir(obj)}/${obj.name}.strict.schema.json`, + content: renderJson(strictSchema(obj, root)), + })); +} + +function generateSchemaFiles(ctx: GenContext, options: JsonSchemaFileOptions): EmittedFile[] { + const root = ctx.loadedRoot; + const objects = contractObjects(ctx.entities.filter(ctx.matches), root); + return options.profile === "strict" ? strictFiles(objects, root) : canonicalFiles(objects, root, options.baseUri); +} + +/** JSON Schema files for the model's objects, in one profile. List it twice for both. */ +export function jsonSchemaFile(options: JsonSchemaFileOptions = {}): Generator { + const generator: Generator = { + name: GENERATOR_NAME, + generate: (ctx) => generateSchemaFiles(ctx, options), + }; + if (options.filter !== undefined) generator.filter = options.filter; + if (options.target !== undefined) generator.target = options.target; + return generator; +} diff --git a/server/typescript/packages/codegen-ts/src/reference/openapi.ts b/server/typescript/packages/codegen-ts/src/reference/openapi.ts new file mode 100644 index 0000000000..cc0d9772d2 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/reference/openapi.ts @@ -0,0 +1,91 @@ +// REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/openapi.ts) and own it. +// Then import it LOCALLY in metaobjects.config.ts: +// import { openApiFile } from "./codegen/generators/openapi.js"; +// +// RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's +// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not +// reach for `Bun.*` globals here; they are undefined and take the whole run down with +// `Bun is not defined`. Use `node:` builtins instead. +// targets: OpenAPI 3.1. The document imports nothing: openapi-generator, Kiota, orval, oasdiff +// and every other OpenAPI tool read it as they read a hand-written one. +// use-when: a consumer of this API works from an OpenAPI document, or you want a change to the +// API to show as a diff (docs/features/contract-emitters.md has the oasdiff recipe). +// emits: /openapi.json: every route of the REST contract the model serves, with the +// filter, sort and paging parameters, the error envelope, and the model's wire +// shapes as components. +// customize: which objects it covers (`documentObjects`, narrowed by the `filter` option) and +// where the file lands (the `path` option). Copied beside it, so yours too: +// `buildOpenApiDocument`, which decides which operations exist and what each one +// answers. If your API has routes of its own, that is the file to change. +// composes-with: contract-schema, whose canonical schemas are these components. Reads what the +// routes generators declare (`expose`, `filter`), so a verb a project does not mount +// is not in the document. For a project whose routes another port generates, pass +// `expose` here, because this run cannot see them. +// +// `title`, `version`, `servers`, `securitySchemes` and `security` are options and not metadata: +// they are facts about a deployment and an application, and the model is shared across both. +// +// Everything below imports the engine from `@metaobjectsdev/codegen-ts` and the contract stages from +// `../contract/`. `meta eject` copies those modules into your project, under `codegen/templates/`, +// and your copy of this file imports them from there: they are yours too. +// +// The document never depends on the run. It covers the whole model, narrowed by this generator's +// `filter` and the project's `scope`, so `meta gen Author` writes the same bytes as a full run. + +import { generatableObjects, type ExposeOption, type GenContext, type Generator } from "@metaobjectsdev/codegen-ts/engine"; +import { buildOpenApiDocument, type OpenApiDocumentOptions } from "../contract/build-openapi.js"; +import { contractObjects } from "../contract/object-schema.js"; +import type { MetaObject } from "@metaobjectsdev/metadata"; + +/** The generator's stable name. */ +const GENERATOR_NAME = "openapi"; + +const DEFAULT_PATH = "openapi.json"; + +export interface OpenApiFileOptions extends OpenApiDocumentOptions { + /** Output path relative to the target's outDir. Default: `openapi.json`. */ + readonly path?: string; + /** + * The verbs your routes mount, for a project whose routes this run does not generate (a port + * other than TypeScript). Default: what the routes generators in this run declare, else all of them. + */ + readonly expose?: ExposeOption; + /** Narrow which objects the document covers. An object a kept one references stays as a component. */ + readonly filter?: (obj: MetaObject) => boolean; + /** Named output target: the file lands under its outDir. */ + readonly target?: string; +} + +function documentObjects(ctx: GenContext): MetaObject[] { + const root = ctx.loadedRoot; + // A served report is its read model here, as it is in `ctx.entities`. + const inScope = generatableObjects(root.objects(), root).filter((o) => (ctx.select?.(o.resolutionKey()) ?? true) && ctx.matches(o)); + return contractObjects(inScope, root); +} + +/** One OpenAPI 3.1 document for the whole model. */ +export function openApiFile(options: OpenApiFileOptions = {}): Generator { + const { path, expose, filter, target, ...document } = options; + const generator: Generator = { + name: GENERATOR_NAME, + generate: (ctx) => { + const declared = ctx.config.routesExpose; + const surface = expose ?? declared?.rest ?? declared?.restHono; + const mounted = declared?.restMounted ?? declared?.restHonoMounted; + const doc = buildOpenApiDocument({ + ...document, + root: ctx.loadedRoot, + objects: documentObjects(ctx), + apiPrefix: ctx.renderContext?.apiPrefix ?? "", + relationMap: ctx.renderContext?.relationMap ?? new Map(), + reportExport: ctx.config.includeReportExport ?? false, + ...(surface !== undefined && { expose: surface }), + ...(mounted !== undefined && { mounted }), + }); + return [{ path: path ?? DEFAULT_PATH, content: `${JSON.stringify(doc, null, 2)}\n` }]; + }, + }; + if (filter !== undefined) generator.filter = filter; + if (target !== undefined) generator.target = target; + return generator; +} diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts b/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts new file mode 100644 index 0000000000..6dbc5e7790 --- /dev/null +++ b/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts @@ -0,0 +1,304 @@ +// FR-022 — what the goldens hold but cannot explain: each behaviour the contract emitters promise, +// stated with a payload a validator takes or refuses, and with the generator options the corpus does +// not vary. The corpus proves the bytes hold. This file proves the bytes mean what the docs say. + +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { MetaObject } from "@metaobjectsdev/metadata"; +import { type ContractCase, contractCases, contractTree, loadCaseModel } from "../../scripts/gen-contract-conformance.js"; +import { readTree } from "../../scripts/gen-cube-model-canonical.js"; +import { DECIMAL_CANONICAL_PATTERN } from "../../src/contract/field-schema.js"; +import { jsonSchemaFile, openApiFile } from "../../src/generators/index.js"; +import { defineConfig, type Generator, runGen } from "../../src/index.js"; +import { jsonSchemaFile as refJsonSchemaFile } from "../../src/reference/contract-schema.js"; +import { openApiFile as refOpenApiFile } from "../../src/reference/openapi.js"; +import { ajvOver, type JsonDoc } from "./contract-validators.js"; + +const CASES = new Map(contractCases().map((c) => [c.name, c])); + +function corpusCase(name: string): ContractCase { + const c = CASES.get(name); + if (c === undefined) throw new Error(`no contract case '${name}'`); + return c; +} + +interface RunOptions { + entityFilter?: string[]; +} + +async function emit(c: ContractCase, generators: Generator[], run: RunOptions = {}): Promise> { + const root = await loadCaseModel(c); + const dir = mkdtempSync(join(tmpdir(), "contract-behaviour-")); + try { + const outDir = join(dir, "out"); + await runGen({ + config: defineConfig({ outDir, dialect: "postgres", apiPrefix: "/api", generators }), + metadata: root, + genStateDir: join(dir, ".gen-state"), + ...(run.entityFilter !== undefined && { entityFilter: run.entityFilter }), + }); + return readTree(outDir); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +function jsonAt(tree: ReadonlyMap, path: string): JsonDoc { + const content = tree.get(path); + if (content === undefined) throw new Error(`${path} was not written; wrote ${[...tree.keys()].join(", ")}`); + return JSON.parse(content) as JsonDoc; +} + +/** A validator for one schema file of a tree, every other schema in the tree loaded for its `$ref`s. */ +function validatorOf(tree: ReadonlyMap, path: string): (value: unknown) => boolean { + const schemas = [...tree].filter(([p]) => p.endsWith(".schema.json") && !p.endsWith(".strict.schema.json")); + const ajv = ajvOver(schemas.map(([, content]) => JSON.parse(content) as JsonDoc)); + const validate = ajv.getSchema(jsonAt(tree, path)["$id"] as string); + if (validate === undefined) throw new Error(`${path} did not compile`); + return (value) => validate(value) as boolean; +} + +const ALL_FIELDS = "schemas/acme/contract/AllFields"; +const ADDRESS = "schemas/acme/contract/Address"; + +function sampleRow(): Record { + return { + id: "6f1c2b1e-0d7a-4d55-9c1e-0a4e8f1d2c3b", + title: "A title", email: null, host: null, quantity: 3, views: null, ratio: null, score: null, amount: null, + active: null, startsOn: null, startsAt: null, publishedAt: null, wallClock: null, priceCents: null, status: "draft", + stage: null, homepage: null, looseLink: null, address: null, shipTo: null, stops: null, tags: null, + internalNote: null, issuedBy: null, syncedBy: null, createdAt: null, updatedAt: null, + }; +} + +describe("a read answers what the wire setting says", () => { + test("wire.decimals: string fixes the canonical string, and refuses a number", async () => { + const tree = await contractTree(corpusCase("all-fields-wire-string-omit")); + const valid = validatorOf(tree, `${ALL_FIELDS}.schema.json`); + expect(valid({ ...sampleRow(), amount: "19.99" })).toBe(true); + expect(valid({ ...sampleRow(), amount: 19.99 })).toBe(false); + expect(valid({ ...sampleRow(), amount: "1e3" })).toBe(false); + }); + + test("the default takes either form, because each server language writes its own", async () => { + const tree = await contractTree(corpusCase("all-fields")); + const valid = validatorOf(tree, `${ALL_FIELDS}.schema.json`); + expect(valid({ ...sampleRow(), amount: "19.99" })).toBe(true); + expect(valid({ ...sampleRow(), amount: 19.99 })).toBe(true); + expect(valid({ ...sampleRow(), amount: true })).toBe(false); + }); + + test("a decimal is accepted for a write as a string or a number, in every setting", async () => { + for (const name of ["all-fields", "all-fields-wire-string-omit", "all-fields-wire-string-emit"]) { + const tree = await contractTree(corpusCase(name)); + const amount = (jsonAt(tree, `${ALL_FIELDS}.create.schema.json`)["properties"] as Record)["amount"]!; + expect(amount["type"], name).toEqual(["string", "number"]); + } + }); + + test("the canonical decimal pattern is the normalized form", () => { + const canonical = new RegExp(DECIMAL_CANONICAL_PATTERN); + for (const ok of ["0", "-1", "19.99", "0.5", "1000"]) expect(canonical.test(ok), ok).toBe(true); + for (const bad of ["1e3", "+1", "1.", ".5", "01", "1.50"]) expect(canonical.test(bad), bad).toBe(false); + }); + + test("nullMembers: omit leaves a null member out of a value object", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields-wire-string-omit")), `${ADDRESS}.schema.json`); + expect(valid({ street: "1 Main" })).toBe(true); + expect(valid({ street: "1 Main", unit: null })).toBe(false); + }); + + test("nullMembers: emit writes every member, null included", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields-wire-string-emit")), `${ADDRESS}.schema.json`); + expect(valid({ street: "1 Main", unit: null, latitude: null })).toBe(true); + expect(valid({ street: "1 Main" })).toBe(false); + }); + + test("the default takes either, because the language's serializer decides", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields")), `${ADDRESS}.schema.json`); + expect(valid({ street: "1 Main" })).toBe(true); + expect(valid({ street: "1 Main", unit: null })).toBe(true); + }); + + test("a top-level optional field is written null, whatever the setting", async () => { + for (const name of ["all-fields", "all-fields-wire-string-omit", "all-fields-wire-string-emit"]) { + const tree = await contractTree(corpusCase(name)); + const valid = validatorOf(tree, `${ALL_FIELDS}.schema.json`); + const { email: _dropped, ...withoutEmail } = sampleRow(); + expect(valid(sampleRow()), name).toBe(true); + expect(valid(withoutEmail), name).toBe(false); + } + }); +}); + +describe("types the JSON Schema formats do not state", () => { + test("a time has no offset, so it is a pattern", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields")), `${ALL_FIELDS}.schema.json`); + expect(valid({ ...sampleRow(), startsAt: "09:30:00" })).toBe(true); + expect(valid({ ...sampleRow(), startsAt: "09:30:00.250" })).toBe(true); + expect(valid({ ...sampleRow(), startsAt: "9am" })).toBe(false); + }); + + test("a local timestamp has no offset, and a zoned one must", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields")), `${ALL_FIELDS}.schema.json`); + expect(valid({ ...sampleRow(), wallClock: "2026-10-30T09:30:00" })).toBe(true); + expect(valid({ ...sampleRow(), wallClock: "2026-10-30T09:30:00Z" })).toBe(false); + expect(valid({ ...sampleRow(), publishedAt: "2026-10-30T09:30:00Z" })).toBe(true); + expect(valid({ ...sampleRow(), publishedAt: "2026-10-30T09:30:00" })).toBe(false); + }); +}); + +describe("a write takes what a route takes", () => { + test("a timestamp with no offset is read as UTC, on a write and nowhere else", async () => { + const tree = await contractTree(corpusCase("all-fields")); + const create = validatorOf(tree, `${ALL_FIELDS}.create.schema.json`); + const required = { title: "A title", quantity: 1, status: "draft" }; + expect(create({ ...required, publishedAt: "2026-10-30T09:30:00" })).toBe(true); + expect(create({ ...required, publishedAt: "2026-10-30T09:30:00Z" })).toBe(true); + expect(create({ ...required, publishedAt: "2026-10-30T09:30:00+02:00" })).toBe(true); + expect(create({ ...required, publishedAt: "next tuesday" })).toBe(false); + expect(validatorOf(tree, `${ALL_FIELDS}.schema.json`)({ ...sampleRow(), publishedAt: "2026-10-30T09:30:00" })).toBe(false); + }); + + test("a report's list route takes format only when the project wires report-export", async () => { + const formatOn = (tree: ReadonlyMap): boolean => { + const list = (jsonAt(tree, "openapi.json")["paths"] as Record)["/api/invoice_totals"]!; + return list.get.parameters.some((p) => p.name === "format"); + }; + expect(formatOn(await contractTree(corpusCase("report")))).toBe(true); + expect(formatOn(await emit(corpusCase("report"), [openApiFile({ title: "t", version: "1" })]))).toBe(false); + }); +}); + +describe("the three shapes", () => { + test("an update takes null for an optional field, and not for a required one", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields")), `${ALL_FIELDS}.update.schema.json`); + expect(valid({})).toBe(true); + expect(valid({ email: null })).toBe(true); + expect(valid({ title: null })).toBe(false); + }); + + test("a create leaves an optional field out, and does not take null for it", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields")), `${ALL_FIELDS}.create.schema.json`); + const title = "A title"; + const status = "draft"; + expect(valid({ title, quantity: 1, status })).toBe(true); + expect(valid({ title, quantity: 1, status, email: null })).toBe(false); + expect(valid({ quantity: 1, status })).toBe(false); + }); + + test("validators hold on a write: length, pattern and range", async () => { + const valid = validatorOf(await contractTree(corpusCase("all-fields")), `${ALL_FIELDS}.create.schema.json`); + const status = "draft"; + expect(valid({ title: "Abcd", quantity: 1, status })).toBe(true); + expect(valid({ title: "Ab", quantity: 1, status })).toBe(false); + expect(valid({ title: "lowercase", quantity: 1, status })).toBe(false); + expect(valid({ title: "A title", quantity: 1001, status })).toBe(false); + }); + + test("a credential is on no shape, and @formExclude is only on the read", async () => { + const tree = await contractTree(corpusCase("secret-fields")); + const keysOf = (path: string): string[] => Object.keys(jsonAt(tree, path)["properties"] as JsonDoc); + const account = "schemas/acme/secrets/Account"; + expect(keysOf(`${account}.schema.json`)).toEqual(["id", "name", "tokenHash"]); + expect(keysOf(`${account}.create.schema.json`)).toEqual(["name"]); + expect(keysOf(`${account}.update.schema.json`)).toEqual(["name"]); + expect(tree.get("openapi.json")).not.toContain("pinHash"); + }); + + test("a projection and a report read, and nothing writes them", async () => { + const tree = await contractTree(corpusCase("projection")); + const paths = [...tree.keys()]; + expect(paths.some((p) => p.endsWith(".create.schema.json") && p.includes("Summary"))).toBe(false); + const doc = jsonAt(tree, "openapi.json"); + for (const [path, item] of Object.entries(doc["paths"] as Record)) { + if (!path.includes("summar")) continue; + expect(Object.keys(item).filter((verb) => verb !== "parameters"), path).toEqual(["get"]); + } + }); +}); + +describe("what a project's run changes", () => { + test("openapi writes the same bytes for a run narrowed to one entity", async () => { + const c = corpusCase("m2m"); + const whole = await emit(c, [openApiFile({ title: "t", version: "1" })]); + const root = await loadCaseModel(c); + const first = root.objects()[0]!.name; + const narrowed = await emit(c, [openApiFile({ title: "t", version: "1" })], { entityFilter: [first] }); + expect(narrowed.get("openapi.json")).toBe(whole.get("openapi.json")!); + }); + + test("a generator filter narrows the document, and keeps what a kept object references", async () => { + const c = corpusCase("all-fields"); + const onlyAllFields = (o: MetaObject): boolean => o.name === "AllFields"; + const tree = await emit(c, [openApiFile({ title: "t", version: "1", filter: onlyAllFields })]); + const doc = jsonAt(tree, "openapi.json"); + expect(Object.keys(doc["paths"] as JsonDoc)).toEqual(["/api/all_fields", "/api/all_fields/{id}"]); + expect(Object.keys((doc["components"] as { schemas: JsonDoc }).schemas)).toContain("Address"); + expect(Object.keys((doc["components"] as { schemas: JsonDoc }).schemas)).not.toContain("Bag"); + }); + + test("a schema filter keeps a file for what a kept object references, so no $ref dangles", async () => { + const c = corpusCase("all-fields"); + const tree = await emit(c, [jsonSchemaFile({ filter: (o) => o.name === "AllFields" })]); + expect(tree.has("schemas/acme/contract/Address.schema.json")).toBe(true); + expect(tree.has("schemas/acme/contract/Bag.schema.json")).toBe(false); + validatorOf(tree, `${ALL_FIELDS}.schema.json`); + }); + + test("expose narrows the document to the verbs the project mounts", async () => { + const tree = await emit(corpusCase("blog"), [openApiFile({ title: "t", version: "1", expose: ["list", "get"] })]); + const paths = jsonAt(tree, "openapi.json")["paths"] as Record; + const verbsOf = (path: string): string[] => Object.keys(paths[path]!).filter((key) => key !== "parameters"); + expect(verbsOf("/api/authors")).toEqual(["get"]); + expect(verbsOf("/api/authors/{id}")).toEqual(["get"]); + }); + + test("baseUri prefixes every $id, and the relative $refs still resolve", async () => { + const baseUri = "https://schemas.example.com/v1/"; + const tree = await emit(corpusCase("all-fields"), [jsonSchemaFile({ baseUri })]); + const id = jsonAt(tree, `${ALL_FIELDS}.schema.json`)["$id"] as string; + expect(id).toBe(`${baseUri}${ALL_FIELDS}.schema.json`); + validatorOf(tree, `${ALL_FIELDS}.schema.json`); + }); + + test("title, version, servers and security are options, and pass through untouched", async () => { + const tree = await emit(corpusCase("blog"), [openApiFile({ + title: "Blog API", + version: "2.1.0", + description: "The blog.", + servers: [{ url: "https://api.example.com" }], + securitySchemes: { bearer: { type: "http", scheme: "bearer" } }, + security: [{ bearer: [] }], + })]); + const doc = jsonAt(tree, "openapi.json"); + expect(doc["info"]).toEqual({ title: "Blog API", version: "2.1.0", description: "The blog." }); + expect(doc["servers"]).toEqual([{ url: "https://api.example.com" }]); + expect(doc["security"]).toEqual([{ bearer: [] }]); + expect((doc["components"] as JsonDoc)["securitySchemes"]).toEqual({ bearer: { type: "http", scheme: "bearer" } }); + }); + + test("the path option moves the document, and the strict profile writes only .strict files", async () => { + const tree = await emit(corpusCase("blog"), [ + openApiFile({ title: "t", version: "1", path: "api/openapi.json" }), + jsonSchemaFile({ profile: "strict" }), + ]); + expect(tree.has("api/openapi.json")).toBe(true); + const schemas = [...tree.keys()].filter((p) => p.endsWith(".json") && p !== "api/openapi.json"); + expect(schemas.length).toBeGreaterThan(0); + for (const p of schemas) expect(p.endsWith(".strict.schema.json"), p).toBe(true); + }); +}); + +describe("the reference copies", () => { + for (const c of contractCases()) { + if (c.name.startsWith("error-")) continue; + test(`${c.name}: the copy a project ejects writes the same tree`, async () => { + const packaged = await contractTree(c); + const owned = await contractTree(c, { jsonSchemaFile: refJsonSchemaFile, openApiFile: refOpenApiFile }); + expect(owned).toEqual(packaged); + }); + } +}); diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts b/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts new file mode 100644 index 0000000000..bbfab2248d --- /dev/null +++ b/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts @@ -0,0 +1,111 @@ +// FR-022 — the contract corpus (fixtures/contract-conformance/). Each case holds either `expected/`, +// the exact tree the emitters write, or `expected-error.txt`, the refusal. Every document a case +// writes goes through a real validator as well: Ajv strict for JSON Schema 2020-12, the OpenAPI 3.1 +// specification's schema for the document, and the strict subset's own rules. The goldens prove the +// bytes hold; the validators prove stock tools take them. + +import { describe, expect, test } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { ContractEmitError } from "../../src/contract/contract-errors.js"; +import { CONTRACT_SCRIPT, contractCases, contractTree } from "../../scripts/gen-contract-conformance.js"; +import { readTree } from "../../scripts/gen-cube-model-canonical.js"; +import { ajvOver, type JsonDoc, openApiErrors, strictViolations } from "./contract-validators.js"; + +const STRICT_SUFFIX = ".strict.schema.json"; +const OPENAPI_FILE = "openapi.json"; + +function contractErrorOf(e: unknown): ContractEmitError | undefined { + for (let at: unknown = e; at instanceof Error; at = at.cause) { + if (at instanceof ContractEmitError) return at; + } + return undefined; +} + +function documents(tree: ReadonlyMap, keep: (path: string) => boolean): Map { + const out = new Map(); + for (const [path, content] of tree) if (keep(path)) out.set(path, JSON.parse(content) as JsonDoc); + return out; +} + +const cases = contractCases(); + +describe("the contract corpus", () => { + test("has cases of both kinds", () => { + expect(cases.some((c) => existsSync(join(c.dir, "expected")))).toBe(true); + expect(cases.some((c) => existsSync(join(c.dir, "expected-error.txt")))).toBe(true); + }); + + test("the README lists every case, and no case it does not hold", () => { + const readme = readFileSync(join(cases[0]!.dir, "..", "README.md"), "utf8"); + const listed = [...readme.matchAll(/^\| `([a-z0-9-]+)` \|/gm)].map((m) => m[1]!); + expect(listed.slice().sort()).toEqual(cases.map((c) => c.name)); + }); + + for (const c of cases) { + const errorFile = join(c.dir, "expected-error.txt"); + if (existsSync(errorFile)) { + test(`${c.name}: refuses`, async () => { + let thrown: unknown; + try { + await contractTree(c); + } catch (e) { + thrown = e; + } + expect(contractErrorOf(thrown)?.message).toBe(readFileSync(errorFile, "utf8").trimEnd()); + }); + continue; + } + + describe(c.name, () => { + const written = contractTree(c); + + test(`writes the golden tree (regenerate with ${CONTRACT_SCRIPT})`, async () => { + const expected = readTree(join(c.dir, "expected")); + const tree = await written; + expect([...tree.keys()].sort()).toEqual([...expected.keys()].sort()); + for (const [path, content] of tree) expect(content, path).toBe(expected.get(path)!); + }); + + test("every canonical schema compiles in strict Ajv, every $ref resolved", async () => { + const canonical = documents(await written, (p) => p.endsWith(".schema.json") && !p.endsWith(STRICT_SUFFIX)); + expect(canonical.size).toBeGreaterThan(0); + const ajv = ajvOver([...canonical.values()]); + for (const [path, schema] of canonical) expect(ajv.getSchema(schema["$id"] as string), path).toBeDefined(); + }); + + test("every strict schema compiles, and holds to the subset", async () => { + const strict = documents(await written, (p) => p.endsWith(STRICT_SUFFIX)); + const ajv = ajvOver([]); + for (const [path, schema] of strict) { + expect(ajv.compile(schema), path).toBeDefined(); + expect(strictViolations(schema), path).toEqual([]); + } + }); + + test("the document is valid OpenAPI 3.1", async () => { + const doc = documents(await written, (p) => p === OPENAPI_FILE).get(OPENAPI_FILE)!; + expect(await openApiErrors(doc)).toEqual([]); + }); + + test("the document is a valid schema set: every component compiles in Ajv", async () => { + const doc = documents(await written, (p) => p === OPENAPI_FILE).get(OPENAPI_FILE)!; + const ajv = ajvOver([]); + ajv.opts.strictSchema = false; // OpenAPI's `discriminator` is its own keyword + ajv.addSchema(doc as never, "openapi"); + const schemas = (doc["components"] as { schemas: JsonDoc }).schemas; + for (const name of Object.keys(schemas)) { + expect(ajv.compile({ $ref: `openapi#/components/schemas/${name}` }), name).toBeDefined(); + } + }); + + test("every operation has a unique operationId", async () => { + const doc = documents(await written, (p) => p === OPENAPI_FILE).get(OPENAPI_FILE)!; + const ids = Object.values(doc["paths"] as Record>) + .flatMap((item) => Object.values(item).map((op) => op.operationId)) + .filter((id) => id !== undefined); + expect(new Set(ids).size).toBe(ids.length); + }); + }); + } +}); diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts b/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts new file mode 100644 index 0000000000..9ed529f4e6 --- /dev/null +++ b/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts @@ -0,0 +1,48 @@ +// The real validators the contract corpus runs the emitted documents through: Ajv in its strict +// mode for JSON Schema 2020-12, with the standard formats, and a validator of the OpenAPI 3.1 +// specification's own schema. Neither knows anything about MetaObjects, which is the point. + +import { Validator } from "@seriousme/openapi-schema-validator"; +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; + +export type JsonDoc = Record; + +/** A strict Ajv holding every canonical schema of a tree, so relative `$ref`s resolve. */ +export function ajvOver(schemas: readonly JsonDoc[]): Ajv2020 { + // `allowUnionTypes`: `["string", "null"]` is plain 2020-12, and Ajv's strictTypes only logs it. + const ajv = new Ajv2020({ strict: true, allowUnionTypes: true }); + addFormats(ajv as never); // ajv-formats resolves its own ajv: the instance is the same shape + for (const schema of schemas) ajv.addSchema(schema); + return ajv; +} + +export async function openApiErrors(doc: JsonDoc): Promise { + const result = await new Validator().validate(doc); + return result.valid ? [] : (result.errors as unknown[]); +} + +/** The only keywords a strict schema may use, anywhere in it. */ +const STRICT_KEYWORDS: ReadonlySet = new Set([ + "title", "description", "type", "properties", "required", "additionalProperties", "items", "enum", +]); + +/** Every way a strict schema leaves the subset: empty when it holds. */ +export function strictViolations(schema: JsonDoc, at = "#"): string[] { + const found: string[] = []; + for (const key of Object.keys(schema)) { + if (!STRICT_KEYWORDS.has(key)) found.push(`${at}: keyword '${key}'`); + } + const type = schema["type"]; + const named = Array.isArray(type) ? type.filter((t) => t !== "null") : [type]; + if (named.length !== 1) found.push(`${at}: type ${JSON.stringify(type)} is not one type, plus null`); + if (named[0] === "object") { + const properties = (schema["properties"] ?? {}) as Record; + if (schema["additionalProperties"] !== false) found.push(`${at}: an object is not closed`); + const required = (schema["required"] ?? []) as string[]; + if ([...required].sort().join() !== Object.keys(properties).sort().join()) found.push(`${at}: not every property is required`); + for (const [name, value] of Object.entries(properties)) found.push(...strictViolations(value, `${at}/${name}`)); + } + if (named[0] === "array") found.push(...strictViolations(schema["items"] as JsonDoc, `${at}/items`)); + return found; +} diff --git a/server/typescript/packages/codegen-ts/test/generator-registry.test.ts b/server/typescript/packages/codegen-ts/test/generator-registry.test.ts index 89a3c44fc8..8f3b86ef50 100644 --- a/server/typescript/packages/codegen-ts/test/generator-registry.test.ts +++ b/server/typescript/packages/codegen-ts/test/generator-registry.test.ts @@ -36,9 +36,17 @@ const EXPECTED_NATIVE = [ "shared-model", ] as const; -// NEUTRAL: language-neutral pages about the model. A description of the output only: every +// NEUTRAL: language-neutral artifacts about the model. A description of the output only: every // entry, either tier, runs when the project lists it (ADR-0034 Amendment 9). -const EXPECTED_NEUTRAL = ["docs", "requirements", "agent-docs", "metamodel-docs", "docs-site"] as const; +const EXPECTED_NEUTRAL = [ + "docs", + "requirements", + "agent-docs", + "metamodel-docs", + "docs-site", + "contract-schema", + "openapi", +] as const; describe("generator-registry (ADR-0021 D3)", () => { test("contains every expected stable name", () => { diff --git a/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts b/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts index a67dff3293..32f99156a7 100644 --- a/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts +++ b/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts @@ -44,6 +44,10 @@ import { cubeModel as builtinCubeModel } from "../src/generators/cube-model.js"; import { cubeModel as refCubeModel } from "../src/reference/cube-model.js"; import { prismaSchemaFile as builtinPrismaSchema } from "../src/generators/prisma-schema-file.js"; import { prismaSchemaFile as refPrismaSchema } from "../src/reference/prisma-schema.js"; +import { jsonSchemaFile as builtinContractSchema } from "../src/generators/contract-schema-file.js"; +import { jsonSchemaFile as refContractSchema } from "../src/reference/contract-schema.js"; +import { openApiFile as builtinOpenApi } from "../src/generators/openapi-file.js"; +import { openApiFile as refOpenApi } from "../src/reference/openapi.js"; import { callableFile as builtinCallable } from "../src/generators/callable-file.js"; import { callableFile as refCallable } from "../src/reference/callable.js"; import { traceHelperFile as builtinTraceHelper } from "../src/generators/trace-helper-file.js"; @@ -184,6 +188,8 @@ const PAIRS: Record Generator; ref: () // vocabulary), so the pair is compared over two empty sets there; the describe at the end // of this file runs both over models that DO, under both Cube dialects. "cube-model": { builtin: builtinCubeModel, ref: refCubeModel }, + "contract-schema": { builtin: builtinContractSchema, ref: refContractSchema }, + openapi: { builtin: builtinOpenApi, ref: refOpenApi }, // These fixtures run under sqlite, where a CHECK is refused unless omitted, and some declare // views, which a Prisma schema cannot create; the describe at the end of this file runs the // pair over the Prisma fixture under each provider, checks and refusals included. diff --git a/server/typescript/packages/codegen-ts/test/reference-templates.test.ts b/server/typescript/packages/codegen-ts/test/reference-templates.test.ts index d7e8fb4422..91a71cbcda 100644 --- a/server/typescript/packages/codegen-ts/test/reference-templates.test.ts +++ b/server/typescript/packages/codegen-ts/test/reference-templates.test.ts @@ -15,7 +15,7 @@ describe("reference-templates reader", () => { expect([...REFERENCE_GENERATOR_NAMES]).toEqual([ "entity", "queries", "routes", "routes-hono", "barrel", "names", "report-export", "prompt-render", "output-parser", "extractor", "output-prompt", "render-helper", - "requirement-tests", "cube-model", "prisma-schema", "callable", "trace-helper", + "requirement-tests", "cube-model", "prisma-schema", "contract-schema", "openapi", "callable", "trace-helper", "shared-model", "docs", "api-docs", "requirements", "agent-docs", "metamodel-docs", ]); }); diff --git a/server/typescript/packages/integration-tests/package.json b/server/typescript/packages/integration-tests/package.json index 4b8e2eb6ed..3828059ab7 100644 --- a/server/typescript/packages/integration-tests/package.json +++ b/server/typescript/packages/integration-tests/package.json @@ -35,6 +35,8 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.5.4", "@types/pg": "^8.11.6", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "bun-types": "^1.1.27", "miniflare": "4.20251210.0", "mysql2": "^3.11.0", diff --git a/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts b/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts new file mode 100644 index 0000000000..7b0fafdff4 --- /dev/null +++ b/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts @@ -0,0 +1,118 @@ +// api-contract-openapi-check.ts — hold a GENERATED server to the document the openapi generator +// writes for it (FR-022). The api-contract corpora assert what a route answers; this asserts the +// document says so: every exchange in a generated lane must be a documented route, every 2xx request +// must be one the document accepts, and every response must match the schema documented for it. +// +// The document under test is the golden in fixtures/contract-conformance//expected/openapi.json, +// so a server and a document that drift apart fail here even when each passes its own corpus. +// Nothing in it knows about MetaObjects: routes are matched from `paths`, and bodies are validated +// by Ajv through `$ref`s into the document. + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; +import type { ApiRequest } from "./api-contract-scenario.ts"; + +const CONTRACT_CORPUS_DIR = resolve(import.meta.dir, "..", "..", "..", "..", "..", "fixtures", "contract-conformance"); +const DOCUMENT_ID = "openapi"; +const JSON_MEDIA_TYPE = "application/json"; + +/** + * The REST contract's TypeScript-only query extensions (docs/features/api-contract.md, "TS-only + * filter extensions"). The document states the cross-port contract, so it leaves these out, and a + * scenario that sends one is outside what this check can hold. + */ +const TS_ONLY_QUERY = /^(search|filter\[(or|and)\])/; + +type JsonObject = Record; + +interface Operation { + parameters?: Array<{ name: string; in: string }>; + requestBody?: { content?: Record }; + responses: Record }>; +} + +type PathItem = Record & { parameters?: Array<{ name: string; in: string }> }; + +export type ContractCheck = (req: ApiRequest, status: number, bodyText: string) => void; + +function templateRegex(template: string): RegExp { + return new RegExp(`^${template.split("/").map((part) => (part.startsWith("{") ? "[^/]+" : part)).join("/")}$`); +} + +/** A document's path templates, literal ones first: `/x/subtypes` wins over `/x/{id}`. */ +function matchers(paths: Record): Array<{ template: string; pattern: RegExp }> { + return Object.keys(paths) + .sort((a, b) => a.split("{").length - b.split("{").length) + .map((template) => ({ template, pattern: templateRegex(template) })); +} + +function pointerToken(token: string): string { + return token.replace(/~/g, "~0").replace(/\//g, "~1"); +} + +function queryNames(path: string): string[] { + const at = path.indexOf("?"); + if (at < 0) return []; + return [...new URLSearchParams(path.slice(at + 1)).keys()]; +} + +/** A checker over one contract case's golden document. */ +export function openApiCheck(caseName: string): ContractCheck { + const document = JSON.parse(readFileSync(resolve(CONTRACT_CORPUS_DIR, caseName, "expected", "openapi.json"), "utf8")) as { + paths: Record; + }; + // `strictSchema: false`: OpenAPI's `discriminator` is its own keyword, not JSON Schema's. + const ajv = new Ajv2020({ strictSchema: false, allowUnionTypes: true, allErrors: true }); + addFormats(ajv as never); // ajv-formats resolves its own ajv: the instance is the same shape + ajv.addSchema(document as never, DOCUMENT_ID); + const templates = matchers(document.paths); + + function fail(req: ApiRequest, why: string): never { + throw new Error(`${caseName}: ${req.method} ${req.path} (${req.id}): ${why}`); + } + + /** Compiled by its place in the document, so the `$ref`s inside resolve against the document. */ + function validate(req: ApiRequest, what: string, at: string[], value: unknown): void { + const check = ajv.compile({ $ref: `${DOCUMENT_ID}#/${at.map(pointerToken).join("/")}` }); + if (check(value)) return; + fail(req, `${what} does not match the document: ${ajv.errorsText(check.errors)}: ${JSON.stringify(value)}`); + } + + return (req, status, bodyText) => { + const route = req.path.split("?")[0]!; + const template = templates.find((t) => t.pattern.test(route))?.template; + const item = template === undefined ? undefined : document.paths[template]; + const operation = item?.[req.method.toLowerCase()] as Operation | undefined; + if (operation === undefined) { + // The document states the routes that exist. A 404 or 405 is the answer to one that does not. + if (status === 404 || status === 405) return; + fail(req, `answered ${status} on a route the document does not hold`); + } + const at = ["paths", template!, req.method.toLowerCase()]; + const documented = operation.responses[String(status)]; + if (documented === undefined) fail(req, `answered ${status}, which the document does not list`); + + if (status >= 200 && status < 300) { + const declared = new Set([...(item?.parameters ?? []), ...(operation.parameters ?? [])].map((p) => p.name)); + for (const name of queryNames(req.path)) { + if (!declared.has(name) && !TS_ONLY_QUERY.test(name)) fail(req, `query parameter '${name}' is not in the document`); + } + const request = operation.requestBody?.content?.[JSON_MEDIA_TYPE]?.schema; + if (request !== undefined && req.body !== undefined) { + validate(req, "the request body", [...at, "requestBody", "content", JSON_MEDIA_TYPE, "schema"], req.body); + } + } + + const response = documented.content?.[JSON_MEDIA_TYPE]?.schema; + if (response === undefined || bodyText.length === 0) return; + let body: unknown; + try { + body = JSON.parse(bodyText); + } catch { + return; // a CSV or XML export: the document states it by media type, and its bytes are the corpus's + } + validate(req, "the response", [...at, "responses", String(status), "content", JSON_MEDIA_TYPE, "schema"], body); + }; +} diff --git a/server/typescript/packages/integration-tests/src/api-contract-scenario.ts b/server/typescript/packages/integration-tests/src/api-contract-scenario.ts index 41ea9becb3..ba4019d47d 100644 --- a/server/typescript/packages/integration-tests/src/api-contract-scenario.ts +++ b/server/typescript/packages/integration-tests/src/api-contract-scenario.ts @@ -109,7 +109,11 @@ function parseScenario(file: string): ApiScenario { * shared by the report lane's variants (SQLite, D1, MySQL) so the request envelope lives once — * the Postgres lane's own copy predates this and is the cross-port runner's reference. */ -export async function runScenario(scenario: ApiScenario, baseUrl: string): Promise { +export async function runScenario( + scenario: ApiScenario, + baseUrl: string, + check?: (req: ApiRequest, status: number, bodyText: string) => void, +): Promise { for (const req of scenario.requests) { const init: RequestInit = { method: req.method }; const headers: Record = { ...req.headers }; @@ -121,6 +125,7 @@ export async function runScenario(scenario: ApiScenario, baseUrl: string): Promi const res = await fetch(baseUrl + req.path, init); const bodyText = await res.text(); assertResponseFull(scenario.name, req, res.status, bodyText, (name) => res.headers.get(name)); + check?.(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-decimal.test.ts b/server/typescript/packages/integration-tests/test/api-contract-decimal.test.ts index 289337c38b..bdabdec2aa 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-decimal.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-decimal.test.ts @@ -10,6 +10,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { API_CONTRACT_DECIMAL_DIR, API_CONTRACT_DECIMAL_SCENARIOS_DIR } from "../src/paths.ts"; +import { openApiCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, runScenario } from "../src/api-contract-scenario.ts"; import { startDecimalServer, type DecimalEngine } from "../src/api-contract-decimal-server.ts"; import type { ReportSeed } from "../src/api-contract-report-generated-server.ts"; @@ -50,7 +51,7 @@ for (const engine of ENGINES) describe(`api contract decimal (ADR-0019 Amendment for (const scenario of scenarios) { if (scenario.engines !== undefined && !scenario.engines.includes(engine)) continue; test(scenario.name, async () => { - await withServer(engine, (baseUrl) => runScenario(scenario, baseUrl)); + await withServer(engine, (baseUrl) => runScenario(scenario, baseUrl, openApiCheck("decimal"))); }, { timeout: 120_000 }); } diff --git a/server/typescript/packages/integration-tests/test/api-contract-generated.test.ts b/server/typescript/packages/integration-tests/test/api-contract-generated.test.ts index 31ac7d28ba..384a8fbc84 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-generated.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-generated.test.ts @@ -15,6 +15,7 @@ import { describe, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { API_CONTRACT_DIR, API_CONTRACT_SCENARIOS_DIR } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startGeneratedServer, @@ -26,6 +27,8 @@ import { startPostgres } from "../src/postgres-container.ts"; const SEED = JSON.parse(readFileSync(join(API_CONTRACT_DIR, "seed.json"), "utf8")) as { rows: AuthorRow[] }; const META_PATH = join(API_CONTRACT_DIR, "meta.json"); +const contractCheck: ContractCheck = openApiCheck("blog"); + describe("api contract conformance — TS GENERATED routes runner", () => { for (const scenario of loadScenarios(API_CONTRACT_SCENARIOS_DIR)) { test(scenario.name, async () => { @@ -59,5 +62,6 @@ async function executeRequests(scenario: ApiScenario, server: GeneratedServerHan try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + contractCheck(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-jsonb.test.ts b/server/typescript/packages/integration-tests/test/api-contract-jsonb.test.ts index 0562be5046..557229d9ff 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-jsonb.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-jsonb.test.ts @@ -15,6 +15,7 @@ import { describe, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { API_CONTRACT_JSONB_DIR, API_CONTRACT_JSONB_SCENARIOS_DIR } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { loadMetadataFile } from "../src/load-metadata.ts"; @@ -55,7 +56,7 @@ describe("api contract jsonb open-bag — GENERATED routes lane", () => { try { server = await startGeneratedJsonbServer(pg.connectionUri, META_PATH); await server.applySeed(SEED.rows); - await runScenario(scenario, server); + await runScenario(scenario, server, openApiCheck("jsonb")); } finally { if (server) await server.close(); await pg.stop(); @@ -64,7 +65,7 @@ describe("api contract jsonb open-bag — GENERATED routes lane", () => { } }); -async function runScenario(scenario: ApiScenario, server: Lane): Promise { +async function runScenario(scenario: ApiScenario, server: Lane, check?: ContractCheck): Promise { for (const req of scenario.requests) { const init: RequestInit = { method: req.method }; if (req.body !== undefined) { @@ -78,5 +79,6 @@ async function runScenario(scenario: ApiScenario, server: Lane): Promise { try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + check?.(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-m2m.test.ts b/server/typescript/packages/integration-tests/test/api-contract-m2m.test.ts index 3a393b7a51..1fc5674bfa 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-m2m.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-m2m.test.ts @@ -14,6 +14,7 @@ import { describe, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { API_CONTRACT_M2M_DIR, API_CONTRACT_M2M_SCENARIOS_DIR } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { startM2mServer, type M2mServerHandle } from "../src/api-contract-m2m-server.ts"; @@ -53,7 +54,7 @@ describe("api contract M:N — GENERATED routes lane", () => { try { server = await startGeneratedM2mServer(pg.connectionUri, META_PATH); await server.applySeed(SEED); - await runScenario(scenario, server); + await runScenario(scenario, server, openApiCheck("m2m")); } finally { if (server) await server.close(); await pg.stop(); @@ -62,7 +63,7 @@ describe("api contract M:N — GENERATED routes lane", () => { } }); -async function runScenario(scenario: ApiScenario, server: Lane): Promise { +async function runScenario(scenario: ApiScenario, server: Lane, check?: ContractCheck): Promise { for (const req of scenario.requests) { const init: RequestInit = { method: req.method }; if (req.body !== undefined) { @@ -76,5 +77,6 @@ async function runScenario(scenario: ApiScenario, server: Lane): Promise { try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + check?.(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-openapi-check.test.ts b/server/typescript/packages/integration-tests/test/api-contract-openapi-check.test.ts new file mode 100644 index 0000000000..e01ad0f670 --- /dev/null +++ b/server/typescript/packages/integration-tests/test/api-contract-openapi-check.test.ts @@ -0,0 +1,66 @@ +// The check every generated API lane runs (FR-022), held to failing. A lane that passes proves +// nothing unless the check can refuse, so each way a server and its document can disagree is here, +// with no server: one exchange at a time against a corpus document. No Docker needed. + +import { describe, expect, test } from "bun:test"; +import { openApiCheck } from "../src/api-contract-openapi-check.ts"; +import type { ApiRequest, HttpMethod } from "../src/api-contract-scenario.ts"; + +const check = openApiCheck("blog"); + +function request(method: HttpMethod, path: string, body?: unknown): ApiRequest { + return { id: "probe", method, path, body, expect: { status: 200 } }; +} + +const AUTHOR = { id: 1, name: "Ada", bio: null, createdAt: "2026-10-30T09:30:00Z", issuedCurrency: null, autoCreatedAt: null, autoUpdatedAt: null }; + +describe("openApiCheck", () => { + test("takes an exchange the document states", () => { + const row = { ...AUTHOR }; + expect(() => check(request("GET", "/api/authors?limit=10&offset=0&withCount=1"), 200, JSON.stringify({ rows: [row], total: 1 }))).not.toThrow(); + expect(() => check(request("GET", "/api/authors"), 200, JSON.stringify([row]))).not.toThrow(); + expect(() => check(request("GET", "/api/authors/1"), 200, JSON.stringify(row))).not.toThrow(); + expect(() => check(request("GET", "/api/authors/999"), 404, JSON.stringify({ error: "not_found" }))).not.toThrow(); + }); + + test("takes a 404 or 405 on a route the document does not hold", () => { + expect(() => check(request("GET", "/api/nothing"), 404, "")).not.toThrow(); + expect(() => check(request("DELETE", "/api/nothing/1"), 405, "")).not.toThrow(); + }); + + test("refuses a route the document does not hold, when it answers", () => { + expect(() => check(request("GET", "/api/nothing"), 200, "[]")).toThrow("does not hold"); + }); + + test("refuses a status the document does not list", () => { + expect(() => check(request("GET", "/api/authors/1"), 418, "{}")).toThrow("does not list"); + }); + + test("refuses a response that does not match its schema", () => { + expect(() => check(request("GET", "/api/authors/1"), 200, JSON.stringify({ id: "one" }))).toThrow("does not match"); + }); + + test("refuses a list that is neither the array nor the page", () => { + expect(() => check(request("GET", "/api/authors"), 200, JSON.stringify({ rows: [AUTHOR] }))).toThrow("does not match"); + }); + + test("refuses an error that is not the envelope", () => { + expect(() => check(request("GET", "/api/authors/1"), 404, JSON.stringify({ message: "no" }))).toThrow("does not match"); + }); + + test("refuses a query parameter a 2xx answered and the document does not state", () => { + expect(() => check(request("GET", "/api/authors?shiny=1"), 200, "[]")).toThrow("'shiny' is not in the document"); + }); + + test("leaves the TypeScript-only extensions alone", () => { + expect(() => check(request("GET", "/api/authors?search=ada"), 200, "[]")).not.toThrow(); + }); + + test("refuses a request body the document would not accept, when a 2xx took it", () => { + expect(() => check(request("POST", "/api/authors", { name: 7 }), 201, JSON.stringify(AUTHOR))).toThrow("the request body"); + }); + + test("leaves a request the server refused alone: those scenarios send bad input on purpose", () => { + expect(() => check(request("POST", "/api/authors", { name: 7 }), 400, JSON.stringify({ error: "validation" }))).not.toThrow(); + }); +}); diff --git a/server/typescript/packages/integration-tests/test/api-contract-projection.test.ts b/server/typescript/packages/integration-tests/test/api-contract-projection.test.ts index 2997b2015e..66041ef8bd 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-projection.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-projection.test.ts @@ -17,6 +17,7 @@ import { API_CONTRACT_PROJECTION_DIR, API_CONTRACT_PROJECTION_SCENARIOS_DIR, } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { @@ -30,6 +31,8 @@ const SEED = JSON.parse( ) as ProjectionSeed; const META_PATH = join(API_CONTRACT_PROJECTION_DIR, "meta.json"); +const contractCheck: ContractCheck = openApiCheck("projection"); + describe("api contract projection (F22) — GENERATED routes lane", () => { for (const scenario of loadScenarios(API_CONTRACT_PROJECTION_SCENARIOS_DIR)) { test(scenario.name, async () => { @@ -64,5 +67,6 @@ async function runScenario( try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + contractCheck(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-report.test.ts b/server/typescript/packages/integration-tests/test/api-contract-report.test.ts index d5880efbc7..cb1f619baf 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-report.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-report.test.ts @@ -9,6 +9,7 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { API_CONTRACT_REPORT_DIR, API_CONTRACT_REPORT_SCENARIOS_DIR } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponseFull, type ApiScenario } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { @@ -34,6 +35,8 @@ const SERVED_PATHS: Array<[string, string]> = [ ["ProductRefunds", "/api/product_refunds"], ]; +const contractCheck: ContractCheck = openApiCheck("report"); + describe("api contract report (FR-044) — GENERATED routes lane", () => { for (const scenario of loadScenarios(API_CONTRACT_REPORT_SCENARIOS_DIR)) { test(scenario.name, async () => { @@ -105,6 +108,8 @@ async function runScenario( } if (Object.keys(headers).length > 0) init.headers = headers; const res = await fetch(server.baseUrl + req.path, init); - assertResponseFull(scenario.name, req, res.status, await res.text(), (name) => res.headers.get(name)); + const bodyText = await res.text(); + assertResponseFull(scenario.name, req, res.status, bodyText, (name) => res.headers.get(name)); + contractCheck(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-secret-fields.test.ts b/server/typescript/packages/integration-tests/test/api-contract-secret-fields.test.ts index e9d9f3827b..89c89e5cf3 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-secret-fields.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-secret-fields.test.ts @@ -13,6 +13,7 @@ import { API_CONTRACT_SECRET_FIELDS_DIR, API_CONTRACT_SECRET_FIELDS_SCENARIOS_DIR, } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { @@ -26,6 +27,8 @@ const SEED = JSON.parse( ) as SecretFieldsSeed; const META_PATH = join(API_CONTRACT_SECRET_FIELDS_DIR, "meta.json"); +const contractCheck: ContractCheck = openApiCheck("secret-fields"); + describe("api contract secret fields (#484) — GENERATED routes lane", () => { for (const scenario of loadScenarios(API_CONTRACT_SECRET_FIELDS_SCENARIOS_DIR)) { test(scenario.name, async () => { @@ -91,6 +94,7 @@ async function runScenario(scenario: ApiScenario, server: GeneratedSecretFieldsS try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + contractCheck(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-tph.test.ts b/server/typescript/packages/integration-tests/test/api-contract-tph.test.ts index cf1c55009b..51916a3e74 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-tph.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-tph.test.ts @@ -15,6 +15,7 @@ import { describe, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { API_CONTRACT_TPH_DIR, API_CONTRACT_TPH_SCENARIOS_DIR } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { startTphServer, type TphServerHandle } from "../src/api-contract-tph-server.ts"; @@ -60,7 +61,7 @@ describe("api contract TPH — GENERATED routes lane", () => { try { server = await startGeneratedTphServer(pg.connectionUri, META_PATH); await server.applySeed(SEED); - await runScenario(scenario, server); + await runScenario(scenario, server, openApiCheck("tph")); } finally { if (server) await server.close(); await pg.stop(); @@ -69,7 +70,7 @@ describe("api contract TPH — GENERATED routes lane", () => { } }); -async function runScenario(scenario: ApiScenario, server: Lane): Promise { +async function runScenario(scenario: ApiScenario, server: Lane, check?: ContractCheck): Promise { for (const req of scenario.requests) { const init: RequestInit = { method: req.method }; if (req.body !== undefined) { @@ -83,5 +84,6 @@ async function runScenario(scenario: ApiScenario, server: Lane): Promise { try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + check?.(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/api-contract-write-through.test.ts b/server/typescript/packages/integration-tests/test/api-contract-write-through.test.ts index 9b73b66272..2ec5d26efa 100644 --- a/server/typescript/packages/integration-tests/test/api-contract-write-through.test.ts +++ b/server/typescript/packages/integration-tests/test/api-contract-write-through.test.ts @@ -18,6 +18,7 @@ import { API_CONTRACT_WRITE_THROUGH_DIR, API_CONTRACT_WRITE_THROUGH_SCENARIOS_DIR, } from "../src/paths.ts"; +import { openApiCheck, type ContractCheck } from "../src/api-contract-openapi-check.ts"; import { loadScenarios, assertResponse, type ApiScenario, parseBodyExact } from "../src/api-contract-scenario.ts"; import { startPostgres } from "../src/postgres-container.ts"; import { @@ -31,6 +32,8 @@ const SEED = JSON.parse( ) as WriteThroughSeed; const META_PATH = join(API_CONTRACT_WRITE_THROUGH_DIR, "meta.json"); +const contractCheck: ContractCheck = openApiCheck("write-through"); + describe("api contract write-through (#214) — GENERATED routes lane", () => { for (const scenario of loadScenarios(API_CONTRACT_WRITE_THROUGH_SCENARIOS_DIR)) { test(scenario.name, async () => { @@ -65,5 +68,6 @@ async function runScenario( try { body = parseBodyExact(bodyText); } catch { body = bodyText; } } assertResponse(scenario.name, req, res.status, body); + contractCheck(req, res.status, bodyText); } } diff --git a/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts b/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts new file mode 100644 index 0000000000..9edfd9fea3 --- /dev/null +++ b/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts @@ -0,0 +1,187 @@ +// FR-022 — the release check, run for real: two emitted OpenAPI documents through oasdiff, the +// tool docs/features/contract-emitters.md tells a project to use. It proves stock oasdiff reads what +// the generator writes, and that a change to the model shows up as the change it is: adding an +// optional field is compatible, and removing one breaks consumers. +// +// oasdiff comes from PATH, else from the pinned image through Docker. With neither, the test is +// skipped and says why, the way the lanes that need a container do. + +import { describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { defineConfig, runGen } from "@metaobjectsdev/codegen-ts"; +import { openApiFile } from "@metaobjectsdev/codegen-ts/generators"; +import { loadUris } from "@metaobjectsdev/metadata"; +import { pathToFileURL } from "node:url"; + +const OASDIFF_IMAGE = "tufin/oasdiff:v1.35.0"; +const TIMEOUT_MS = 180_000; + +type Runner = (args: string[], dir: string) => { status: number | null; stdout: string; stderr: string }; + +function succeeds(command: string, args: string[]): boolean { + return spawnSync(command, args, { stdio: "ignore" }).status === 0; +} + +function findRunner(): Runner | undefined { + if (succeeds("oasdiff", ["--version"])) { + return (args, dir) => { + const r = spawnSync("oasdiff", args, { cwd: dir, encoding: "utf8" }); + return { status: r.status, stdout: r.stdout, stderr: r.stderr }; + }; + } + if (!succeeds("docker", ["info"])) return undefined; + return (args, dir) => { + const r = spawnSync("docker", ["run", "--rm", "-v", `${dir}:/work`, "-w", "/work", OASDIFF_IMAGE, ...args], { encoding: "utf8" }); + return { status: r.status, stdout: r.stdout, stderr: r.stderr }; + }; +} + +const BASE_META = { + "metadata.root": { + package: "acme::oasdiff", + children: [{ + "object.entity": { + name: "Customer", + children: [ + { "source.rdb": { "@table": "customers" } }, + { "field.long": { name: "id" } }, + { "field.string": { name: "name", "@required": true, "@maxLength": 100, "@filterable": true } }, + { "field.string": { name: "nickname", "@maxLength": 100 } }, + { "identity.primary": { name: "pk", "@fields": "id", "@generation": "increment" } }, + ], + }, + }], + }, +}; + +type Child = Record>; + +function customerWith(change: (children: Child[]) => Child[]): unknown { + const copy = structuredClone(BASE_META); + const entity = copy["metadata.root"].children[0]!["object.entity"]; + entity.children = change(entity.children as Child[]) as typeof entity.children; + return copy; +} + +async function emitDocument(meta: unknown, dir: string, name: string): Promise { + const metaPath = join(dir, `${name}.meta.json`); + writeFileSync(metaPath, JSON.stringify(meta)); + const result = await loadUris([pathToFileURL(metaPath).href]); + expect(result.errors).toEqual([]); + const outDir = join(dir, `${name}-out`); + await runGen({ + config: defineConfig({ outDir, dialect: "postgres", apiPrefix: "/api", generators: [openApiFile({ title: "Customers", version: "1.0.0" })] }), + metadata: result.root, + genStateDir: join(dir, `${name}-state`), + }); + const target = join(dir, `${name}.json`); + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, readFileSync(join(outDir, "openapi.json"), "utf8")); +} + +const runner = findRunner(); + +describe.skipIf(runner === undefined)("a change to the model, through oasdiff", () => { + // WARN, not ERR: oasdiff grades a removed query parameter a warning, because most APIs ignore one + // they do not know. These routes answer it with a 400, so here it breaks a consumer. + const breakingArgs = ["breaking", "base.json", "revision.json", "--fail-on", "WARN"]; + + async function compare(revision: unknown): Promise<{ status: number | null; output: string }> { + const dir = mkdtempSync(join(resolve(tmpdir()), "contract-oasdiff-")); + try { + await emitDocument(BASE_META, dir, "base"); + await emitDocument(revision, dir, "revision"); + const { status, stdout, stderr } = runner!(breakingArgs, dir); + return { status, output: stdout + stderr }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } + } + + test("oasdiff reads the document and finds nothing against itself", async () => { + const { status, output } = await compare(BASE_META); + expect(output).not.toContain("error:"); + expect(status).toBe(0); + }, { timeout: TIMEOUT_MS }); + + test("adding an optional field is compatible", async () => { + const added = customerWith((children) => [ + ...children.slice(0, 3), + { "field.string": { name: "city", "@maxLength": 60 } }, + ...children.slice(3), + ]); + const { status, output } = await compare(added); + expect(output).not.toContain("error:"); + expect(status).toBe(0); + }, { timeout: TIMEOUT_MS }); + + test("a list keeps its components, so a new field is no change to its oneOf", async () => { + const { output } = await compare(customerWith((children) => [ + ...children.slice(0, 3), + { "field.string": { name: "city", "@maxLength": 60 } }, + ...children.slice(3), + ])); + expect(output).not.toContain("one-of"); + }, { timeout: TIMEOUT_MS }); + + test("removing a field breaks consumers, and oasdiff says which", async () => { + const removed = customerWith((children) => children.filter((child) => child["field.string"]?.["name"] !== "nickname")); + const { status, output } = await compare(removed); + expect(status).toBe(1); + expect(output).toContain("nickname"); + }, { timeout: TIMEOUT_MS }); + + test("a new required field breaks consumers: every existing create is refused", async () => { + const required = customerWith((children) => [ + ...children.slice(0, 3), + { "field.string": { name: "city", "@required": true, "@maxLength": 60 } }, + ...children.slice(3), + ]); + const { status, output } = await compare(required); + expect(status).toBe(1); + expect(output).toContain("city"); + }, { timeout: TIMEOUT_MS }); + + test("tightening a bound breaks consumers: a valid request is now refused", async () => { + const tightened = customerWith((children) => children.map((child) => { + const field = child["field.string"]; + return field?.["name"] === "nickname" ? { "field.string": { ...field, "@maxLength": 10 } } : child; + })); + const { status, output } = await compare(tightened); + expect(status).toBe(1); + expect(output).toContain("max-length-decreased"); + }, { timeout: TIMEOUT_MS }); + + test("a new entity breaks nobody", async () => { + const withOrder = customerWith((children) => children); + const root = withOrder as typeof BASE_META; + root["metadata.root"].children.push({ + "object.entity": { + name: "Invoice", + children: [ + { "source.rdb": { "@table": "invoices" } }, + { "field.long": { name: "id" } }, + { "identity.primary": { name: "pk", "@fields": "id", "@generation": "increment" } }, + ], + }, + } as never); + const { status, output } = await compare(withOrder); + expect(output).not.toContain("error:"); + expect(status).toBe(0); + }, { timeout: TIMEOUT_MS }); + + test("removing a filter breaks consumers: the document holds the query surface too", async () => { + const unfiltered = customerWith((children) => children.map((child) => { + const field = child["field.string"]; + if (field?.["name"] !== "name") return child; + const { "@filterable": _dropped, ...rest } = field; + return { "field.string": rest }; + })); + const { status, output } = await compare(unfiltered); + expect(status).toBe(1); + expect(output).toContain("filter[name]"); + }, { timeout: TIMEOUT_MS }); +}); diff --git a/spec/decisions/ADR-0034-codegen-scaffold-and-own.md b/spec/decisions/ADR-0034-codegen-scaffold-and-own.md index d65f7bda12..7a50e45b70 100644 --- a/spec/decisions/ADR-0034-codegen-scaffold-and-own.md +++ b/spec/decisions/ADR-0034-codegen-scaffold-and-own.md @@ -1203,3 +1203,27 @@ policy for this release, recorded here so the CHANGELOG's breaking notice can ci **Open.** Nothing. The C# file name was ruled 2026-10-14: the list lives in `codegen/Generators.cs`, and `codegen/Program.cs` is a two-line entry point. + +### Amendment 10 (2026-10-30) — the contract emitters are stock generators + +**Ruling (maintainer).** Amendment 4 item 4 made JSON Schema and OpenAPI recipes and "not a product +surface". Round 7 of the positioning work found the missing emitter was the condition API-first +teams name for adopting, and the "fourth source of truth" objection every time they read it. The +maintainer decided the emitter is the next feature (FR-022). + +**The rule.** + +1. `contract-schema` (`jsonSchemaFile()`) and `openapi` (`openApiFile()`) are stock generators in + the catalog, `tier: "neutral"`, `eject: "full"`. +2. They are still reference helpers under Amendment 3: listed, importable, ejectable, gated by + `verify --codegen`. What the core guarantees has not changed. A defect in one is a defect in a + reference, not in a core guarantee. +3. One engine, in TypeScript, for all five ports. Their output depends on the model and the `wire` + setting and never on the port, so a project on any port runs them through the Node `meta` CLI. + That is ADR-0022's test for a neutral artifact, and the manifest records it as + `ports: ["typescript"]` with a note. + +**Amendment 4 stands** for everything else: writing a generator is the primary path. The two recipes +stay as worked examples of writing one, and say that a stock generator now exists. + +**Packaged output.** Two new generators. No existing generator emits different bytes. diff --git a/spec/roadmap.md b/spec/roadmap.md index 65c98d5a35..1a5276800b 100644 --- a/spec/roadmap.md +++ b/spec/roadmap.md @@ -141,7 +141,7 @@ under **Shipped**; planned FRs under **Planned** + the **Release plan**. ✅ shi | FR-019 | Shared / externally-provided enums | 📋 | 1.1 | [#5](https://github.com/metaobjectsdev/metaobjects/issues/5) | | FR-020 | `@inheritance` joined strategy | 📋 | 1.x | [#6](https://github.com/metaobjectsdev/metaobjects/issues/6) | | FR-021 | api metadata + contract projections | 📋 | 1.3 | [#7](https://github.com/metaobjectsdev/metaobjects/issues/7) | -| FR-022 | Contract emitters (JSON Schema/OpenAPI/protobuf) | 📋 | 1.3 | [#8](https://github.com/metaobjectsdev/metaobjects/issues/8) | +| FR-022 | Contract emitters (JSON Schema/OpenAPI/protobuf) | 🔵 JSON Schema and OpenAPI 1.2.1 · protobuf 1.3 | 1.2.1 | [#8](https://github.com/metaobjectsdev/metaobjects/issues/8) | | FR-023 | Metadata dependencies (cross-repo sharing: generated shared-model artifact, `meta deps sync`, committed snapshot + lock, default exclusion of imported metadata, consumer extend/overlay across the boundary) | 🟢 Phase 1a (TS + Python) **shipped** per DESIGN §11, Java/Kotlin consume (Phase 2 S3) — the usage-aware breaking-change classifier and the `npm`/`python` transports are deferred (§11.4); C# consumer shipped in Phase 2 S6 | 1.1 | [#9](https://github.com/metaobjectsdev/metaobjects/issues/9) | | FR-024 | Entity surfaces (projection/value + declared API) | 🟢 taxonomy + B3/B4a/B4b/B5/B6 validation parity + projection codegen **shipped 5 ports (1.0)**; declared-API surface (`api.operational`/`operation`/`binding.rest`) **deferred to a later 1.x** (not in 1.1.0; foundation for FR-034) | 1.0 · 1.x (later) | [#10](https://github.com/metaobjectsdev/metaobjects/issues/10) | | FR-025 | Cross-port package-binding codegen config | 📋 | 1.1 | [#11](https://github.com/metaobjectsdev/metaobjects/issues/11) | @@ -405,7 +405,7 @@ _(FR-032 was developed under the working number "FR-026" — see commit history; - **FR-024 — Entity surfaces: `object.projection`, universal field-`extends`, and the declared API.** A third object subtype (derived, read-only, borrowed identity via `extends`; the declared field set IS the exposure — fail-closed), universal `Entity.child` extends-resolution (load-time drift gate on every contract shape), `@via` single-hop-unique inference, multi-source entity view behavior, and the `api.operational` / `operation.query|command` / `binding.rest` surface — across all 5 ports, conformance-gated; the two pre-taxonomy spellings (entity-`extends`-entity views, proc-results-as-entities) are removed outright (hard cutover, pre-GA). Decisions: [ADR-0028](decisions/ADR-0028-object-taxonomy-projection-value-purity.md) / [ADR-0029](decisions/ADR-0029-entity-child-extends-and-via-inference.md) / [ADR-0030](decisions/ADR-0030-declared-api-surface-and-org-tier-boundary.md). Design: `docs/superpowers/specs/2026-06-12-fr-024-entity-surfaces-projections-design.md`; program plan: `docs/superpowers/plans/2026-06-12-fr-024-entity-surfaces-program.md`. Supersedes the shape-vocabulary half of the FR-021 sketch (its `api`/`wireId` direction stands, retyped onto projection/value). - **FR-021 — `api` metadata type + contract projections.** Declared API surfaces (operations + per-protocol bindings) over the derived-CRUD default; operation payloads are declared contract shapes (post-#210: an `object.value` carries `origin.passthrough` lineage only — assembly origins live on `object.projection`, the same machinery as prompts and views); wire-stable `wireId` numbering lives on the contract projection (never the entity) so domain evolution can't break wire compat. Design sketch: `docs/superpowers/specs/2026-06-11-fr-021-api-metadata-and-contract-projections-design.md`. **Revised by FR-024:** contract shapes are `object.projection` (query outputs) / `object.value` (command inputs); subtype vocabulary `api.base`/`api.operational`; see ADR-0030. - **FR-034 — Ecosystem tier: connecting meta-modeled systems over a network.** A semantic topology layer that models the *ecosystem* — many meta-modeled systems and how they call each other — rather than a single system's shapes. C4-style two-level nesting: `system` (logical software system) → `container.service|client` (the deployable network participants), each carrying `surface.provided|consumed` edges whose `@apiRef` resolves (FQN, ADR-0041) to an `api.*` node in the same or a dependency package; plus `environment.deployment` (the named instantiation that keys a generated per-environment address-config matrix). URLs are **not** metadata (a generated, fail-closed *address contract* keyed per environment carries them); the logical topology moves **into** core (superseding ADR-0030 §5) while the physical tier stays permanently out. Reuses FR-023 (metadata-package transport), ADR-0041 (resolution), and ADR-0029 (the load-time drift gate, now spanning repos → cross-system drift detection = pillar 3, one tier up). Depends on the FR-024 declared-API surface. Owner rulings (2026-07-10): C4-nesting, topology-into-core, names-only environments, defer third-party. **Draft, deferred post-1.0.** Design: `docs/superpowers/specs/2026-07-10-fr-034-ecosystem-tier-connected-systems-design.md`. Downstream convergence evidence (a consumer independently re-deriving the consumed-edge address contract + protocol-off-the-type-axis): `spec/design-docs/2026-07-11-downstream-convergence-outbound-adapters.md`. -- **FR-022 — Contract emitters (Tier-2): JSON Schema 2020-12 (canonical + strict structured-output profile) → OpenAPI 3.1 → protobuf.** One shared neutral engine per ADR-0020; strict profile targets the cross-provider structured-output intersection (also MCP input schemas); proto emission consumes FR-021 `wireId`s, maps types per the locked table (Decimal/minor-units/uuid-string/optional/UNSPECIFIED-enum-zero), and inherits wire-compat regression detection via `buf breaking` on the emitted artifacts. Design sketch: `docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md`. +- **FR-022 — Contract emitters (Tier-2): JSON Schema 2020-12 (canonical + strict structured-output profile) → OpenAPI 3.1 → protobuf.** One shared neutral engine per ADR-0022; strict profile targets the cross-provider structured-output intersection (also MCP input schemas). The JSON Schema and OpenAPI phases target 1.2.1. Proto emission stays under 1.3 with binary protocols: it maps types per the locked table (Decimal/minor-units/uuid-string/optional/UNSPECIFIED-enum-zero) and inherits wire-compat regression detection via `buf breaking` on the emitted artifacts. Design: `docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md`. - **FR-023 — Metadata dependencies: cross-repo distribution + reuse.** **Phase 1a shipped** (TS + Python, per DESIGN §11's "B+" ruling — thin dependencies plus default exclusion of imported metadata): a publisher's `sharedModelFile()` @@ -676,12 +676,17 @@ the breaking half at `1.1`. JSON/XML download via the 1.1 serializer & field-subset + export conformance + the secondary grid gaps (column width/visibility/selection, consistent server `search`). -#### 1.3 — Binary protocols + contract emitters +#### 1.2.1 — Contract emitters + +- **FR-022 — JSON Schema and OpenAPI emitters**, with the oasdiff recipe that checks a change + against the last release. + +#### 1.3 — Binary protocols - **FR-030 binary** — protobuf / MessagePack / CBOR over the SPI → completes the **json → xml → binary → json** no-data-loss round-trip gate. -- **FR-022 — contract emitters** (JSON Schema / OpenAPI / protobuf) — overlaps the protobuf - serializer (shared `wireId`/type mapping); do together. +- **FR-022 — protobuf emitter** — overlaps the protobuf serializer (shared `wireId`/type + mapping); do together. #### 1.4 — Metadata API + runtime-driven UI From 16a3bbb12235dcda0155f6c0c86a7b40d40eef95 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sat, 31 Oct 2026 00:55:56 -0400 Subject: [PATCH 2/6] fix(codegen): keep emitted contract documents valid (FR-022) Findings from the pre-gate review: - withNull no longer repeats "null" on a schema that already allows it, which wrote a duplicate into `type` and `enum` in strict schemas. - OpenAPI path $refs now queue their shapes, so a generator filter cannot leave a traversal target or a TPH subtype's write shape dangling. - A duplicate operationId is refused, as a component name collision is. - An enum with no values stays an open string, not `enum: []`. - The oasdiff test probes the image rather than the Docker daemon, and the traffic check escapes literal path segments. --- docs/CONFORMANCE.md | 2 +- docs/features/contract-emitters.md | 2 +- fixtures/contract-conformance/README.md | 2 + .../expected-error.txt | 1 + .../error-operation-id-collision/meta.json | 134 ++++ .../nullable-members/expected/openapi.json | 644 ++++++++++++++++++ .../acme/contract/Place.create.schema.json | 15 + .../schemas/acme/contract/Place.schema.json | 14 + .../acme/contract/Place.strict.schema.json | 14 + .../acme/contract/Reading.create.schema.json | 21 + .../schemas/acme/contract/Reading.schema.json | 35 + .../acme/contract/Reading.strict.schema.json | 45 ++ .../acme/contract/Sensor.create.schema.json | 11 + .../schemas/acme/contract/Sensor.schema.json | 26 + .../acme/contract/Sensor.strict.schema.json | 57 ++ .../acme/contract/Sensor.update.schema.json | 18 + .../acme/contract/SensorSummary.schema.json | 26 + .../contract/SensorSummary.strict.schema.json | 60 ++ .../nullable-members/meta.json | 109 +++ .../codegen-ts/src/contract/build-openapi.ts | 103 ++- .../codegen-ts/src/contract/field-schema.ts | 14 +- .../codegen-ts/src/contract/object-schema.ts | 18 +- .../packages/codegen-ts/src/index.ts | 9 +- .../test/contract/contract-behaviours.test.ts | 25 +- .../test/contract/contract-corpus.test.ts | 7 +- .../test/contract/contract-validators.ts | 18 + .../src/api-contract-openapi-check.ts | 3 +- .../test/contract-oasdiff.test.ts | 3 +- 28 files changed, 1385 insertions(+), 51 deletions(-) create mode 100644 fixtures/contract-conformance/error-operation-id-collision/expected-error.txt create mode 100644 fixtures/contract-conformance/error-operation-id-collision/meta.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/openapi.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.create.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.strict.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.create.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.strict.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.create.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.strict.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.update.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.strict.schema.json create mode 100644 fixtures/contract-conformance/nullable-members/meta.json diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 3b3fb43ccb..d042037bbe 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -60,7 +60,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | [`fixtures/wire-conformance/`](../fixtures/wire-conformance/) (the `wire` setting, ADR-0019 Amendment 2) | 6 cases + 8 refusals | ✓ | ✓ | inherits via Java (one config reader) | ✓ | ✓ | | [`fixtures/codegen-noop/`](../fixtures/codegen-noop/) (FR-044 — reporting vocabulary: what is lowered, what stays inert) | 1 model pair (`reporting/with` vs `reporting/without`) | ✓ (codegen + migrate) | ✓ | ✓ | ✓ | ✓ | | [`fixtures/cube-model/`](../fixtures/cube-model/) (FR-044 Plan 4 — the `cube-model` generator's golden Cube files, plus the canonical model's golden) | 46 cases (36 trees + 10 errors) + 1 canonical golden | ✓ (the exporter is TypeScript-only, spec R6; its `cube` lane also loads the files into a real Cube) | — | — | — | — | -| [`fixtures/contract-conformance/`](../fixtures/contract-conformance/) (FR-022 — the `contract-schema` and `openapi` generators' golden trees and refusals) | 16 cases (12 trees + 4 errors) | ✓ (one neutral engine, ADR-0034 Amendment 10; the generated API lanes also hold real traffic to the documents) | — | — | — | — | +| [`fixtures/contract-conformance/`](../fixtures/contract-conformance/) (FR-022 — the `contract-schema` and `openapi` generators' golden trees and refusals) | 18 cases (13 trees + 5 errors) | ✓ (one neutral engine, ADR-0034 Amendment 10; the generated API lanes also hold real traffic to the documents) | — | — | — | — | A ✓ means the port runs the corpus green; an explicit `n / m` is used where a port carries a ledgered divergence. The two ledgered YAML fixtures are documented diff --git a/docs/features/contract-emitters.md b/docs/features/contract-emitters.md index e85ac28a2c..affbe323d7 100644 --- a/docs/features/contract-emitters.md +++ b/docs/features/contract-emitters.md @@ -221,5 +221,5 @@ removed field or filter fails: [Check an API change against the last release](.. | Code | Meaning | Fix | |---|---|---| -| `ERR_CONTRACT_NAME_COLLISION` | Two things would take one component name, such as an object `OrderCreate` beside the create shape of `Order` | Rename one, or leave one out with `filter` | +| `ERR_CONTRACT_NAME_COLLISION` | Two things would take one component name, such as an object `OrderCreate` beside the create shape of `Order`, or one `operationId`, such as the `tags` traversal of `Post` beside an object `PostTags` | Rename one, or leave one out with `filter` | | `ERR_CONTRACT_STRICT_UNREPRESENTABLE` | The strict subset cannot state an object | Leave it out of the strict entry with `filter` | diff --git a/fixtures/contract-conformance/README.md b/fixtures/contract-conformance/README.md index 76d631643e..a3d81606a3 100644 --- a/fixtures/contract-conformance/README.md +++ b/fixtures/contract-conformance/README.md @@ -33,10 +33,12 @@ document for that corpus. | `decimal` | The api-contract `decimal/` model. | | `error-name-collision` | Refused: a real object takes the name of another object's create shape. | | `error-name-collision-list` | Refused: a real object takes the name of another object's list response. | +| `error-operation-id-collision` | Refused: an object's list route takes the `operationId` of another object's traversal. | | `error-strict-map` | Refused: the strict subset has no map. | | `error-strict-recursive` | Refused: the strict subset has no recursion. | | `jsonb` | The api-contract `jsonb/` model: value objects, arrays of them and maps. | | `m2m` | The api-contract `m2m/` model: traversal routes, also over TPH. | +| `nullable-members` | Read-only shapes whose optional members are already nullable: a value object's, an enum's and a nested object's. | | `projection` | The api-contract `projection/` model: read-only surfaces, keyed and keyless. | | `report` | The api-contract `report/` model: served reports, and the rest of the model around them. | | `secret-fields` | The api-contract `secret-fields/` model: a credential is not on the wire. | diff --git a/fixtures/contract-conformance/error-operation-id-collision/expected-error.txt b/fixtures/contract-conformance/error-operation-id-collision/expected-error.txt new file mode 100644 index 0000000000..d53b923d4b --- /dev/null +++ b/fixtures/contract-conformance/error-operation-id-collision/expected-error.txt @@ -0,0 +1 @@ +[ERR_CONTRACT_NAME_COLLISION] the OpenAPI operationId 'listPostTags' is both GET /api/posts/{id}/tags and GET /api/post_tags. Rename one, or leave one out with the generator's filter option. diff --git a/fixtures/contract-conformance/error-operation-id-collision/meta.json b/fixtures/contract-conformance/error-operation-id-collision/meta.json new file mode 100644 index 0000000000..e6a91530e0 --- /dev/null +++ b/fixtures/contract-conformance/error-operation-id-collision/meta.json @@ -0,0 +1,134 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { + "object.entity": { + "name": "Post", + "children": [ + { + "source.rdb": { + "@table": "posts" + } + }, + { + "field.long": { + "name": "id" + } + }, + { + "relationship.association": { + "name": "tags", + "@cardinality": "many", + "@objectRef": "Tag", + "@through": "PostLabel" + } + }, + { + "identity.primary": { + "name": "id", + "@fields": "id", + "@generation": "increment" + } + } + ] + } + }, + { + "object.entity": { + "name": "Tag", + "children": [ + { + "source.rdb": { + "@table": "tags" + } + }, + { + "field.long": { + "name": "id" + } + }, + { + "identity.primary": { + "name": "id", + "@fields": "id", + "@generation": "increment" + } + } + ] + } + }, + { + "object.entity": { + "name": "PostLabel", + "children": [ + { + "source.rdb": { + "@table": "post_labels" + } + }, + { + "field.long": { + "name": "postId", + "@required": true + } + }, + { + "field.long": { + "name": "tagId", + "@required": true + } + }, + { + "identity.primary": { + "name": "id", + "@fields": [ + "postId", + "tagId" + ] + } + }, + { + "identity.reference": { + "name": "fkPost", + "@fields": "postId", + "@references": "Post" + } + }, + { + "identity.reference": { + "name": "fkTag", + "@fields": "tagId", + "@references": "Tag" + } + } + ] + } + }, + { + "object.entity": { + "name": "PostTags", + "children": [ + { + "source.rdb": { + "@table": "post_tag_counts" + } + }, + { + "field.long": { + "name": "id" + } + }, + { + "identity.primary": { + "name": "id", + "@fields": "id", + "@generation": "increment" + } + } + ] + } + } + ] + } +} diff --git a/fixtures/contract-conformance/nullable-members/expected/openapi.json b/fixtures/contract-conformance/nullable-members/expected/openapi.json new file mode 100644 index 0000000000..fb552752b5 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/openapi.json @@ -0,0 +1,644 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "nullable-members", + "version": "1.0.0" + }, + "paths": { + "/api/sensors": { + "get": { + "operationId": "listSensor", + "tags": [ + "Sensor" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SensorList" + }, + { + "$ref": "#/components/schemas/SensorPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createSensor", + "tags": [ + "Sensor" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SensorCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sensor" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sensors/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getSensor", + "tags": [ + "Sensor" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sensor" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateSensor", + "tags": [ + "Sensor" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SensorUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sensor" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putSensor", + "tags": [ + "Sensor" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SensorUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sensor" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteSensor", + "tags": [ + "Sensor" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sensor_summaries": { + "get": { + "operationId": "listSensorSummary", + "tags": [ + "SensorSummary" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SensorSummaryList" + }, + { + "$ref": "#/components/schemas/SensorSummaryPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/sensor_summaries/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getSensorSummary", + "tags": [ + "SensorSummary" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SensorSummary" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Place": { + "title": "Place", + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ] + }, + "PlaceCreate": { + "title": "PlaceCreate", + "type": "object", + "properties": { + "city": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "city" + ] + }, + "Reading": { + "title": "Reading", + "type": "object", + "properties": { + "unit": { + "type": [ + "string", + "null" + ] + }, + "grade": { + "type": [ + "string", + "null" + ], + "enum": [ + "LOW", + "HIGH", + null + ] + }, + "place": { + "anyOf": [ + { + "$ref": "#/components/schemas/Place" + }, + { + "type": "null" + } + ] + } + } + }, + "ReadingCreate": { + "title": "ReadingCreate", + "type": "object", + "properties": { + "unit": { + "type": "string" + }, + "grade": { + "type": "string", + "enum": [ + "LOW", + "HIGH" + ] + }, + "place": { + "$ref": "#/components/schemas/PlaceCreate" + } + } + }, + "Sensor": { + "title": "Sensor", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reading": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reading" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "reading" + ] + }, + "SensorCreate": { + "title": "SensorCreate", + "type": "object", + "properties": { + "reading": { + "$ref": "#/components/schemas/ReadingCreate" + } + } + }, + "SensorList": { + "title": "SensorList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Sensor" + } + }, + "SensorPage": { + "title": "SensorPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sensor" + } + }, + "total": { + "type": "integer" + } + } + }, + "SensorSummary": { + "title": "SensorSummary", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reading": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reading" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "reading" + ] + }, + "SensorSummaryList": { + "title": "SensorSummaryList", + "type": "array", + "items": { + "$ref": "#/components/schemas/SensorSummary" + } + }, + "SensorSummaryPage": { + "title": "SensorSummaryPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SensorSummary" + } + }, + "total": { + "type": "integer" + } + } + }, + "SensorUpdate": { + "title": "SensorUpdate", + "type": "object", + "properties": { + "reading": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReadingCreate" + }, + { + "type": "null" + } + ] + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.create.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.create.schema.json new file mode 100644 index 0000000000..29bd817937 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.create.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Place.create.schema.json", + "title": "Place.create", + "type": "object", + "properties": { + "city": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "city" + ] +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.schema.json new file mode 100644 index 0000000000..5bc7f5794e --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Place.schema.json", + "title": "Place", + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ] +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.strict.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.strict.schema.json new file mode 100644 index 0000000000..1eea5e9a9a --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Place.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "Place", + "type": "object", + "properties": { + "city": { + "description": "Minimum length: 1.", + "type": "string" + } + }, + "required": [ + "city" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.create.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.create.schema.json new file mode 100644 index 0000000000..f18a686fab --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.create.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Reading.create.schema.json", + "title": "Reading.create", + "type": "object", + "properties": { + "unit": { + "type": "string" + }, + "grade": { + "type": "string", + "enum": [ + "LOW", + "HIGH" + ] + }, + "place": { + "$ref": "./Place.create.schema.json" + } + } +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.schema.json new file mode 100644 index 0000000000..5dd3726038 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Reading.schema.json", + "title": "Reading", + "type": "object", + "properties": { + "unit": { + "type": [ + "string", + "null" + ] + }, + "grade": { + "type": [ + "string", + "null" + ], + "enum": [ + "LOW", + "HIGH", + null + ] + }, + "place": { + "anyOf": [ + { + "$ref": "./Place.schema.json" + }, + { + "type": "null" + } + ] + } + } +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.strict.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.strict.schema.json new file mode 100644 index 0000000000..2318ff31a5 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Reading.strict.schema.json @@ -0,0 +1,45 @@ +{ + "title": "Reading", + "type": "object", + "properties": { + "unit": { + "type": [ + "string", + "null" + ] + }, + "grade": { + "type": [ + "string", + "null" + ], + "enum": [ + "LOW", + "HIGH", + null + ] + }, + "place": { + "type": [ + "object", + "null" + ], + "properties": { + "city": { + "description": "Minimum length: 1.", + "type": "string" + } + }, + "required": [ + "city" + ], + "additionalProperties": false + } + }, + "required": [ + "unit", + "grade", + "place" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.create.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.create.schema.json new file mode 100644 index 0000000000..70ea324318 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.create.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Sensor.create.schema.json", + "title": "Sensor.create", + "type": "object", + "properties": { + "reading": { + "$ref": "./Reading.create.schema.json" + } + } +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.schema.json new file mode 100644 index 0000000000..b626699a10 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Sensor.schema.json", + "title": "Sensor", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reading": { + "anyOf": [ + { + "$ref": "./Reading.schema.json" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "reading" + ] +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.strict.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.strict.schema.json new file mode 100644 index 0000000000..96cd755b1f --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.strict.schema.json @@ -0,0 +1,57 @@ +{ + "title": "Sensor", + "type": "object", + "properties": { + "reading": { + "type": [ + "object", + "null" + ], + "properties": { + "unit": { + "type": [ + "string", + "null" + ] + }, + "grade": { + "type": [ + "string", + "null" + ], + "enum": [ + "LOW", + "HIGH", + null + ] + }, + "place": { + "type": [ + "object", + "null" + ], + "properties": { + "city": { + "description": "Minimum length: 1.", + "type": "string" + } + }, + "required": [ + "city" + ], + "additionalProperties": false + } + }, + "required": [ + "unit", + "grade", + "place" + ], + "additionalProperties": false + } + }, + "required": [ + "reading" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.update.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.update.schema.json new file mode 100644 index 0000000000..c9c9555132 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/Sensor.update.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/Sensor.update.schema.json", + "title": "Sensor.update", + "type": "object", + "properties": { + "reading": { + "anyOf": [ + { + "$ref": "./Reading.create.schema.json" + }, + { + "type": "null" + } + ] + } + } +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.schema.json new file mode 100644 index 0000000000..0e7ebda7c2 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/contract/SensorSummary.schema.json", + "title": "SensorSummary", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "reading": { + "anyOf": [ + { + "$ref": "./Reading.schema.json" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "reading" + ] +} diff --git a/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.strict.schema.json b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.strict.schema.json new file mode 100644 index 0000000000..87c2a4c364 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/expected/schemas/acme/contract/SensorSummary.strict.schema.json @@ -0,0 +1,60 @@ +{ + "title": "SensorSummary", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "reading": { + "type": [ + "object", + "null" + ], + "properties": { + "unit": { + "type": [ + "string", + "null" + ] + }, + "grade": { + "type": [ + "string", + "null" + ], + "enum": [ + "LOW", + "HIGH", + null + ] + }, + "place": { + "type": [ + "object", + "null" + ], + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ], + "additionalProperties": false + } + }, + "required": [ + "unit", + "grade", + "place" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "reading" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/nullable-members/meta.json b/fixtures/contract-conformance/nullable-members/meta.json new file mode 100644 index 0000000000..2bda839915 --- /dev/null +++ b/fixtures/contract-conformance/nullable-members/meta.json @@ -0,0 +1,109 @@ +{ + "metadata.root": { + "package": "acme::contract", + "children": [ + { + "object.value": { + "name": "Place", + "children": [ + { + "field.string": { + "name": "city", + "@required": true + } + } + ] + } + }, + { + "object.value": { + "name": "Reading", + "children": [ + { + "field.string": { + "name": "unit" + } + }, + { + "field.enum": { + "name": "grade", + "@values": [ + "LOW", + "HIGH" + ] + } + }, + { + "field.object": { + "name": "place", + "@objectRef": "Place" + } + } + ] + } + }, + { + "object.entity": { + "name": "Sensor", + "children": [ + { + "source.rdb": { + "@table": "sensors" + } + }, + { + "field.long": { + "name": "id" + } + }, + { + "field.object": { + "name": "reading", + "@objectRef": "Reading", + "@storage": "jsonb" + } + }, + { + "identity.primary": { + "name": "pk", + "@fields": "id", + "@generation": "increment" + } + } + ] + } + }, + { + "object.projection": { + "name": "SensorSummary", + "children": [ + { + "source.rdb": { + "@kind": "view", + "@view": "v_sensor_summary" + } + }, + { + "field.long": { + "name": "id", + "extends": "Sensor.id" + } + }, + { + "field.object": { + "name": "reading", + "extends": "Sensor.reading" + } + }, + { + "identity.primary": { + "name": "pk", + "extends": "Sensor.pk" + } + } + ] + } + } + ] + } +} diff --git a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts index ded2866d30..f0e4e1dbf5 100644 --- a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts +++ b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts @@ -10,7 +10,6 @@ import { type MetaField, type MetaObject, type MetaRoot, opsForField } from "@metaobjectsdev/metadata"; import { FIELD_ATTR_FILTERABLE, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_TIMESTAMP, FILTER_OP_IN, FILTER_OP_IS_NULL, FILTER_OP_LIKE } from "@metaobjectsdev/metadata/constants"; import { hasItemRoute, itemRouteField, servedPath, servesReadApi } from "../api-surface.js"; -import { enumValues } from "../enum-meta.js"; import { isProjection } from "../projection/projection-detector.js"; import type { RelationMap } from "../relation-resolver.js"; import { CRUD_VERBS, type CrudVerb, type ExposeOption, intersectExpose, resolveExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js"; @@ -19,8 +18,8 @@ import { sortableFields } from "../templates/filter-shared.js"; import { tphPlan } from "../templates/tph-discriminator.js"; import { isTphSubtype } from "../tph.js"; import { ContractEmitError, ERR_CONTRACT_NAME_COLLISION } from "./contract-errors.js"; -import { type ContractShape, DECIMAL_INPUT_PATTERN, type JsonSchema, scalarSchema, timestampInputSchema } from "./field-schema.js"; -import { collectContractEntries } from "./object-schema.js"; +import { type ContractShape, DECIMAL_INPUT_PATTERN, enumSchema, type JsonSchema, scalarSchema, timestampInputSchema } from "./field-schema.js"; +import { collectContractEntries, type ContractTarget } from "./object-schema.js"; export const OPENAPI_VERSION = "3.1.0"; @@ -76,6 +75,21 @@ function shapeRef(obj: MetaObject, shape: ContractShape): JsonSchema { return { $ref: componentRef(componentName(obj, shape)) }; } +/** + * What the paths point at, gathered as they are built. A path can name a shape nothing else in the + * document reaches, such as the target of a traversal when the generator's `filter` leaves it out. + */ +interface PathRefs { + readonly shapes: ContractTarget[]; + /** The objects a list route answers, for their list components. */ + readonly listed: Set; +} + +function pathRef(refs: PathRefs, obj: MetaObject, shape: ContractShape): JsonSchema { + refs.shapes.push({ obj, shape }); + return shapeRef(obj, shape); +} + function jsonContent(schema: JsonSchema): JsonSchema { return { "application/json": { schema } }; } @@ -95,7 +109,7 @@ function filterValueSchema(field: MetaField, op: string): JsonSchema { if (op === FILTER_OP_LIKE) return { type: "string" }; if (field.subType === FIELD_SUBTYPE_DECIMAL) return { type: "string", pattern: DECIMAL_INPUT_PATTERN }; if (field.subType === FIELD_SUBTYPE_TIMESTAMP) return timestampInputSchema(field); - if (field.subType === FIELD_SUBTYPE_ENUM) return { type: "string", enum: enumValues(field) ?? [] }; + if (field.subType === FIELD_SUBTYPE_ENUM) return enumSchema(field); return scalarSchema(field.subType); } @@ -176,8 +190,7 @@ interface ResourceOps { readonly report: boolean; readonly reportExport: boolean; readonly item: boolean; - /** Collects each object a list route answers, for its list components. */ - readonly listed: Set; + readonly refs: PathRefs; } function listOperation(r: ResourceOps): JsonSchema { @@ -188,7 +201,8 @@ function listOperation(r: ResourceOps): JsonSchema { ...pageParameters(r.report ? 1 : 0), ...(r.reportExport ? [{ name: "format", in: "query", schema: { type: "string", enum: ["csv", "json", "xml"] } }] : []), ]; - r.listed.add(row); + r.refs.listed.add(row); + r.refs.shapes.push({ obj: row, shape: "read" }); const content = jsonContent(listSchema(row)); const exports = r.reportExport ? { "text/csv": { schema: { type: "string" } }, "application/xml": { schema: { type: "string" } } } : {}; return { @@ -199,17 +213,17 @@ function listOperation(r: ResourceOps): JsonSchema { }; } -function writeBody(row: MetaObject, shape: ContractShape): JsonSchema { - return { required: true, content: jsonContent(shapeRef(row, shape)) }; +function writeBody(r: ResourceOps, shape: ContractShape): JsonSchema { + return { required: true, content: jsonContent(pathRef(r.refs, r.row, shape)) }; } -function updateOperation(row: MetaObject, operationId: string): JsonSchema { +function updateOperation(r: ResourceOps, operationId: string): JsonSchema { return { operationId, - tags: [row.name], - requestBody: writeBody(row, "update"), + tags: [r.row.name], + requestBody: writeBody(r, "update"), responses: { - "200": { description: "The row.", content: jsonContent(shapeRef(row, "read")) }, + "200": { description: "The row.", content: jsonContent(pathRef(r.refs, r.row, "read")) }, "400": BAD_REQUEST, "404": NOT_FOUND, "409": CONFLICT, @@ -225,13 +239,13 @@ function itemOperations(r: ResourceOps): JsonSchema { get: { operationId: `get${row.name}`, tags, - responses: { "200": { description: "The row.", content: jsonContent(shapeRef(row, "read")) }, "404": NOT_FOUND }, + responses: { "200": { description: "The row.", content: jsonContent(pathRef(r.refs, row, "read")) }, "404": NOT_FOUND }, }, }), ...(verbs.has("update") && { - patch: updateOperation(row, `update${row.name}`), + patch: updateOperation(r, `update${row.name}`), // The contract's PUT is an alias of PATCH: a partial update, not a replacement. - put: updateOperation(row, `put${row.name}`), + put: updateOperation(r, `put${row.name}`), }), ...(verbs.has("delete") && { delete: { @@ -251,9 +265,9 @@ function collectionOperations(r: ResourceOps): JsonSchema { post: { operationId: `create${row.name}`, tags: [row.name], - requestBody: writeBody(row, "create"), + requestBody: writeBody(r, "create"), responses: { - "201": { description: "The new row.", content: jsonContent(shapeRef(row, "read")) }, + "201": { description: "The new row.", content: jsonContent(pathRef(r.refs, row, "read")) }, "400": BAD_REQUEST, "409": CONFLICT, }, @@ -272,7 +286,7 @@ function resourcePaths(r: ResourceOps): Record { } /** `GET /{id}/`: the M:N traversal, an array of the target's rows. */ -function m2mPaths(source: MetaObject, path: string, relationMap: RelationMap): Record { +function m2mPaths(source: MetaObject, path: string, relationMap: RelationMap, refs: PathRefs): Record { const paths: Record = {}; for (const entry of relationMap.get(source.name) ?? []) { if (entry.junctionEntity === undefined) continue; @@ -282,7 +296,7 @@ function m2mPaths(source: MetaObject, path: string, relationMap: RelationMap): R operationId: `list${source.name}${entry.name.charAt(0).toUpperCase()}${entry.name.slice(1)}`, tags: [source.name], responses: { - "200": { description: "The related rows.", content: jsonContent({ type: "array", items: shapeRef(entry.target, "read") }) }, + "200": { description: "The related rows.", content: jsonContent({ type: "array", items: pathRef(refs, entry.target, "read") }) }, }, }, }; @@ -298,39 +312,59 @@ function pathsOf( obj: MetaObject, input: OpenApiBuildInput, reportNames: ReadonlySet, - listed: Set, + refs: PathRefs, ): Record { const { root, apiPrefix, expose, relationMap } = input; const path = servedPath(obj, apiPrefix); const report = reportNames.has(obj.name); if (isProjection(obj)) { return resourcePaths({ - row: obj, path, report, reportExport: report && input.reportExport, item: hasItemRoute(obj), listed, + row: obj, path, report, reportExport: report && input.reportExport, item: hasItemRoute(obj), refs, verbs: new Set(["list", "get"]), }); } const plan = tphPlan(obj, root); if (plan === null) { return { - ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, listed, verbs: verbsOf(CRUD_VERBS, obj, expose) }), - ...m2mPaths(obj, path, relationMap), + ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, refs, verbs: verbsOf(CRUD_VERBS, obj, expose) }), + ...m2mPaths(obj, path, relationMap, refs), }; } const paths: Record = { - ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, listed, verbs: verbsOf(TPH_POLYMORPHIC_VERBS, obj, expose) }), - ...m2mPaths(obj, path, relationMap), + ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, refs, verbs: verbsOf(TPH_POLYMORPHIC_VERBS, obj, expose) }), + ...m2mPaths(obj, path, relationMap, refs), }; for (const { entity, routeSegment } of plan.subtypes) { const subPath = `${path}/${routeSegment}`; Object.assign(paths, resourcePaths({ - row: entity, path: subPath, report: false, reportExport: false, item: true, listed, + row: entity, path: subPath, report: false, reportExport: false, item: true, refs, verbs: verbsOf(CRUD_VERBS, obj, expose), excludeField: plan.discriminatorField, - }), m2mPaths(entity, subPath, relationMap)); + }), m2mPaths(entity, subPath, relationMap, refs)); } return paths; } -function collectSchemas(input: OpenApiBuildInput, listed: ReadonlySet): Record { +/** An `operationId` is unique in a document, and a client generator names a method after it. */ +function refuseDuplicateOperationIds(paths: Record): void { + const seen = new Map(); + for (const [path, item] of Object.entries(paths)) { + for (const [method, operation] of Object.entries(item)) { + const id = (operation as JsonSchema).operationId; + if (typeof id !== "string") continue; + const here = `${method.toUpperCase()} ${path}`; + const other = seen.get(id); + if (other !== undefined) { + throw new ContractEmitError( + ERR_CONTRACT_NAME_COLLISION, + `the OpenAPI operationId '${id}' is both ${other} and ${here}. Rename one, or leave one out with the generator's filter option.`, + ); + } + seen.set(id, here); + } + } +} + +function collectSchemas(input: OpenApiBuildInput, refs: PathRefs): Record { const taken = new Map([[ERROR_COMPONENT, "the error envelope"]]); const schemas: Record = {}; function claim(name: string, holder: string): void { @@ -347,14 +381,14 @@ function collectSchemas(input: OpenApiBuildInput, listed: ReadonlySet componentRef(componentName(target, shape)), - { openApiDiscriminator: true }, + { openApiDiscriminator: true, referenced: refs.shapes }, ); for (const { obj, shape, schema } of entries) { const name = componentName(obj, shape); claim(name, `'${obj.resolutionKey()}' (${shape} shape)`); schemas[name] = { title: name, ...schema }; } - for (const row of listed) { + for (const row of refs.listed) { for (const [name, schema] of Object.entries(listComponents(row))) { claim(name, `the list response of '${row.resolutionKey()}'`); schemas[name] = schema; @@ -380,8 +414,9 @@ export function buildOpenApiDocument(input: OpenApiBuildInput): JsonSchema { const objects = [...input.objects].sort(byName); const served = objects.filter((o) => servesReadApi(o) && !isTphSubtype(o) && (input.mounted?.(o) ?? true)); const paths: Record = {}; - const listed = new Set(); - for (const obj of served) Object.assign(paths, pathsOf(obj, input, reportNames, listed)); + const refs: PathRefs = { shapes: [], listed: new Set() }; + for (const obj of served) Object.assign(paths, pathsOf(obj, input, reportNames, refs)); + refuseDuplicateOperationIds(paths); return { openapi: OPENAPI_VERSION, info: { @@ -393,7 +428,7 @@ export function buildOpenApiDocument(input: OpenApiBuildInput): JsonSchema { ...(input.security !== undefined && { security: input.security }), paths, components: { - schemas: collectSchemas({ ...input, objects }, listed), + schemas: collectSchemas({ ...input, objects }, refs), ...(input.securitySchemes !== undefined && { securitySchemes: input.securitySchemes }), }, }; diff --git a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts index 06fb2ec011..389625c812 100644 --- a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts +++ b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts @@ -147,6 +147,12 @@ export function scalarSchema(subType: string): JsonSchema { } } +/** An enum as a string. With no values to state it stays open: `enum: []` would refuse everything. */ +export function enumSchema(field: MetaField): JsonSchema { + const values = enumValues(field); + return values === undefined || values.length === 0 ? { type: "string" } : { type: "string", enum: values }; +} + /** One value of a field that holds no reference to another object. */ function plainValueSchema(field: MetaField, shape: ContractShape): JsonSchema { const lenient = field.attr(FIELD_ATTR_LENIENT) === true; @@ -162,7 +168,7 @@ function plainValueSchema(field: MetaField, shape: ContractShape): JsonSchema { case FIELD_SUBTYPE_INET: return lenient ? { type: "string" } : scalarSchema(field.subType); case FIELD_SUBTYPE_ENUM: - return { type: "string", enum: enumValues(field) ?? [] }; + return enumSchema(field); case FIELD_SUBTYPE_CURRENCY: { const code = field.attr(FIELD_ATTR_CURRENCY); const unit = typeof code === "string" ? ` of ${code}` : ""; @@ -283,7 +289,7 @@ export function fieldSchema(field: MetaField, shape: ContractShape, refFor: RefF return schema; } -/** The schema that also accepts `null`. */ +/** The schema that also accepts `null`. Safe on one that already does: `type` entries must be unique. */ export function withNull(schema: JsonSchema): JsonSchema { if (Object.keys(schema).length === 0) return schema; // an open value already holds null if (schema.$ref !== undefined) { @@ -293,8 +299,8 @@ export function withNull(schema: JsonSchema): JsonSchema { const out: JsonSchema = { ...schema }; const type = schema.type; if (typeof type === "string") out.type = [type, "null"]; - else if (Array.isArray(type)) out.type = [...type, "null"]; + else if (Array.isArray(type) && !type.includes("null")) out.type = [...type, "null"]; // `enum` is checked on its own: without null in it, a nullable enum still refuses null. - if (Array.isArray(schema.enum)) out.enum = [...schema.enum, null]; + if (Array.isArray(schema.enum) && !schema.enum.includes(null)) out.enum = [...schema.enum, null]; return out; } diff --git a/server/typescript/packages/codegen-ts/src/contract/object-schema.ts b/server/typescript/packages/codegen-ts/src/contract/object-schema.ts index 73b8555521..ee9dd4609f 100644 --- a/server/typescript/packages/codegen-ts/src/contract/object-schema.ts +++ b/server/typescript/packages/codegen-ts/src/contract/object-schema.ts @@ -138,9 +138,13 @@ export function objectSchema(obj: MetaObject, shape: ContractShape, env: ObjectS return { ...(typeof description === "string" && { description }), ...body }; } -export interface ContractEntry { +/** One object in one wire shape. */ +export interface ContractTarget { readonly obj: MetaObject; readonly shape: ContractShape; +} + +export interface ContractEntry extends ContractTarget { readonly schema: JsonSchema; } @@ -155,14 +159,19 @@ export function collectContractEntries( objects: readonly MetaObject[], root: MetaRoot, refFrom: RefFrom, - options: { readonly openApiDiscriminator?: boolean } = {}, + options: { + readonly openApiDiscriminator?: boolean; + /** Shapes something outside these schemas points at, such as an OpenAPI path. */ + readonly referenced?: readonly ContractTarget[]; + } = {}, ): ContractEntry[] { - const queued = new Map(); + const queued = new Map(); const queue = (obj: MetaObject, shape: ContractShape): void => { const key = `${obj.resolutionKey()}#${shape}`; if (!queued.has(key)) queued.set(key, { obj, shape }); }; for (const obj of objects) for (const shape of shapesOf(obj, root)) queue(obj, shape); + for (const { obj, shape } of options.referenced ?? []) queue(obj, shape); const entries: ContractEntry[] = []; // A Map iterates what is added while it is walked, so this visits everything `refFor` queues. @@ -171,7 +180,8 @@ export function collectContractEntries( queue(target, targetShape); return refFrom(obj, target, targetShape); }; - entries.push({ obj, shape, schema: objectSchema(obj, shape, { root, refFor, ...options }) }); + const { openApiDiscriminator } = options; + entries.push({ obj, shape, schema: objectSchema(obj, shape, { root, refFor, ...(openApiDiscriminator !== undefined && { openApiDiscriminator }) }) }); } return entries; } diff --git a/server/typescript/packages/codegen-ts/src/index.ts b/server/typescript/packages/codegen-ts/src/index.ts index f5afab9c24..fb473b5739 100644 --- a/server/typescript/packages/codegen-ts/src/index.ts +++ b/server/typescript/packages/codegen-ts/src/index.ts @@ -429,9 +429,8 @@ export { withClientDirective, CLIENT_DIRECTIVE } from "./client-directive.js"; export { buildSharedModel, renderSharedModelFiles } from "./shared-model/build-shared-model.js"; export type { SharedModel, SharedModelFiles, SharedModelInput } from "./shared-model/build-shared-model.js"; -// FR-044 Plan 4 — the Cube exporter. `cubeModel()` is the reference generator; the two pure -// stages it wires are public so an owned (ejected) copy imports only this package and keeps -// only the parts it owns: which entities get a cube, the file layout and the YAML text. +// FR-022 — the contract emitters. `jsonSchemaFile()` and `openApiFile()` are the reference +// generators; what they wire is public so an owned (ejected) copy imports only this package. export { jsonSchemaFile } from "./generators/contract-schema-file.js"; export type { JsonSchemaFileOptions } from "./generators/contract-schema-file.js"; export { openApiFile } from "./generators/openapi-file.js"; @@ -444,6 +443,10 @@ export type { ContractShape, JsonSchema } from "./contract/field-schema.js"; export { strictSchema } from "./contract/strict-profile.js"; export { ContractEmitError, CONTRACT_ERROR_CODES, ERR_CONTRACT_NAME_COLLISION, ERR_CONTRACT_STRICT_UNREPRESENTABLE } from "./contract/contract-errors.js"; export type { ContractErrorCode } from "./contract/contract-errors.js"; + +// FR-044 Plan 4 — the Cube exporter. `cubeModel()` is the reference generator; the two pure +// stages it wires are public so an owned (ejected) copy imports only this package and keeps +// only the parts it owns: which entities get a cube, the file layout and the YAML text. export { cubeModel } from "./generators/cube-model.js"; export type { CubeModelGeneratorOptions } from "./generators/cube-model.js"; export { buildCubeModel, hasReportingVocabulary } from "./cube/build-cube-model.js"; diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts b/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts index 6dbc5e7790..ea189296d6 100644 --- a/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts +++ b/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts @@ -14,7 +14,7 @@ import { jsonSchemaFile, openApiFile } from "../../src/generators/index.js"; import { defineConfig, type Generator, runGen } from "../../src/index.js"; import { jsonSchemaFile as refJsonSchemaFile } from "../../src/reference/contract-schema.js"; import { openApiFile as refOpenApiFile } from "../../src/reference/openapi.js"; -import { ajvOver, type JsonDoc } from "./contract-validators.js"; +import { ajvOver, danglingRefs, type JsonDoc } from "./contract-validators.js"; const CASES = new Map(contractCases().map((c) => [c.name, c])); @@ -240,6 +240,29 @@ describe("what a project's run changes", () => { expect(Object.keys((doc["components"] as { schemas: JsonDoc }).schemas)).not.toContain("Bag"); }); + test("a filter that drops a traversal's target keeps the target's component", async () => { + const tree = await emit(corpusCase("m2m"), [openApiFile({ title: "t", version: "1", filter: (o) => o.name === "Post" })]); + const doc = jsonAt(tree, "openapi.json"); + expect(Object.keys(doc["paths"] as JsonDoc)).toContain("/api/posts/{id}/tags"); + expect(danglingRefs(doc)).toEqual([]); + }); + + test("a filter that keeps only a TPH base keeps each subtype's write shapes", async () => { + const tree = await emit(corpusCase("tph"), [openApiFile({ title: "t", version: "1", filter: (o) => o.name === "Auth" })]); + const doc = jsonAt(tree, "openapi.json"); + expect(Object.keys(doc["paths"] as JsonDoc).length).toBeGreaterThan(2); + expect(danglingRefs(doc)).toEqual([]); + }); + + test("an optional member that is already nullable is not made nullable twice", async () => { + const tree = await emit(corpusCase("nullable-members"), [jsonSchemaFile({ profile: "strict" })]); + const reading = (jsonAt(tree, "schemas/acme/contract/SensorSummary.strict.schema.json")["properties"] as Record }>)["reading"]!; + expect(reading.properties["unit"]!["type"]).toEqual(["string", "null"]); + expect(reading.properties["grade"]!["type"]).toEqual(["string", "null"]); + expect(reading.properties["grade"]!["enum"]).toEqual(["LOW", "HIGH", null]); + expect(reading.properties["place"]!["type"]).toEqual(["object", "null"]); + }); + test("a schema filter keeps a file for what a kept object references, so no $ref dangles", async () => { const c = corpusCase("all-fields"); const tree = await emit(c, [jsonSchemaFile({ filter: (o) => o.name === "AllFields" })]); diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts b/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts index bbfab2248d..4d8c1988ce 100644 --- a/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts +++ b/server/typescript/packages/codegen-ts/test/contract/contract-corpus.test.ts @@ -10,7 +10,7 @@ import { join } from "node:path"; import { ContractEmitError } from "../../src/contract/contract-errors.js"; import { CONTRACT_SCRIPT, contractCases, contractTree } from "../../scripts/gen-contract-conformance.js"; import { readTree } from "../../scripts/gen-cube-model-canonical.js"; -import { ajvOver, type JsonDoc, openApiErrors, strictViolations } from "./contract-validators.js"; +import { ajvOver, danglingRefs, type JsonDoc, openApiErrors, strictViolations } from "./contract-validators.js"; const STRICT_SUFFIX = ".strict.schema.json"; const OPENAPI_FILE = "openapi.json"; @@ -99,6 +99,11 @@ describe("the contract corpus", () => { } }); + test("every $ref in the document has a component behind it", async () => { + const doc = documents(await written, (p) => p === OPENAPI_FILE).get(OPENAPI_FILE)!; + expect(danglingRefs(doc)).toEqual([]); + }); + test("every operation has a unique operationId", async () => { const doc = documents(await written, (p) => p === OPENAPI_FILE).get(OPENAPI_FILE)!; const ids = Object.values(doc["paths"] as Record>) diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts b/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts index 9ed529f4e6..fdf1492267 100644 --- a/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts +++ b/server/typescript/packages/codegen-ts/test/contract/contract-validators.ts @@ -22,6 +22,24 @@ export async function openApiErrors(doc: JsonDoc): Promise { return result.valid ? [] : (result.errors as unknown[]); } +/** Every `$ref` in a document with no component behind it: empty when none dangles. */ +export function danglingRefs(doc: JsonDoc): string[] { + const schemas = (doc["components"] as { schemas: JsonDoc }).schemas; + const found = new Set(); + const walk = (value: unknown): void => { + if (Array.isArray(value)) value.forEach(walk); + else if (typeof value === "object" && value !== null) { + for (const [key, child] of Object.entries(value)) { + if (key === "$ref" && typeof child === "string") { + if (!(child.split("/").pop()! in schemas)) found.add(child); + } else walk(child); + } + } + }; + walk(doc); + return [...found]; +} + /** The only keywords a strict schema may use, anywhere in it. */ const STRICT_KEYWORDS: ReadonlySet = new Set([ "title", "description", "type", "properties", "required", "additionalProperties", "items", "enum", diff --git a/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts b/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts index 7b0fafdff4..2d992968e9 100644 --- a/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts +++ b/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts @@ -38,7 +38,8 @@ type PathItem = Record & { parameters?: Array<{ name: string; i export type ContractCheck = (req: ApiRequest, status: number, bodyText: string) => void; function templateRegex(template: string): RegExp { - return new RegExp(`^${template.split("/").map((part) => (part.startsWith("{") ? "[^/]+" : part)).join("/")}$`); + const literal = (part: string): string => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`^${template.split("/").map((part) => (part.startsWith("{") ? "[^/]+" : literal(part))).join("/")}$`); } /** A document's path templates, literal ones first: `/x/subtypes` wins over `/x/{id}`. */ diff --git a/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts b/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts index 9edfd9fea3..f74f27235f 100644 --- a/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts +++ b/server/typescript/packages/integration-tests/test/contract-oasdiff.test.ts @@ -32,7 +32,8 @@ function findRunner(): Runner | undefined { return { status: r.status, stdout: r.stdout, stderr: r.stderr }; }; } - if (!succeeds("docker", ["info"])) return undefined; + // Docker alone is not enough: the image has to be here, or pullable. + if (!succeeds("docker", ["run", "--rm", OASDIFF_IMAGE, "--version"])) return undefined; return (args, dir) => { const r = spawnSync("docker", ["run", "--rm", "-v", `${dir}:/work`, "-w", "/work", OASDIFF_IMAGE, ...args], { encoding: "utf8" }); return { status: r.status, stdout: r.stdout, stderr: r.stderr }; From 55995b05c4f6b31214748965b7e809473eba4323 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sat, 31 Oct 2026 01:01:00 -0400 Subject: [PATCH 3/6] refactor(codegen): simplify the OpenAPI path builder (FR-022) Share one comparator, drop a filter branch scalarSchema already covers, fold the TPH and plain path branches together, and compile the traffic check's validators once per pointer. Output is unchanged. --- .../codegen-ts/src/contract/build-openapi.ts | 22 +++++++++---------- .../codegen-ts/src/contract/field-schema.ts | 2 +- .../src/api-contract-openapi-check.ts | 7 +++++- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts index f0e4e1dbf5..1108b9be35 100644 --- a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts +++ b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts @@ -8,7 +8,7 @@ // caller can use (the api docs omit them for the same reason). import { type MetaField, type MetaObject, type MetaRoot, opsForField } from "@metaobjectsdev/metadata"; -import { FIELD_ATTR_FILTERABLE, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_TIMESTAMP, FILTER_OP_IN, FILTER_OP_IS_NULL, FILTER_OP_LIKE } from "@metaobjectsdev/metadata/constants"; +import { FIELD_ATTR_FILTERABLE, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_TIMESTAMP, FILTER_OP_IN, FILTER_OP_IS_NULL, FILTER_OP_LIKE } from "@metaobjectsdev/metadata/constants"; import { hasItemRoute, itemRouteField, servedPath, servesReadApi } from "../api-surface.js"; import { isProjection } from "../projection/projection-detector.js"; import type { RelationMap } from "../relation-resolver.js"; @@ -18,7 +18,7 @@ import { sortableFields } from "../templates/filter-shared.js"; import { tphPlan } from "../templates/tph-discriminator.js"; import { isTphSubtype } from "../tph.js"; import { ContractEmitError, ERR_CONTRACT_NAME_COLLISION } from "./contract-errors.js"; -import { type ContractShape, DECIMAL_INPUT_PATTERN, enumSchema, type JsonSchema, scalarSchema, timestampInputSchema } from "./field-schema.js"; +import { type ContractShape, enumSchema, type JsonSchema, scalarSchema, timestampInputSchema } from "./field-schema.js"; import { collectContractEntries, type ContractTarget } from "./object-schema.js"; export const OPENAPI_VERSION = "3.1.0"; @@ -59,8 +59,12 @@ export interface OpenApiBuildInput extends OpenApiDocumentOptions { readonly reportExport: boolean; } +function compareNames(a: string, b: string): number { + return a < b ? -1 : a > b ? 1 : 0; +} + function byName(a: MetaObject, b: MetaObject): number { - return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; + return compareNames(a.name, b.name); } function componentName(obj: MetaObject, shape: ContractShape): string { @@ -107,7 +111,6 @@ function filterValueSchema(field: MetaField, op: string): JsonSchema { if (op === FILTER_OP_IS_NULL) return { type: "boolean" }; if (op === FILTER_OP_IN) return { type: "string", description: "Comma-separated values." }; if (op === FILTER_OP_LIKE) return { type: "string" }; - if (field.subType === FIELD_SUBTYPE_DECIMAL) return { type: "string", pattern: DECIMAL_INPUT_PATTERN }; if (field.subType === FIELD_SUBTYPE_TIMESTAMP) return timestampInputSchema(field); if (field.subType === FIELD_SUBTYPE_ENUM) return enumSchema(field); return scalarSchema(field.subType); @@ -324,16 +327,11 @@ function pathsOf( }); } const plan = tphPlan(obj, root); - if (plan === null) { - return { - ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, refs, verbs: verbsOf(CRUD_VERBS, obj, expose) }), - ...m2mPaths(obj, path, relationMap, refs), - }; - } const paths: Record = { - ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, refs, verbs: verbsOf(TPH_POLYMORPHIC_VERBS, obj, expose) }), + ...resourcePaths({ row: obj, path, report: false, reportExport: false, item: true, refs, verbs: verbsOf(plan === null ? CRUD_VERBS : TPH_POLYMORPHIC_VERBS, obj, expose) }), ...m2mPaths(obj, path, relationMap, refs), }; + if (plan === null) return paths; for (const { entity, routeSegment } of plan.subtypes) { const subPath = `${path}/${routeSegment}`; Object.assign(paths, resourcePaths({ @@ -405,7 +403,7 @@ function collectSchemas(input: OpenApiBuildInput, refs: PathRefs): Record (a < b ? -1 : a > b ? 1 : 0))); + return Object.fromEntries(Object.entries(schemas).sort(([a], [b]) => compareNames(a, b))); } /** The whole document, in the key order it is written. */ diff --git a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts index 389625c812..dcec799ac7 100644 --- a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts +++ b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts @@ -85,7 +85,7 @@ export const LOCAL_TIMESTAMP_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0- * as UTC (the api-contract corpus sends one), which `date-time` refuses, so a write states what a * route takes and a read states what it answers. */ -export const TIMESTAMP_INPUT_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$"; +const TIMESTAMP_INPUT_PATTERN = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$"; const NUMERIC_SUBTYPES: ReadonlySet = new Set([ FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, diff --git a/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts b/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts index 2d992968e9..b51029e2a2 100644 --- a/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts +++ b/server/typescript/packages/integration-tests/src/api-contract-openapi-check.ts @@ -74,9 +74,14 @@ export function openApiCheck(caseName: string): ContractCheck { throw new Error(`${caseName}: ${req.method} ${req.path} (${req.id}): ${why}`); } + // Compiled once per pointer: Ajv caches by schema identity, so a fresh `$ref` object each call recompiles. + const compiled = new Map>(); + /** Compiled by its place in the document, so the `$ref`s inside resolve against the document. */ function validate(req: ApiRequest, what: string, at: string[], value: unknown): void { - const check = ajv.compile({ $ref: `${DOCUMENT_ID}#/${at.map(pointerToken).join("/")}` }); + const pointer = `${DOCUMENT_ID}#/${at.map(pointerToken).join("/")}`; + const check = compiled.get(pointer) ?? ajv.compile({ $ref: pointer }); + compiled.set(pointer, check); if (check(value)) return; fail(req, `${what} does not match the document: ${ajv.errorsText(check.errors)}: ${JSON.stringify(value)}`); } From 3a528177be5fc5f524aca41372fdb6b372a255f8 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sat, 31 Oct 2026 01:13:07 -0400 Subject: [PATCH 4/6] no-mistakes(review): apply reviewer inline fixes --- docs/features/contract-emitters.md | 5 +++-- .../specs/2026-06-11-fr-022-contract-emitters-design.md | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/features/contract-emitters.md b/docs/features/contract-emitters.md index affbe323d7..59a2595f3a 100644 --- a/docs/features/contract-emitters.md +++ b/docs/features/contract-emitters.md @@ -147,8 +147,9 @@ of what those hosts accept. What the subset cannot state is refused, not dropped, because a schema narrowed in silence gives a model a different contract from the one you declared. A map, an open object, a recursive type or a -polymorphic TPH base stops the run with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path. Leave -that object out with `filter`. +reference to a polymorphic TPH base stops the run with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming +the path. Leave that object out with `filter`. A TPH base gets no strict file of its own, because each +of its subtypes has one. ## The OpenAPI document diff --git a/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md b/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md index 1605b44375..7adb478086 100644 --- a/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md +++ b/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md @@ -110,10 +110,11 @@ own rules. None needed a product decision from outside them. strict file covers the object's create shape, the thing a model is asked to fill in, or its read shape when it has no create shape. It carries no `$schema` or `$id`, because some hosts refuse them, and `format`, `pattern` and the bound keywords become `description` prose. What the - subset cannot state (a map, an open object, a recursive type, a polymorphic base) is - **refused with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path**, never dropped: a - silently narrowed schema gives a model a different contract from the one declared. A project - leaves such an object out with `filter`. + subset cannot state (a map, an open object, a recursive type, a reference to a polymorphic + base) is **refused with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path**, never dropped: + a silently narrowed schema gives a model a different contract from the one declared. A project + leaves such an object out with `filter`. A TPH base gets no strict file of its own, because + each of its subtypes has one. 4. **Proto naming (open question 4).** Left to Phase 3 with the rest of it. 5. **Stable names.** `json-schema` was already taken: it is the Java generator for metadata files, a different concept. The emitter's stable name is `contract-schema`, its factory From f730b213bdbd1f3c4ba12165dc70352d92dbd154 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sat, 31 Oct 2026 01:44:00 -0400 Subject: [PATCH 5/6] no-mistakes(review): Align contract emitters with routes, drop unrequired surface --- docs/CONFORMANCE.md | 2 +- docs/features/contract-emitters.md | 13 +- ...6-06-11-fr-022-contract-emitters-design.md | 9 +- fixtures/contract-conformance/README.md | 4 +- fixtures/contract-conformance/blog/case.json | 2 +- .../contract-conformance/decimal/case.json | 2 +- .../decimal/expected/openapi.json | 35 +- .../schemas/acme/ledger/Entry.schema.json | 3 +- .../acme/ledger/EntryTotals.schema.json | 22 +- .../ledger/EntryTotals.strict.schema.json | 12 +- .../schemas/acme/ledger/Sample.schema.json | 10 +- fixtures/contract-conformance/jsonb/case.json | 2 +- .../jsonb/expected/openapi.json | 41 +- .../schemas/acme/store/Marker.schema.json | 5 +- .../schemas/acme/store/Measure.schema.json | 18 +- .../schemas/acme/store/Pricing.schema.json | 5 +- .../schemas/acme/store/Telemetry.schema.json | 5 +- .../schemas/acme/store/Tier.schema.json | 8 +- fixtures/contract-conformance/m2m/case.json | 2 +- .../contract-conformance/projection/case.json | 2 +- .../projection/expected/openapi.json | 6 +- .../schemas/acme/sales/Invoice.schema.json | 3 +- .../acme/sales/InvoiceLedger.schema.json | 3 +- .../sales/InvoiceLedger.strict.schema.json | 2 +- .../contract-conformance/report/case.json | 2 +- .../report/expected/openapi.json | 3 +- .../acme/sales/InvoiceTotals.schema.json | 3 +- .../sales/InvoiceTotals.strict.schema.json | 2 +- .../secret-fields/case.json | 2 +- .../tph-shared-marks/expected/openapi.json | 950 ++++++++++++++++++ .../schemas/acme/auth/Auth.schema.json | 13 + .../acme/auth/BridgeAuth.create.schema.json | 16 + .../schemas/acme/auth/BridgeAuth.schema.json | 32 + .../acme/auth/BridgeAuth.strict.schema.json | 14 + .../acme/auth/BridgeAuth.update.schema.json | 13 + .../acme/auth/CopayAuth.create.schema.json | 16 + .../schemas/acme/auth/CopayAuth.schema.json | 32 + .../acme/auth/CopayAuth.strict.schema.json | 14 + .../acme/auth/CopayAuth.update.schema.json | 13 + .../tph-shared-marks/meta.json | 36 + fixtures/contract-conformance/tph/case.json | 2 +- .../tph/expected/openapi.json | 3 +- .../schemas/acme/auth/CopayAuth.schema.json | 3 +- .../write-through/case.json | 2 +- .../packages/cli/src/commands/init.ts | 4 +- .../eject-contract-emitters.test.ts | 139 +++ .../test/unit/init-scaffold-config.test.ts | 10 + .../packages/codegen-ts/src/api-surface.ts | 23 +- .../codegen-ts/src/contract/build-openapi.ts | 12 +- .../codegen-ts/src/contract/field-schema.ts | 19 +- .../codegen-ts/src/contract/object-schema.ts | 41 +- .../codegen-ts/src/generator-registry.ts | 4 +- .../src/generators/contract-schema-file.ts | 9 +- .../codegen-ts/src/generators/openapi-file.ts | 10 +- .../packages/codegen-ts/src/index.ts | 15 - .../codegen-ts/src/metaobjects-config.ts | 5 +- .../src/reference/contract-schema.ts | 9 +- .../codegen-ts/src/reference/openapi.ts | 12 +- .../packages/codegen-ts/src/runner.ts | 1 + .../test/contract/contract-behaviours.test.ts | 90 +- .../test/metaobjects-config.test.ts | 2 +- 61 files changed, 1530 insertions(+), 262 deletions(-) create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/openapi.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/Auth.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.create.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.strict.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.update.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.create.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.strict.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.update.schema.json create mode 100644 fixtures/contract-conformance/tph-shared-marks/meta.json create mode 100644 server/typescript/packages/cli/test/integration/eject-contract-emitters.test.ts diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index d042037bbe..73b8182683 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -60,7 +60,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | [`fixtures/wire-conformance/`](../fixtures/wire-conformance/) (the `wire` setting, ADR-0019 Amendment 2) | 6 cases + 8 refusals | ✓ | ✓ | inherits via Java (one config reader) | ✓ | ✓ | | [`fixtures/codegen-noop/`](../fixtures/codegen-noop/) (FR-044 — reporting vocabulary: what is lowered, what stays inert) | 1 model pair (`reporting/with` vs `reporting/without`) | ✓ (codegen + migrate) | ✓ | ✓ | ✓ | ✓ | | [`fixtures/cube-model/`](../fixtures/cube-model/) (FR-044 Plan 4 — the `cube-model` generator's golden Cube files, plus the canonical model's golden) | 46 cases (36 trees + 10 errors) + 1 canonical golden | ✓ (the exporter is TypeScript-only, spec R6; its `cube` lane also loads the files into a real Cube) | — | — | — | — | -| [`fixtures/contract-conformance/`](../fixtures/contract-conformance/) (FR-022 — the `contract-schema` and `openapi` generators' golden trees and refusals) | 18 cases (13 trees + 5 errors) | ✓ (one neutral engine, ADR-0034 Amendment 10; the generated API lanes also hold real traffic to the documents) | — | — | — | — | +| [`fixtures/contract-conformance/`](../fixtures/contract-conformance/) (FR-022 — the `contract-schema` and `openapi` generators' golden trees and refusals) | 19 cases (14 trees + 5 errors) | ✓ (one neutral engine, ADR-0034 Amendment 10; the generated API lanes also hold real traffic to the documents) | — | — | — | — | A ✓ means the port runs the corpus green; an explicit `n / m` is used where a port carries a ledgered divergence. The two ledgered YAML fixtures are documented diff --git a/docs/features/contract-emitters.md b/docs/features/contract-emitters.md index 59a2595f3a..75818dcf6e 100644 --- a/docs/features/contract-emitters.md +++ b/docs/features/contract-emitters.md @@ -50,10 +50,8 @@ the model. | both | `filter` | every object | Narrow what is covered. An object a kept one references stays, so a `$ref` never dangles. | | both | `target` | the default target | The named output target the files land under. | | `jsonSchemaFile` | `profile` | `"canonical"` | `"strict"` writes the structured-output subset. | -| `jsonSchemaFile` | `baseUri` | the file's relative path | An absolute prefix for each `$id`, when a schema registry needs one. | | `openApiFile` | `title`, `version`, `description` | `API`, `0.0.0`, none | The document's `info`. | | `openApiFile` | `servers`, `securitySchemes`, `security` | none | Passed through untouched. See [Security](#security-and-servers). | -| `openApiFile` | `path` | `openapi.json` | Where the file lands under the target. | | `openApiFile` | `expose` | what the routes generators declare, else every verb | The verbs your routes mount. Needed when another port generates them. | ## What it writes @@ -86,7 +84,9 @@ A read, a create and an update differ in what is required and what may be null, | create | `@required` fields | may be left out, may not be `null` | server-set fields, a generated key, `@formExclude`, credentials, a pinned TPH discriminator | | update | none | may be left out, and `null` clears them | server-set fields, `@formExclude`, credentials, a TPH discriminator | -A nested value object in a write takes the target's create shape. +A nested value object in a write takes the target's create shape. A column several TPH subtypes +declare is one column, so a credential or `@formExclude` on one declaration holds on every subtype +that has it, as it does in the routes. ## Types @@ -147,9 +147,10 @@ of what those hosts accept. What the subset cannot state is refused, not dropped, because a schema narrowed in silence gives a model a different contract from the one you declared. A map, an open object, a recursive type or a -reference to a polymorphic TPH base stops the run with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming -the path. Leave that object out with `filter`. A TPH base gets no strict file of its own, because each -of its subtypes has one. +reference to a polymorphic TPH base reached through a read shape, that is, from an object with no +create shape, stops the run with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path. Leave that +object out with `filter`. A reference from a create shape inlines the base's own create fields. A TPH +base gets no strict file of its own, because each of its subtypes has one. ## The OpenAPI document diff --git a/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md b/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md index 7adb478086..abf4fda8db 100644 --- a/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md +++ b/docs/superpowers/specs/2026-06-11-fr-022-contract-emitters-design.md @@ -111,10 +111,11 @@ own rules. None needed a product decision from outside them. read shape when it has no create shape. It carries no `$schema` or `$id`, because some hosts refuse them, and `format`, `pattern` and the bound keywords become `description` prose. What the subset cannot state (a map, an open object, a recursive type, a reference to a polymorphic - base) is **refused with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path**, never dropped: - a silently narrowed schema gives a model a different contract from the one declared. A project - leaves such an object out with `filter`. A TPH base gets no strict file of its own, because - each of its subtypes has one. + base reached through a read shape, that is, from an object with no create shape) is **refused + with `ERR_CONTRACT_STRICT_UNREPRESENTABLE`, naming the path**, never dropped: a silently + narrowed schema gives a model a different contract from the one declared. A project leaves such + an object out with `filter`. A reference from a create shape inlines the base's own create + fields. A TPH base gets no strict file of its own, because each of its subtypes has one. 4. **Proto naming (open question 4).** Left to Phase 3 with the rest of it. 5. **Stable names.** `json-schema` was already taken: it is the Java generator for metadata files, a different concept. The emitter's stable name is `contract-schema`, its factory diff --git a/fixtures/contract-conformance/README.md b/fixtures/contract-conformance/README.md index a3d81606a3..e86c11df10 100644 --- a/fixtures/contract-conformance/README.md +++ b/fixtures/contract-conformance/README.md @@ -10,7 +10,8 @@ Each case is a directory. - `meta.json`, or `case.json` naming a shared metadata file. Most cases reuse an api-contract corpus model, because that is the surface every port's routes are held to. - `case.json` keys: `meta` (repository-relative), `wire` (the setting, as in - `.metaobjects/config.json`), `strictExclude` (objects the strict profile leaves out, because the + `.metaobjects/config.json`; a case on an api-contract model pins what that corpus pins, because the + integration suite holds that corpus's answers to this document), `strictExclude` (objects the strict profile leaves out, because the strict subset cannot state them) and `reportExport` (the project wires `report-export`). - `expected/`: the exact tree the emitters write: the canonical schemas, the strict schemas and `openapi.json`. Or `expected-error.txt`: the exact refusal. @@ -43,4 +44,5 @@ document for that corpus. | `report` | The api-contract `report/` model: served reports, and the rest of the model around them. | | `secret-fields` | The api-contract `secret-fields/` model: a credential is not on the wire. | | `tph` | The api-contract `tph/` model: a polymorphic base and per-subtype routes. | +| `tph-shared-marks` | Subtypes sharing a column that only one declaration marks as a credential or `@formExclude`. | | `write-through` | The api-contract `write-through/` model. | diff --git a/fixtures/contract-conformance/blog/case.json b/fixtures/contract-conformance/blog/case.json index c90b3c8185..ee85dd0638 100644 --- a/fixtures/contract-conformance/blog/case.json +++ b/fixtures/contract-conformance/blog/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/meta.json" } +{ "meta": "fixtures/api-contract-conformance/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/fixtures/contract-conformance/decimal/case.json b/fixtures/contract-conformance/decimal/case.json index a3c6c54562..099ba18278 100644 --- a/fixtures/contract-conformance/decimal/case.json +++ b/fixtures/contract-conformance/decimal/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/decimal/meta.json" } +{ "meta": "fixtures/api-contract-conformance/decimal/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/fixtures/contract-conformance/decimal/expected/openapi.json b/fixtures/contract-conformance/decimal/expected/openapi.json index b596813ec9..a742bb695a 100644 --- a/fixtures/contract-conformance/decimal/expected/openapi.json +++ b/fixtures/contract-conformance/decimal/expected/openapi.json @@ -1584,10 +1584,9 @@ "amount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "flagged": { "type": "boolean" @@ -1667,49 +1666,41 @@ "total": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "totalOrZero": { - "type": [ - "string", - "number" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "mean": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "top": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "flaggedShare": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "perFlagged": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ @@ -1786,20 +1777,16 @@ "null" ], "items": { - "type": [ - "string", - "number" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 9, scale 4." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "free": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json index 10b241eec9..7b7476c42b 100644 --- a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Entry.schema.json @@ -15,10 +15,9 @@ "amount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "flagged": { "type": "boolean" diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json index 4bdeacec70..660b3bc8ed 100644 --- a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.schema.json @@ -15,49 +15,41 @@ "total": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "totalOrZero": { - "type": [ - "string", - "number" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "mean": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "top": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "flaggedShare": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "perFlagged": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json index 8431c36d0d..5ddbabb142 100644 --- a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/EntryTotals.strict.schema.json @@ -10,39 +10,39 @@ "type": "integer" }, "total": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" ] }, "totalOrZero": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": "string" }, "mean": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" ] }, "top": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 28, scale 10.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" ] }, "flaggedShare": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" ] }, "perFlagged": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" diff --git a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json index f51a204783..aee2c0e1f4 100644 --- a/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json +++ b/fixtures/contract-conformance/decimal/expected/schemas/acme/ledger/Sample.schema.json @@ -14,20 +14,16 @@ "null" ], "items": { - "type": [ - "string", - "number" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 9, scale 4." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "free": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/jsonb/case.json b/fixtures/contract-conformance/jsonb/case.json index 948b352712..410b0c92a6 100644 --- a/fixtures/contract-conformance/jsonb/case.json +++ b/fixtures/contract-conformance/jsonb/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/jsonb/meta.json", "strictExclude": ["Document", "Pricing"] } +{ "meta": "fixtures/api-contract-conformance/jsonb/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"}, "strictExclude": ["Document", "Pricing"] } diff --git a/fixtures/contract-conformance/jsonb/expected/openapi.json b/fixtures/contract-conformance/jsonb/expected/openapi.json index bcc6621edd..11ff07dcaa 100644 --- a/fixtures/contract-conformance/jsonb/expected/openapi.json +++ b/fixtures/contract-conformance/jsonb/expected/openapi.json @@ -725,10 +725,7 @@ "maxLength": 40 }, "score": { - "type": [ - "integer", - "null" - ], + "type": "integer", "format": "int32" } }, @@ -759,29 +756,19 @@ "type": "object", "properties": { "ratio": { - "type": [ - "number", - "null" - ], + "type": "number", "format": "double" }, "samples": { - "type": [ - "array", - "null" - ], + "type": "array", "items": { "type": "number", "format": "double" } }, "amount": { - "type": [ - "string", - "number", - "null" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } } }, @@ -815,10 +802,7 @@ "type": "object", "properties": { "tiers": { - "type": [ - "object", - "null" - ], + "type": "object", "additionalProperties": { "$ref": "#/components/schemas/Tier" } @@ -850,10 +834,7 @@ "format": "double" }, "tempC": { - "type": [ - "integer", - "null" - ], + "type": "integer", "format": "int32" } }, @@ -890,12 +871,8 @@ "type": "object", "properties": { "amount": { - "type": [ - "string", - "number", - "null" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } } }, diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json index e3cec584ae..0a2b401072 100644 --- a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Marker.schema.json @@ -9,10 +9,7 @@ "maxLength": 40 }, "score": { - "type": [ - "integer", - "null" - ], + "type": "integer", "format": "int32" } }, diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json index f9bd343db8..e21a38aefd 100644 --- a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Measure.schema.json @@ -5,29 +5,19 @@ "type": "object", "properties": { "ratio": { - "type": [ - "number", - "null" - ], + "type": "number", "format": "double" }, "samples": { - "type": [ - "array", - "null" - ], + "type": "array", "items": { "type": "number", "format": "double" } }, "amount": { - "type": [ - "string", - "number", - "null" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } } } diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json index 9b59d930f5..b6e5dd946f 100644 --- a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Pricing.schema.json @@ -5,10 +5,7 @@ "type": "object", "properties": { "tiers": { - "type": [ - "object", - "null" - ], + "type": "object", "additionalProperties": { "$ref": "./Tier.schema.json" } diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json index 2f4a8ca706..e651674090 100644 --- a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Telemetry.schema.json @@ -13,10 +13,7 @@ "format": "double" }, "tempC": { - "type": [ - "integer", - "null" - ], + "type": "integer", "format": "int32" } }, diff --git a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json index 23a18d7b1a..5d53183c74 100644 --- a/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json +++ b/fixtures/contract-conformance/jsonb/expected/schemas/acme/store/Tier.schema.json @@ -5,12 +5,8 @@ "type": "object", "properties": { "amount": { - "type": [ - "string", - "number", - "null" - ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 12, scale 4." + "type": "string", + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } } } diff --git a/fixtures/contract-conformance/m2m/case.json b/fixtures/contract-conformance/m2m/case.json index efacd86bf5..9c456f6100 100644 --- a/fixtures/contract-conformance/m2m/case.json +++ b/fixtures/contract-conformance/m2m/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/m2m/meta.json" } +{ "meta": "fixtures/api-contract-conformance/m2m/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/fixtures/contract-conformance/projection/case.json b/fixtures/contract-conformance/projection/case.json index afc8ee3f88..7df31120b3 100644 --- a/fixtures/contract-conformance/projection/case.json +++ b/fixtures/contract-conformance/projection/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/projection/meta.json" } +{ "meta": "fixtures/api-contract-conformance/projection/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/fixtures/contract-conformance/projection/expected/openapi.json b/fixtures/contract-conformance/projection/expected/openapi.json index 013f3d895c..fa22cc89b5 100644 --- a/fixtures/contract-conformance/projection/expected/openapi.json +++ b/fixtures/contract-conformance/projection/expected/openapi.json @@ -1533,10 +1533,9 @@ "discount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "weight": { "type": [ @@ -1607,10 +1606,9 @@ "discount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "weight": { "type": [ diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json index e4e9380bdc..ce8a80e508 100644 --- a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/Invoice.schema.json @@ -23,10 +23,9 @@ "discount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "weight": { "type": [ diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json index 6f287783a6..944095ad1e 100644 --- a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.schema.json @@ -15,10 +15,9 @@ "discount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" }, "weight": { "type": [ diff --git a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json index 8fa0079fe1..0b582217bb 100644 --- a/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json +++ b/fixtures/contract-conformance/projection/expected/schemas/acme/sales/InvoiceLedger.strict.schema.json @@ -10,7 +10,7 @@ "type": "string" }, "discount": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" diff --git a/fixtures/contract-conformance/report/case.json b/fixtures/contract-conformance/report/case.json index 70ec7ff1aa..ebebb502a7 100644 --- a/fixtures/contract-conformance/report/case.json +++ b/fixtures/contract-conformance/report/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/report/meta.json", "strictExclude": ["Ticket", "TicketsByTags"], "reportExport": true } +{ "meta": "fixtures/api-contract-conformance/report/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"}, "strictExclude": ["Ticket", "TicketsByTags"], "reportExport": true } diff --git a/fixtures/contract-conformance/report/expected/openapi.json b/fixtures/contract-conformance/report/expected/openapi.json index e014146cf1..ebd15ce842 100644 --- a/fixtures/contract-conformance/report/expected/openapi.json +++ b/fixtures/contract-conformance/report/expected/openapi.json @@ -6572,10 +6572,9 @@ "paidShare": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json index 1e840b66ed..b399ec8e07 100644 --- a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.schema.json @@ -18,10 +18,9 @@ "paidShare": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json index 64e94ce688..6fe933750d 100644 --- a/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json +++ b/fixtures/contract-conformance/report/expected/schemas/acme/sales/InvoiceTotals.strict.schema.json @@ -12,7 +12,7 @@ ] }, "paidShare": { - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages.", + "description": "Pattern: ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$.", "type": [ "string", "null" diff --git a/fixtures/contract-conformance/secret-fields/case.json b/fixtures/contract-conformance/secret-fields/case.json index e998108cca..51ec47303f 100644 --- a/fixtures/contract-conformance/secret-fields/case.json +++ b/fixtures/contract-conformance/secret-fields/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/secret-fields/meta.json" } +{ "meta": "fixtures/api-contract-conformance/secret-fields/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/openapi.json b/fixtures/contract-conformance/tph-shared-marks/expected/openapi.json new file mode 100644 index 0000000000..f11226954f --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/openapi.json @@ -0,0 +1,950 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "tph-shared-marks", + "version": "1.0.0" + }, + "paths": { + "/api/auths": { + "get": { + "operationId": "listAuth", + "tags": [ + "Auth" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AuthList" + }, + { + "$ref": "#/components/schemas/AuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getAuth", + "tags": [ + "Auth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Auth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/bridge": { + "get": { + "operationId": "listBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/BridgeAuthList" + }, + { + "$ref": "#/components/schemas/BridgeAuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuthCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/bridge/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BridgeAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteBridgeAuth", + "tags": [ + "BridgeAuth" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/copay": { + "get": { + "operationId": "listCopayAuth", + "tags": [ + "CopayAuth" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "withCount", + "in": "query", + "description": "Answer { rows, total } instead of the array.", + "schema": { + "type": "integer", + "enum": [ + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "The rows.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CopayAuthList" + }, + { + "$ref": "#/components/schemas/CopayAuthPage" + } + ] + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "post": { + "operationId": "createCopayAuth", + "tags": [ + "CopayAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuthCreate" + } + } + } + }, + "responses": { + "201": { + "description": "The new row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/api/auths/copay/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "get": { + "operationId": "getCopayAuth", + "tags": [ + "CopayAuth" + ], + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "patch": { + "operationId": "updateCopayAuth", + "tags": [ + "CopayAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "put": { + "operationId": "putCopayAuth", + "tags": [ + "CopayAuth" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuthUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "The row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopayAuth" + } + } + } + }, + "400": { + "description": "The request was refused.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCopayAuth", + "tags": [ + "CopayAuth" + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No such row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "A constraint refused the write.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiError": { + "title": "ApiError", + "description": "The error envelope. `error` is the stable code; `message` is free prose.", + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "field": { + "type": "string", + "description": "The rejected filter or sort field, on the four codes that name one." + } + } + }, + "Auth": { + "title": "Auth", + "oneOf": [ + { + "$ref": "#/components/schemas/BridgeAuth" + }, + { + "$ref": "#/components/schemas/CopayAuth" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "Bridge": "#/components/schemas/BridgeAuth", + "Copay": "#/components/schemas/CopayAuth" + } + } + }, + "AuthList": { + "title": "AuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/Auth" + } + }, + "AuthPage": { + "title": "AuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Auth" + } + }, + "total": { + "type": "integer" + } + } + }, + "BridgeAuth": { + "title": "BridgeAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Bridge" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "auditRef": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + }, + "required": [ + "id", + "type", + "reference", + "auditRef" + ] + }, + "BridgeAuthCreate": { + "title": "BridgeAuthCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "reference" + ] + }, + "BridgeAuthList": { + "title": "BridgeAuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/BridgeAuth" + } + }, + "BridgeAuthPage": { + "title": "BridgeAuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BridgeAuth" + } + }, + "total": { + "type": "integer" + } + } + }, + "BridgeAuthUpdate": { + "title": "BridgeAuthUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + }, + "CopayAuth": { + "title": "CopayAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Copay" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "auditRef": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + }, + "required": [ + "id", + "type", + "reference", + "auditRef" + ] + }, + "CopayAuthCreate": { + "title": "CopayAuthCreate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "reference" + ] + }, + "CopayAuthList": { + "title": "CopayAuthList", + "type": "array", + "items": { + "$ref": "#/components/schemas/CopayAuth" + } + }, + "CopayAuthPage": { + "title": "CopayAuthPage", + "type": "object", + "required": [ + "rows", + "total" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CopayAuth" + } + }, + "total": { + "type": "integer" + } + } + }, + "CopayAuthUpdate": { + "title": "CopayAuthUpdate", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + } + } + } +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/Auth.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/Auth.schema.json new file mode 100644 index 0000000000..8b67c7624b --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/Auth.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/Auth.schema.json", + "title": "Auth", + "oneOf": [ + { + "$ref": "./BridgeAuth.schema.json" + }, + { + "$ref": "./CopayAuth.schema.json" + } + ] +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.create.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.create.schema.json new file mode 100644 index 0000000000..88c95caa26 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/BridgeAuth.create.schema.json", + "title": "BridgeAuth.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "reference" + ] +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.schema.json new file mode 100644 index 0000000000..1936d47095 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/BridgeAuth.schema.json", + "title": "BridgeAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Bridge" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "auditRef": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + }, + "required": [ + "id", + "type", + "reference", + "auditRef" + ] +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.strict.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.strict.schema.json new file mode 100644 index 0000000000..46de5e9c98 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "BridgeAuth", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + } + }, + "required": [ + "reference" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.update.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.update.schema.json new file mode 100644 index 0000000000..c352d97c16 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/BridgeAuth.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/BridgeAuth.update.schema.json", + "title": "BridgeAuth.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.create.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.create.schema.json new file mode 100644 index 0000000000..664615d88d --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.create.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/CopayAuth.create.schema.json", + "title": "CopayAuth.create", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "required": [ + "reference" + ] +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.schema.json new file mode 100644 index 0000000000..c302a0d48b --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/CopayAuth.schema.json", + "title": "CopayAuth", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "const": "Copay" + }, + "reference": { + "type": "string", + "maxLength": 80 + }, + "auditRef": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + }, + "required": [ + "id", + "type", + "reference", + "auditRef" + ] +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.strict.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.strict.schema.json new file mode 100644 index 0000000000..80c1ee1b79 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.strict.schema.json @@ -0,0 +1,14 @@ +{ + "title": "CopayAuth", + "type": "object", + "properties": { + "reference": { + "description": "Minimum length: 1. Maximum length: 80.", + "type": "string" + } + }, + "required": [ + "reference" + ], + "additionalProperties": false +} diff --git a/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.update.schema.json b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.update.schema.json new file mode 100644 index 0000000000..48d05089b8 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/expected/schemas/acme/auth/CopayAuth.update.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schemas/acme/auth/CopayAuth.update.schema.json", + "title": "CopayAuth.update", + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } +} diff --git a/fixtures/contract-conformance/tph-shared-marks/meta.json b/fixtures/contract-conformance/tph-shared-marks/meta.json new file mode 100644 index 0000000000..c3cff88705 --- /dev/null +++ b/fixtures/contract-conformance/tph-shared-marks/meta.json @@ -0,0 +1,36 @@ +{ + "metadata.root": { + "package": "acme::auth", + "children": [ + { "object.entity": { + "name": "Auth", + "@discriminator": "type", + "children": [ + { "source.rdb": { "@table": "auths" } }, + { "field.long": { "name": "id" } }, + { "field.enum": { "name": "type", "@values": ["Bridge", "Copay"] } }, + { "field.string": { "name": "reference", "@required": true, "@maxLength": 80 } }, + { "identity.primary": { "name": "id", "@fields": "id", "@generation": "increment" } } + ] + }}, + { "object.entity": { + "name": "BridgeAuth", + "extends": "Auth", + "@discriminatorValue": "Bridge", + "children": [ + { "field.string": { "name": "token", "@maxLength": 100, "children": [ { "view.password": {} } ] } }, + { "field.string": { "name": "auditRef", "@maxLength": 100, "@formExclude": true } } + ] + }}, + { "object.entity": { + "name": "CopayAuth", + "extends": "Auth", + "@discriminatorValue": "Copay", + "children": [ + { "field.string": { "name": "token", "@maxLength": 100 } }, + { "field.string": { "name": "auditRef", "@maxLength": 100 } } + ] + }} + ] + } +} diff --git a/fixtures/contract-conformance/tph/case.json b/fixtures/contract-conformance/tph/case.json index 302701ba16..986490c531 100644 --- a/fixtures/contract-conformance/tph/case.json +++ b/fixtures/contract-conformance/tph/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/tph/meta.json" } +{ "meta": "fixtures/api-contract-conformance/tph/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/fixtures/contract-conformance/tph/expected/openapi.json b/fixtures/contract-conformance/tph/expected/openapi.json index 3693e1e4f8..439dc9defb 100644 --- a/fixtures/contract-conformance/tph/expected/openapi.json +++ b/fixtures/contract-conformance/tph/expected/openapi.json @@ -2026,10 +2026,9 @@ "copayAmount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json index a53fcc5ceb..a71e5f76bb 100644 --- a/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json +++ b/fixtures/contract-conformance/tph/expected/schemas/acme/auth/CopayAuth.schema.json @@ -39,10 +39,9 @@ "copayAmount": { "type": [ "string", - "number", "null" ], - "description": "A decimal. Set wire.decimals to \"string\" to fix one form across server languages. Precision 10, scale 2." + "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$" } }, "required": [ diff --git a/fixtures/contract-conformance/write-through/case.json b/fixtures/contract-conformance/write-through/case.json index e616937046..1b7e6bd8cb 100644 --- a/fixtures/contract-conformance/write-through/case.json +++ b/fixtures/contract-conformance/write-through/case.json @@ -1 +1 @@ -{ "meta": "fixtures/api-contract-conformance/write-through/meta.json" } +{ "meta": "fixtures/api-contract-conformance/write-through/meta.json", "wire": {"decimals": "string", "nullMembers": "omit"} } diff --git a/server/typescript/packages/cli/src/commands/init.ts b/server/typescript/packages/cli/src/commands/init.ts index 0b3f7f04f8..d3dd7634ac 100644 --- a/server/typescript/packages/cli/src/commands/init.ts +++ b/server/typescript/packages/cli/src/commands/init.ts @@ -139,8 +139,8 @@ export default defineConfig({ // // Nothing is generated that this file does not declare, documentation included. // - // Need an output nothing in that catalog emits — OpenAPI, JSON Schema, a client, a - // service layer? Write your own generator; that is the normal path: + // Need an output nothing in that catalog emits — a client, a service layer? Write + // your own generator; that is the normal path: // // meta generator new data-dictionary --scope model // diff --git a/server/typescript/packages/cli/test/integration/eject-contract-emitters.test.ts b/server/typescript/packages/cli/test/integration/eject-contract-emitters.test.ts new file mode 100644 index 0000000000..8bdac61675 --- /dev/null +++ b/server/typescript/packages/cli/test/integration/eject-contract-emitters.test.ts @@ -0,0 +1,139 @@ +/** + * FR-022 — an ejected contract emitter stands on the engine and on what eject copied. + * + * The contract stages are not on the package root. `meta eject` copies them into + * `codegen/templates/`, so an owned `openapi` or `contract-schema` must compile and run with + * the package's public entries alone: this ejects both into a project that links only those + * packages, compiles the whole owned tree with `tsc`, runs `meta gen` through the copies, and + * holds the output to the packaged generators' bytes. + */ +import { describe, test, expect } from "bun:test"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import * as packageRoot from "@metaobjectsdev/codegen-ts"; +import { defineConfig, runGen } from "@metaobjectsdev/codegen-ts"; +import { jsonSchemaFile, openApiFile } from "@metaobjectsdev/codegen-ts/generators"; +import { MetaDataLoader, InMemoryStringSource } from "@metaobjectsdev/metadata"; +import { run } from "../../src/index.js"; + +const CLI_ROOT = resolve(import.meta.dirname, "..", ".."); +const TSC = join(CLI_ROOT, "node_modules", "typescript", "bin", "tsc"); +const DEPS = ["@metaobjectsdev/codegen-ts", "@metaobjectsdev/metadata", "@types/node", "typescript"]; + +const META = JSON.stringify({ + "metadata.root": { + package: "acme::shop", + children: [ + { + "object.entity": { + name: "Product", + children: [ + { "source.rdb": { "@table": "products" } }, + { "field.long": { name: "id" } }, + { "field.string": { name: "name", "@required": true, "@maxLength": 200, "@filterable": true } }, + { "field.decimal": { name: "price", "@precision": 10, "@scale": 2 } }, + { "identity.primary": { "@fields": "id", "@generation": "increment" } }, + ], + }, + }, + ], + }, +}, null, 2); + +const CONFIG = `import { defineConfig } from "@metaobjectsdev/codegen-ts"; +import { jsonSchemaFile } from "./codegen/generators/contract-schema.js"; +import { openApiFile } from "./codegen/generators/openapi.js"; + +export default defineConfig({ + outDir: "src/gen", + dialect: "sqlite", + apiPrefix: "/api", + extStyle: "js", + generators: [jsonSchemaFile(), jsonSchemaFile({ profile: "strict" }), openApiFile({ title: "Shop", version: "1.0.0" })], +}); +`; + +const TSCONFIG = JSON.stringify({ + compilerOptions: { + target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", strict: true, + noEmit: true, skipLibCheck: true, types: ["node"], + }, + include: ["codegen/**/*.ts", "metaobjects.config.ts"], +}, null, 2); + +function scaffold(): string { + const root = mkdtempSync(join(tmpdir(), "mo-owned-contract-")); + mkdirSync(join(root, "metaobjects"), { recursive: true }); + writeFileSync(join(root, "metaobjects", "meta.shop.json"), META); + writeFileSync(join(root, "package.json"), JSON.stringify({ name: "owned-contract-app", type: "module" })); + writeFileSync(join(root, "metaobjects.config.ts"), CONFIG); + writeFileSync(join(root, "tsconfig.json"), TSCONFIG); + for (const name of DEPS) { + const target = join(CLI_ROOT, "node_modules", name); + if (!existsSync(target)) throw new Error(`test setup: ${name} is not installed under ${CLI_ROOT}`); + const link = join(root, "node_modules", name); + mkdirSync(dirname(link), { recursive: true }); + symlinkSync(target, link, "dir"); + } + return root; +} + +async function quietly(argv: string[]): Promise { + const { log, error } = console; + console.log = () => {}; + console.error = () => {}; + try { + return await run(argv); + } finally { + console.log = log; + console.error = error; + } +} + +async function packagedOpenApi(outDir: string): Promise { + const loader = new MetaDataLoader(); + const { root } = await loader.load([new InMemoryStringSource(META, { id: "meta.shop.json" })]); + await runGen({ + config: defineConfig({ + outDir, + dialect: "sqlite", + apiPrefix: "/api", + generators: [jsonSchemaFile(), openApiFile({ title: "Shop", version: "1.0.0" })], + }), + metadata: root, + genStateDir: join(outDir, ".gen-state"), + }); + return readFileSync(join(outDir, "openapi.json"), "utf8"); +} + +describe("an ejected contract emitter", () => { + test("the package root holds none of the contract stages", () => { + const root = packageRoot as Record; + for (const name of ["jsonSchemaFile", "openApiFile", "buildOpenApiDocument", "objectSchema", "fieldSchema", "strictSchema", "ContractEmitError"]) { + expect(root[name], name).toBeUndefined(); + } + }); + + test("eject → tsc → gen, with no root export of the stages it copied", async () => { + const root = scaffold(); + try { + expect(await quietly(["eject", "contract-schema", "openapi", "--cwd", root])).toBe(0); + for (const copied of ["build-openapi", "field-schema", "object-schema", "strict-profile", "contract-errors"]) { + expect(existsSync(join(root, "codegen", "templates", "contract", `${copied}.ts`)), copied).toBe(true); + } + + const tsc = spawnSync(process.execPath, [TSC, "-p", "tsconfig.json"], { cwd: root, encoding: "utf8" }); + expect(tsc.stdout + tsc.stderr).toBe(""); + expect(tsc.status).toBe(0); + + expect(await quietly(["gen", "--cwd", root])).toBe(0); + const owned = readFileSync(join(root, "src", "gen", "openapi.json"), "utf8"); + expect(owned).toBe(await packagedOpenApi(join(root, "packaged"))); + expect(existsSync(join(root, "src", "gen", "schemas", "acme", "shop", "Product.strict.schema.json"))).toBe(true); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, 120_000); +}); diff --git a/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts b/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts index 95e5ae5680..99ae9fd1de 100644 --- a/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts +++ b/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts @@ -22,6 +22,16 @@ describe("meta init scaffolds metaobjects.config.ts", () => { expect(result.created).toContain("metaobjects.config.ts"); }); + // The sentence sends a reader to `meta generator new` for what no catalog entry emits, so + // it may not name an output the catalog ships. + test("does not send a reader to hand-write what the catalog ships", async () => { + await init({ cwd: tmp, quiet: true }); + const body = readFileSync(join(tmp, "metaobjects.config.ts"), "utf-8"); + const sentence = /Need an output nothing in that catalog emits[^?]*\?/.exec(body.replace(/\n\s*\/\/\s*/g, " "))?.[0]; + expect(sentence).toBeDefined(); + for (const shipped of ["OpenAPI", "JSON Schema"]) expect(sentence).not.toContain(shipped); + }); + // ADR-0034 Amendment 9: nothing is generated that the config does not declare, and // `meta init` declares nothing, documentation included. It writes no `docs` block (the 1.0 // shape `meta gen` now refuses) and wires no docs generator; it says how to add one. diff --git a/server/typescript/packages/codegen-ts/src/api-surface.ts b/server/typescript/packages/codegen-ts/src/api-surface.ts index 6a7ec0d6b5..e587b27f5c 100644 --- a/server/typescript/packages/codegen-ts/src/api-surface.ts +++ b/server/typescript/packages/codegen-ts/src/api-surface.ts @@ -40,14 +40,15 @@ import { type MetaData, type MetaField, type MetaObject, + type MetaRoot, } from "@metaobjectsdev/metadata"; import { isAbstract } from "./instance-artifacts.js"; import { isProjection } from "./projection/projection-detector.js"; import { hasAnyRdbSource, hasWritableRdbSource, isReport, servedReport } from "./source-detect.js"; import { getPkFields } from "./pk-resolver.js"; import { resourcePath, restPath } from "./templates/entity-ui-descriptor.js"; -import { declaresTphDiscriminator, isTphSubtype, tphDiscriminatorBase, tphDiscriminatorPin } from "./tph.js"; -import { tphRouteSegment } from "./templates/tph-discriminator.js"; +import { declaresTphDiscriminator, isTphSubtype, tphDiscriminatorBase, tphDiscriminatorPin, tphStorageObject } from "./tph.js"; +import { tphPlan, tphRouteSegment } from "./templates/tph-discriminator.js"; /** * True when the object is served by a generated READ endpoint. @@ -185,13 +186,29 @@ function markedFieldNames( return [...new Set(objects.flatMap((o) => o.fields().map((f) => f.name)))].filter((n) => marked.has(n)); } +/** The objects that share `obj`'s table: a TPH base and every subtype, else `obj` alone. */ +export function tphHierarchy(obj: MetaObject, root: MetaRoot): readonly MetaObject[] { + const plan = tphPlan(tphStorageObject(obj), root); + return plan === null ? [obj] : [plan.base, ...plan.subtypes.map((s) => s.entity)]; +} + +/** The names of the fields of `obj` no route reads: a credential on any declaration in `hierarchy`. */ +export function hiddenFieldNames(obj: MetaObject, hierarchy: readonly MetaObject[] = [obj]): string[] { + return markedFieldNames([obj], hierarchy, isCredentialField); +} + +/** The names of the fields of `obj` no route writes: denied on any declaration in `hierarchy`. */ +export function writeDeniedFieldNames(obj: MetaObject, hierarchy: readonly MetaObject[] = [obj]): string[] { + return markedFieldNames([obj], hierarchy, isApiWriteDenied); +} + /** * The `writeDeniedColumns` line of a mount that accepts writes, or "" when no field is denied * (the output stays byte-identical for a model with no `@formExclude` or credential field). * `hierarchy` is a TPH subtype's base and every subtype, when the mount serves one. */ export function writeDeniedColumnsLine(entity: MetaObject, indent: string, hierarchy: readonly MetaObject[] = [entity]): string { - const names = markedFieldNames([entity], hierarchy, isApiWriteDenied); + const names = writeDeniedFieldNames(entity, hierarchy); return names.length === 0 ? "" : `\n${indent}writeDeniedColumns: ${JSON.stringify(names)},`; } diff --git a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts index 1108b9be35..0eb12235b7 100644 --- a/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts +++ b/server/typescript/packages/codegen-ts/src/contract/build-openapi.ts @@ -311,15 +311,10 @@ function verbsOf(fixed: readonly CrudVerb[], obj: MetaObject, expose: ExposeOpti return new Set(intersectExpose([...fixed], resolveExpose(obj, expose))); } -function pathsOf( - obj: MetaObject, - input: OpenApiBuildInput, - reportNames: ReadonlySet, - refs: PathRefs, -): Record { +function pathsOf(obj: MetaObject, input: OpenApiBuildInput, refs: PathRefs): Record { const { root, apiPrefix, expose, relationMap } = input; const path = servedPath(obj, apiPrefix); - const report = reportNames.has(obj.name); + const report = isReport(obj); if (isProjection(obj)) { return resourcePaths({ row: obj, path, report, reportExport: report && input.reportExport, item: hasItemRoute(obj), refs, @@ -408,12 +403,11 @@ function collectSchemas(input: OpenApiBuildInput, refs: PathRefs): Record o.name)); const objects = [...input.objects].sort(byName); const served = objects.filter((o) => servesReadApi(o) && !isTphSubtype(o) && (input.mounted?.(o) ?? true)); const paths: Record = {}; const refs: PathRefs = { shapes: [], listed: new Set() }; - for (const obj of served) Object.assign(paths, pathsOf(obj, input, reportNames, refs)); + for (const obj of served) Object.assign(paths, pathsOf(obj, input, refs)); refuseDuplicateOperationIds(paths); return { openapi: OPENAPI_VERSION, diff --git a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts index dcec799ac7..82cae81a9f 100644 --- a/server/typescript/packages/codegen-ts/src/contract/field-schema.ts +++ b/server/typescript/packages/codegen-ts/src/contract/field-schema.ts @@ -13,7 +13,7 @@ import { type MetaField, type MetaObject, WIRE_DECIMALS_STRING, - wireFormatOf, + type WireFormat, } from "@metaobjectsdev/metadata"; import { DB_COLUMN_TYPE_JSONB, @@ -103,7 +103,7 @@ function decimalPrecisionNote(field: MetaField): string { } /** A decimal. An answer follows `wire.decimals`; a request takes either form in every mode. */ -function decimalSchema(field: MetaField, shape: ContractShape): JsonSchema { +function decimalSchema(field: MetaField, shape: ContractShape, wire: WireFormat): JsonSchema { const note = decimalPrecisionNote(field); if (shape !== "read") { return { @@ -112,7 +112,7 @@ function decimalSchema(field: MetaField, shape: ContractShape): JsonSchema { description: `A decimal, as a string in JSON-number grammar or a JSON number.${note}`, }; } - if (wireFormatOf(field).decimals === WIRE_DECIMALS_STRING) { + if (wire.decimals === WIRE_DECIMALS_STRING) { return { type: "string", pattern: DECIMAL_CANONICAL_PATTERN }; } return { @@ -154,11 +154,11 @@ export function enumSchema(field: MetaField): JsonSchema { } /** One value of a field that holds no reference to another object. */ -function plainValueSchema(field: MetaField, shape: ContractShape): JsonSchema { +function plainValueSchema(field: MetaField, shape: ContractShape, wire: WireFormat): JsonSchema { const lenient = field.attr(FIELD_ATTR_LENIENT) === true; switch (field.subType) { case FIELD_SUBTYPE_DECIMAL: - return decimalSchema(field, shape); + return decimalSchema(field, shape, wire); case FIELD_SUBTYPE_TIMESTAMP: if (shape !== "read") return timestampInputSchema(field); return field.attr(FIELD_ATTR_LOCAL_TIME) === true @@ -264,13 +264,16 @@ function joinDescription(first: unknown, second: unknown): string | undefined { return parts.length === 0 ? undefined : parts.join(" "); } -/** The JSON Schema of a field's value in one shape. Null is the caller's to add: see `withNull`. */ -export function fieldSchema(field: MetaField, shape: ContractShape, refFor: RefFor): JsonSchema { +/** + * The JSON Schema of a field's value in one shape. Null is the caller's to add: see `withNull`. + * `wire` is the loaded root's setting: a report's read model has no root of its own to read. + */ +export function fieldSchema(field: MetaField, shape: ContractShape, refFor: RefFor, wire: WireFormat): JsonSchema { const value: JsonSchema = field.subType === FIELD_SUBTYPE_OBJECT ? objectValueSchema(field, shape, refFor) : field.subType === FIELD_SUBTYPE_MAP ? mapValueSchema(field, shape, refFor) - : plainValueSchema(field, shape); + : plainValueSchema(field, shape, wire); const bounds = boundsOf(field); let schema = value; diff --git a/server/typescript/packages/codegen-ts/src/contract/object-schema.ts b/server/typescript/packages/codegen-ts/src/contract/object-schema.ts index ee9dd4609f..de217b58c6 100644 --- a/server/typescript/packages/codegen-ts/src/contract/object-schema.ts +++ b/server/typescript/packages/codegen-ts/src/contract/object-schema.ts @@ -15,10 +15,11 @@ import { type MetaRoot, WIRE_NULL_MEMBERS_EMIT, WIRE_NULL_MEMBERS_OMIT, + type WireFormat, wireFormatOf, } from "@metaobjectsdev/metadata"; import { DOC_ATTR_DESCRIPTION, OBJECT_SUBTYPE_VALUE } from "@metaobjectsdev/metadata/constants"; -import { isApiWriteDenied, isCredentialField, servesWriteApi } from "../api-surface.js"; +import { hiddenFieldNames, servesWriteApi, tphHierarchy, writeDeniedFieldNames } from "../api-surface.js"; import { isAbstract } from "../instance-artifacts.js"; import { tphPlan } from "../templates/tph-discriminator.js"; import { insertSchemaFields, updateSchemaFields } from "../templates/zod-validators.js"; @@ -67,28 +68,28 @@ function polymorphicSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema | return { oneOf, discriminator: { propertyName: plan.discriminatorField, mapping } }; } -function readSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { +function readSchema(obj: MetaObject, env: ObjectSchemaEnv, wire: WireFormat): JsonSchema { const polymorphic = polymorphicSchema(obj, env); if (polymorphic !== undefined) return polymorphic; const pk = new Set(obj.primaryIdentity()?.fields ?? []); const pin = tphDiscriminatorPin(obj); - const nullMembers = wireFormatOf(obj).nullMembers; + const hidden = new Set(hiddenFieldNames(obj, tphHierarchy(obj, env.root))); const properties: Record = {}; const required: string[] = []; // A credential is never on the wire: the key is absent, not null. - for (const field of obj.fields().filter((f) => !isCredentialField(f))) { + for (const field of obj.fields().filter((f) => !hidden.has(f.name))) { if (pin !== undefined && field.name === pin.fieldName) { properties[field.name] = { const: pin.value }; required.push(field.name); continue; } - const schema = fieldSchema(field, "read", env.refFor); + const schema = fieldSchema(field, "read", env.refFor, wire); if (pk.has(field.name) || field.isRequired) { properties[field.name] = schema; required.push(field.name); - } else if (isValueObject(obj) && nullMembers === WIRE_NULL_MEMBERS_OMIT) { + } else if (isValueObject(obj) && wire.nullMembers === WIRE_NULL_MEMBERS_OMIT) { properties[field.name] = schema; - } else if (isValueObject(obj) && nullMembers !== WIRE_NULL_MEMBERS_EMIT) { + } else if (isValueObject(obj) && wire.nullMembers !== WIRE_NULL_MEMBERS_EMIT) { properties[field.name] = withNull(schema); } else { properties[field.name] = withNull(schema); @@ -98,33 +99,40 @@ function readSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { return objectBody(properties, required); } +/** The fields no client writes, as the routes decide it: across a TPH hierarchy's one table. */ +function writeDenied(obj: MetaObject, env: ObjectSchemaEnv): ReadonlySet { + return new Set(writeDeniedFieldNames(obj, tphHierarchy(obj, env.root))); +} + /** Whether a client writes this field: not a server-filled one, and not a write-denied one. */ -function isClientWritten(field: MetaField | undefined, serverSet: boolean | undefined): field is MetaField { - return field !== undefined && serverSet !== true && !isApiWriteDenied(field); +function isClientWritten(field: MetaField | undefined, serverSet: boolean | undefined, denied: ReadonlySet): field is MetaField { + return field !== undefined && serverSet !== true && !denied.has(field.name); } -function createSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { +function createSchema(obj: MetaObject, env: ObjectSchemaEnv, wire: WireFormat): JsonSchema { const fieldByName = new Map(obj.fields().map((f) => [f.name, f])); + const denied = writeDenied(obj, env); const properties: Record = {}; const required: string[] = []; for (const shape of insertSchemaFields(obj)) { const field = fieldByName.get(shape.name); // A TPH subtype's discriminator is injected from the URL, not read from the body. - if (shape.pinnedLiteral !== undefined || !isClientWritten(field, shape.autoSet)) continue; - properties[shape.name] = fieldSchema(field, "create", env.refFor); + if (shape.pinnedLiteral !== undefined || !isClientWritten(field, shape.autoSet, denied)) continue; + properties[shape.name] = fieldSchema(field, "create", env.refFor, wire); if (!shape.optional) required.push(shape.name); } return objectBody(properties, required); } -function updateSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { +function updateSchema(obj: MetaObject, env: ObjectSchemaEnv, wire: WireFormat): JsonSchema { const fieldByName = new Map(obj.fields().map((f) => [f.name, f])); const pk = new Set(obj.primaryIdentity()?.fields ?? []); + const denied = writeDenied(obj, env); const properties: Record = {}; for (const shape of updateSchemaFields(obj)) { const field = fieldByName.get(shape.name); - if (!isClientWritten(field, shape.autoSet)) continue; - const schema = fieldSchema(field, "update", env.refFor); + if (!isClientWritten(field, shape.autoSet, denied)) continue; + const schema = fieldSchema(field, "update", env.refFor, wire); // FR-035: a present null clears a non-@required field. A key is never cleared. properties[shape.name] = field.isRequired || pk.has(shape.name) ? schema : withNull(schema); } @@ -133,7 +141,8 @@ function updateSchema(obj: MetaObject, env: ObjectSchemaEnv): JsonSchema { /** The object's schema in one shape, without a document's `$schema` or `$id`. */ export function objectSchema(obj: MetaObject, shape: ContractShape, env: ObjectSchemaEnv): JsonSchema { - const body = shape === "read" ? readSchema(obj, env) : shape === "create" ? createSchema(obj, env) : updateSchema(obj, env); + const wire = wireFormatOf(env.root); + const body = shape === "read" ? readSchema(obj, env, wire) : shape === "create" ? createSchema(obj, env, wire) : updateSchema(obj, env, wire); const description = obj.attr(DOC_ATTR_DESCRIPTION); return { ...(typeof description === "string" && { description }), ...body }; } diff --git a/server/typescript/packages/codegen-ts/src/generator-registry.ts b/server/typescript/packages/codegen-ts/src/generator-registry.ts index a4fda8f178..66ff378e91 100644 --- a/server/typescript/packages/codegen-ts/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts/src/generator-registry.ts @@ -271,7 +271,7 @@ export const generatorRegistry: Record = { tier: "neutral", factory: () => jsonSchemaFile(), factoryExport: "jsonSchemaFile", - options: "profile?, baseUri?, filter?, target?", + options: "profile?, filter?, target?", // The files import nothing, and a validator is whatever the reader already has. requires: [], runtimePackages: [], @@ -288,7 +288,7 @@ export const generatorRegistry: Record = { tier: "neutral", factory: () => openApiFile(), factoryExport: "openApiFile", - options: "title?, version?, description?, servers?, securitySchemes?, security?, path?, expose?, filter?, target?", + options: "title?, version?, description?, servers?, securitySchemes?, security?, expose?, filter?, target?", // The document imports nothing. It reads the routes generators' `expose` when they are wired, // and does not need them: another port's routes answer the same contract. requires: [], diff --git a/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts b/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts index 536caf78e5..a426669f05 100644 --- a/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts +++ b/server/typescript/packages/codegen-ts/src/generators/contract-schema-file.ts @@ -24,8 +24,6 @@ const SHAPE_INFIX: Record = { read: "", create: ".create" export interface JsonSchemaFileOptions { /** `canonical` is full fidelity. `strict` is the structured-output subset. Default: canonical. */ readonly profile?: "canonical" | "strict"; - /** An absolute prefix for each `$id`, when a registry needs one. Default: the file's relative path. */ - readonly baseUri?: string; /** Narrow which objects get a file. An object a kept one references still gets its own. */ readonly filter?: (obj: MetaObject) => boolean; /** Named output target: the files land under its outDir. */ @@ -56,12 +54,11 @@ function renderJson(value: JsonSchema): string { return `${JSON.stringify(value, null, 2)}\n`; } -function canonicalFiles(objects: readonly MetaObject[], root: MetaRoot, baseUri: string | undefined): EmittedFile[] { +function canonicalFiles(objects: readonly MetaObject[], root: MetaRoot): EmittedFile[] { return collectContractEntries(objects, root, relativeRef).map(({ obj, shape, schema }) => { const path = schemaPath(obj, shape); const title = shape === "read" ? obj.name : `${obj.name}${SHAPE_INFIX[shape]}`; - const $id = baseUri === undefined ? path : `${baseUri.replace(/\/$/, "")}/${path}`; - return { path, content: renderJson({ $schema: JSON_SCHEMA_DIALECT, $id, title, ...schema }) }; + return { path, content: renderJson({ $schema: JSON_SCHEMA_DIALECT, $id: path, title, ...schema }) }; }); } @@ -75,7 +72,7 @@ function strictFiles(objects: readonly MetaObject[], root: MetaRoot): EmittedFil function generateSchemaFiles(ctx: GenContext, options: JsonSchemaFileOptions): EmittedFile[] { const root = ctx.loadedRoot; const objects = contractObjects(ctx.entities.filter(ctx.matches), root); - return options.profile === "strict" ? strictFiles(objects, root) : canonicalFiles(objects, root, options.baseUri); + return options.profile === "strict" ? strictFiles(objects, root) : canonicalFiles(objects, root); } /** JSON Schema files for the model's objects, in one profile. List it twice for both. */ diff --git a/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts b/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts index cc16e35b04..15aa24d707 100644 --- a/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts +++ b/server/typescript/packages/codegen-ts/src/generators/openapi-file.ts @@ -15,11 +15,9 @@ import type { MetaObject } from "@metaobjectsdev/metadata"; /** The generator's stable name. */ const GENERATOR_NAME = "openapi"; -const DEFAULT_PATH = "openapi.json"; +const DOCUMENT_PATH = "openapi.json"; export interface OpenApiFileOptions extends OpenApiDocumentOptions { - /** Output path relative to the target's outDir. Default: `openapi.json`. */ - readonly path?: string; /** * The verbs your routes mount, for a project whose routes this run does not generate (a port * other than TypeScript). Default: what the routes generators in this run declare, else all of them. @@ -40,7 +38,7 @@ function documentObjects(ctx: GenContext): MetaObject[] { /** One OpenAPI 3.1 document for the whole model. */ export function openApiFile(options: OpenApiFileOptions = {}): Generator { - const { path, expose, filter, target, ...document } = options; + const { expose, filter, target, ...document } = options; const generator: Generator = { name: GENERATOR_NAME, generate: (ctx) => { @@ -53,11 +51,11 @@ export function openApiFile(options: OpenApiFileOptions = {}): Generator { objects: documentObjects(ctx), apiPrefix: ctx.renderContext?.apiPrefix ?? "", relationMap: ctx.renderContext?.relationMap ?? new Map(), - reportExport: ctx.config.includeReportExport ?? false, + reportExport: ctx.config.reportExportInRun ?? false, ...(surface !== undefined && { expose: surface }), ...(mounted !== undefined && { mounted }), }); - return [{ path: path ?? DEFAULT_PATH, content: `${JSON.stringify(doc, null, 2)}\n` }]; + return [{ path: DOCUMENT_PATH, content: `${JSON.stringify(doc, null, 2)}\n` }]; }, }; if (filter !== undefined) generator.filter = filter; diff --git a/server/typescript/packages/codegen-ts/src/index.ts b/server/typescript/packages/codegen-ts/src/index.ts index fb473b5739..b696f9f0ec 100644 --- a/server/typescript/packages/codegen-ts/src/index.ts +++ b/server/typescript/packages/codegen-ts/src/index.ts @@ -429,21 +429,6 @@ export { withClientDirective, CLIENT_DIRECTIVE } from "./client-directive.js"; export { buildSharedModel, renderSharedModelFiles } from "./shared-model/build-shared-model.js"; export type { SharedModel, SharedModelFiles, SharedModelInput } from "./shared-model/build-shared-model.js"; -// FR-022 — the contract emitters. `jsonSchemaFile()` and `openApiFile()` are the reference -// generators; what they wire is public so an owned (ejected) copy imports only this package. -export { jsonSchemaFile } from "./generators/contract-schema-file.js"; -export type { JsonSchemaFileOptions } from "./generators/contract-schema-file.js"; -export { openApiFile } from "./generators/openapi-file.js"; -export type { OpenApiFileOptions } from "./generators/openapi-file.js"; -export { buildOpenApiDocument, OPENAPI_VERSION } from "./contract/build-openapi.js"; -export type { OpenApiBuildInput, OpenApiDocumentOptions } from "./contract/build-openapi.js"; -export { objectSchema, shapesOf } from "./contract/object-schema.js"; -export { fieldSchema, withNull } from "./contract/field-schema.js"; -export type { ContractShape, JsonSchema } from "./contract/field-schema.js"; -export { strictSchema } from "./contract/strict-profile.js"; -export { ContractEmitError, CONTRACT_ERROR_CODES, ERR_CONTRACT_NAME_COLLISION, ERR_CONTRACT_STRICT_UNREPRESENTABLE } from "./contract/contract-errors.js"; -export type { ContractErrorCode } from "./contract/contract-errors.js"; - // FR-044 Plan 4 — the Cube exporter. `cubeModel()` is the reference generator; the two pure // stages it wires are public so an owned (ejected) copy imports only this package and keeps // only the parts it owns: which entities get a cube, the file layout and the YAML text. diff --git a/server/typescript/packages/codegen-ts/src/metaobjects-config.ts b/server/typescript/packages/codegen-ts/src/metaobjects-config.ts index 43362b28be..6fdd32a6ba 100644 --- a/server/typescript/packages/codegen-ts/src/metaobjects-config.ts +++ b/server/typescript/packages/codegen-ts/src/metaobjects-config.ts @@ -127,6 +127,9 @@ export interface ResolvedGenConfig { * the suite's `emitsReportExport` markers). A report's routes mount the `?format=` branch, and * import the `.export` module, only when this is true. Undefined => false. */ includeReportExport?: boolean; + /** Whether the `report-export` generator is active in ANY target of the run: the same fact for a + * generator that describes the routes from a target of its own. Undefined => false. */ + reportExportInRun?: boolean; /** Whether any CLIENT UI generator (form, hook, grid, columns) is active in the * run — aggregated by the runner from the suite's `emitsUiTier` markers. * `agent/ui.md` reads this to decide whether a UI tier exists to describe; @@ -164,7 +167,7 @@ export const DEFAULT_EXT_STYLE: ExtStyle = "js"; * fills inert defaults when they are absent AND the model emits no DB artifacts, and * throws a clear error when they are absent but the model DOES emit DB code (#194). */ -export interface MetaobjectsGenConfig extends Omit { +export interface MetaobjectsGenConfig extends Omit { dbImport?: string; dialect?: Dialect; /** Import-specifier suffix in generated code. Defaults to `"js"` (`./x.js`), which is diff --git a/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts b/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts index fe2cfdae30..3d2d9ecd96 100644 --- a/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts +++ b/server/typescript/packages/codegen-ts/src/reference/contract-schema.ts @@ -49,8 +49,6 @@ const SHAPE_INFIX: Record = { read: "", create: ".create" export interface JsonSchemaFileOptions { /** `canonical` is full fidelity. `strict` is the structured-output subset. Default: canonical. */ readonly profile?: "canonical" | "strict"; - /** An absolute prefix for each `$id`, when a registry needs one. Default: the file's relative path. */ - readonly baseUri?: string; /** Narrow which objects get a file. An object a kept one references still gets its own. */ readonly filter?: (obj: MetaObject) => boolean; /** Named output target: the files land under its outDir. */ @@ -81,12 +79,11 @@ function renderJson(value: JsonSchema): string { return `${JSON.stringify(value, null, 2)}\n`; } -function canonicalFiles(objects: readonly MetaObject[], root: MetaRoot, baseUri: string | undefined): EmittedFile[] { +function canonicalFiles(objects: readonly MetaObject[], root: MetaRoot): EmittedFile[] { return collectContractEntries(objects, root, relativeRef).map(({ obj, shape, schema }) => { const path = schemaPath(obj, shape); const title = shape === "read" ? obj.name : `${obj.name}${SHAPE_INFIX[shape]}`; - const $id = baseUri === undefined ? path : `${baseUri.replace(/\/$/, "")}/${path}`; - return { path, content: renderJson({ $schema: JSON_SCHEMA_DIALECT, $id, title, ...schema }) }; + return { path, content: renderJson({ $schema: JSON_SCHEMA_DIALECT, $id: path, title, ...schema }) }; }); } @@ -100,7 +97,7 @@ function strictFiles(objects: readonly MetaObject[], root: MetaRoot): EmittedFil function generateSchemaFiles(ctx: GenContext, options: JsonSchemaFileOptions): EmittedFile[] { const root = ctx.loadedRoot; const objects = contractObjects(ctx.entities.filter(ctx.matches), root); - return options.profile === "strict" ? strictFiles(objects, root) : canonicalFiles(objects, root, options.baseUri); + return options.profile === "strict" ? strictFiles(objects, root) : canonicalFiles(objects, root); } /** JSON Schema files for the model's objects, in one profile. List it twice for both. */ diff --git a/server/typescript/packages/codegen-ts/src/reference/openapi.ts b/server/typescript/packages/codegen-ts/src/reference/openapi.ts index cc0d9772d2..f3a3293123 100644 --- a/server/typescript/packages/codegen-ts/src/reference/openapi.ts +++ b/server/typescript/packages/codegen-ts/src/reference/openapi.ts @@ -14,7 +14,7 @@ // filter, sort and paging parameters, the error envelope, and the model's wire // shapes as components. // customize: which objects it covers (`documentObjects`, narrowed by the `filter` option) and -// where the file lands (the `path` option). Copied beside it, so yours too: +// where the file lands (the `target` option). Copied beside it, so yours too: // `buildOpenApiDocument`, which decides which operations exist and what each one // answers. If your API has routes of its own, that is the file to change. // composes-with: contract-schema, whose canonical schemas are these components. Reads what the @@ -40,11 +40,9 @@ import type { MetaObject } from "@metaobjectsdev/metadata"; /** The generator's stable name. */ const GENERATOR_NAME = "openapi"; -const DEFAULT_PATH = "openapi.json"; +const DOCUMENT_PATH = "openapi.json"; export interface OpenApiFileOptions extends OpenApiDocumentOptions { - /** Output path relative to the target's outDir. Default: `openapi.json`. */ - readonly path?: string; /** * The verbs your routes mount, for a project whose routes this run does not generate (a port * other than TypeScript). Default: what the routes generators in this run declare, else all of them. @@ -65,7 +63,7 @@ function documentObjects(ctx: GenContext): MetaObject[] { /** One OpenAPI 3.1 document for the whole model. */ export function openApiFile(options: OpenApiFileOptions = {}): Generator { - const { path, expose, filter, target, ...document } = options; + const { expose, filter, target, ...document } = options; const generator: Generator = { name: GENERATOR_NAME, generate: (ctx) => { @@ -78,11 +76,11 @@ export function openApiFile(options: OpenApiFileOptions = {}): Generator { objects: documentObjects(ctx), apiPrefix: ctx.renderContext?.apiPrefix ?? "", relationMap: ctx.renderContext?.relationMap ?? new Map(), - reportExport: ctx.config.includeReportExport ?? false, + reportExport: ctx.config.reportExportInRun ?? false, ...(surface !== undefined && { expose: surface }), ...(mounted !== undefined && { mounted }), }); - return [{ path: path ?? DEFAULT_PATH, content: `${JSON.stringify(doc, null, 2)}\n` }]; + return [{ path: DOCUMENT_PATH, content: `${JSON.stringify(doc, null, 2)}\n` }]; }, }; if (filter !== undefined) generator.filter = filter; diff --git a/server/typescript/packages/codegen-ts/src/runner.ts b/server/typescript/packages/codegen-ts/src/runner.ts index b91801fa17..78e84b97e1 100644 --- a/server/typescript/packages/codegen-ts/src/runner.ts +++ b/server/typescript/packages/codegen-ts/src/runner.ts @@ -887,6 +887,7 @@ export async function runGen(opts: RunGenOpts): Promise { includeRestRoutes, includeNames: namesTargets.has(selfTarget.name), includeReportExport: reportExportTargets.has(selfTarget.name), + reportExportInRun: reportExportTargets.size > 0, includeUiTier, includeUiTierExplicit, ...(routesExpose !== undefined && { routesExpose }), diff --git a/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts b/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts index ea189296d6..310aa57c52 100644 --- a/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts +++ b/server/typescript/packages/codegen-ts/test/contract/contract-behaviours.test.ts @@ -10,7 +10,7 @@ import type { MetaObject } from "@metaobjectsdev/metadata"; import { type ContractCase, contractCases, contractTree, loadCaseModel } from "../../scripts/gen-contract-conformance.js"; import { readTree } from "../../scripts/gen-cube-model-canonical.js"; import { DECIMAL_CANONICAL_PATTERN } from "../../src/contract/field-schema.js"; -import { jsonSchemaFile, openApiFile } from "../../src/generators/index.js"; +import { jsonSchemaFile, openApiFile, reportExport, routesFile } from "../../src/generators/index.js"; import { defineConfig, type Generator, runGen } from "../../src/index.js"; import { jsonSchemaFile as refJsonSchemaFile } from "../../src/reference/contract-schema.js"; import { openApiFile as refOpenApiFile } from "../../src/reference/openapi.js"; @@ -26,6 +26,8 @@ function corpusCase(name: string): ContractCase { interface RunOptions { entityFilter?: string[]; + /** Named targets, each landing in a directory of its own beside the default one. */ + targets?: string[]; } async function emit(c: ContractCase, generators: Generator[], run: RunOptions = {}): Promise> { @@ -34,7 +36,14 @@ async function emit(c: ContractCase, generators: Generator[], run: RunOptions = try { const outDir = join(dir, "out"); await runGen({ - config: defineConfig({ outDir, dialect: "postgres", apiPrefix: "/api", generators }), + config: defineConfig({ + outDir, + dialect: "postgres", + dbImport: "./db", + apiPrefix: "/api", + generators, + ...(run.targets !== undefined && { targets: Object.fromEntries(run.targets.map((t) => [t, { outDir: join(dir, t) }])) }), + }), metadata: root, genStateDir: join(dir, ".gen-state"), ...(run.entityFilter !== undefined && { entityFilter: run.entityFilter }), @@ -60,6 +69,15 @@ function validatorOf(tree: ReadonlyMap, path: string): (value: u return (value) => validate(value) as boolean; } +/** Whether the document's report list takes `format`, and answers CSV. */ +function reportExportOn(tree: ReadonlyMap): boolean { + const paths = jsonAt(tree, "openapi.json")["paths"] as Record } }>; + const { get } = paths["/api/invoice_totals"]!; + const format = get.parameters.some((p) => p.name === "format"); + expect("text/csv" in get.responses["200"]!.content).toBe(format); + return format; +} + const ALL_FIELDS = "schemas/acme/contract/AllFields"; const ADDRESS = "schemas/acme/contract/Address"; @@ -104,6 +122,21 @@ describe("a read answers what the wire setting says", () => { for (const bad of ["1e3", "+1", "1.", ".5", "01", "1.50"]) expect(canonical.test(bad), bad).toBe(false); }); + test("a report's decimal follows the setting, as an entity's does", async () => { + const decimal = corpusCase("decimal"); + const ledger = "schemas/acme/ledger"; + const pinned = await contractTree({ ...decimal, wire: { decimals: "string" } }); + const native = await contractTree({ ...decimal, wire: { decimals: "native" } }); + const total = (tree: ReadonlyMap, object: string): JsonDoc => + (jsonAt(tree, `${ledger}/${object}.schema.json`)["properties"] as Record)["total"]!; + expect(total(pinned, "EntryTotals")).toEqual({ type: ["string", "null"], pattern: DECIMAL_CANONICAL_PATTERN }); + expect(total(native, "EntryTotals")["type"]).toEqual(["string", "number", "null"]); + const inDocument = (jsonAt(pinned, "openapi.json")["components"] as { schemas: Record }> }).schemas; + expect(inDocument["EntryTotals"]!.properties["total"]).toEqual(total(pinned, "EntryTotals")); + const strict = (jsonAt(pinned, `${ledger}/EntryTotals.strict.schema.json`)["properties"] as Record)["total"]!; + expect(strict["type"]).toEqual(["string", "null"]); + }); + test("nullMembers: omit leaves a null member out of a value object", async () => { const valid = validatorOf(await contractTree(corpusCase("all-fields-wire-string-omit")), `${ADDRESS}.schema.json`); expect(valid({ street: "1 Main" })).toBe(true); @@ -163,12 +196,15 @@ describe("a write takes what a route takes", () => { }); test("a report's list route takes format only when the project wires report-export", async () => { - const formatOn = (tree: ReadonlyMap): boolean => { - const list = (jsonAt(tree, "openapi.json")["paths"] as Record)["/api/invoice_totals"]!; - return list.get.parameters.some((p) => p.name === "format"); - }; - expect(formatOn(await contractTree(corpusCase("report")))).toBe(true); - expect(formatOn(await emit(corpusCase("report"), [openApiFile({ title: "t", version: "1" })]))).toBe(false); + expect(reportExportOn(await contractTree(corpusCase("report")))).toBe(true); + expect(reportExportOn(await emit(corpusCase("report"), [openApiFile({ title: "t", version: "1" })]))).toBe(false); + }); + + test("the document states format when report-export lands in another target", async () => { + for (const factory of [openApiFile, refOpenApiFile]) { + const tree = await emit(corpusCase("report"), [factory({ title: "t", version: "1" }), reportExport({ target: "api" })], { targets: ["api"] }); + expect(reportExportOn(tree)).toBe(true); + } }); }); @@ -208,6 +244,26 @@ describe("the three shapes", () => { expect(tree.get("openapi.json")).not.toContain("pinHash"); }); + test("a mark on one subtype's column holds on every subtype, as the routes hold it", async () => { + const c = corpusCase("tph-shared-marks"); + const tree = await contractTree(c); + const keysOf = (path: string): string[] => Object.keys(jsonAt(tree, path)["properties"] as JsonDoc); + const copay = "schemas/acme/auth/CopayAuth"; + const bridge = "schemas/acme/auth/BridgeAuth"; + const row = { id: 1, type: "Copay", reference: "R-1", auditRef: "A-1" }; + expect(validatorOf(tree, `${copay}.schema.json`)(row)).toBe(true); + expect(keysOf(`${copay}.schema.json`)).not.toContain("token"); + expect(keysOf(`${bridge}.schema.json`)).not.toContain("token"); + for (const shape of ["create", "update"]) { + expect(keysOf(`${copay}.${shape}.schema.json`)).toEqual(["reference"]); + expect(keysOf(`${bridge}.${shape}.schema.json`)).toEqual(["reference"]); + } + + const routes = [...(await emit(c, [routesFile()])).values()].join("\n"); + expect(routes).toContain(`hiddenColumns: ["token"]`); + expect(routes).toContain(`writeDeniedColumns: ["token", "auditRef"]`); + }); + test("a projection and a report read, and nothing writes them", async () => { const tree = await contractTree(corpusCase("projection")); const paths = [...tree.keys()]; @@ -279,14 +335,6 @@ describe("what a project's run changes", () => { expect(verbsOf("/api/authors/{id}")).toEqual(["get"]); }); - test("baseUri prefixes every $id, and the relative $refs still resolve", async () => { - const baseUri = "https://schemas.example.com/v1/"; - const tree = await emit(corpusCase("all-fields"), [jsonSchemaFile({ baseUri })]); - const id = jsonAt(tree, `${ALL_FIELDS}.schema.json`)["$id"] as string; - expect(id).toBe(`${baseUri}${ALL_FIELDS}.schema.json`); - validatorOf(tree, `${ALL_FIELDS}.schema.json`); - }); - test("title, version, servers and security are options, and pass through untouched", async () => { const tree = await emit(corpusCase("blog"), [openApiFile({ title: "Blog API", @@ -303,13 +351,9 @@ describe("what a project's run changes", () => { expect((doc["components"] as JsonDoc)["securitySchemes"]).toEqual({ bearer: { type: "http", scheme: "bearer" } }); }); - test("the path option moves the document, and the strict profile writes only .strict files", async () => { - const tree = await emit(corpusCase("blog"), [ - openApiFile({ title: "t", version: "1", path: "api/openapi.json" }), - jsonSchemaFile({ profile: "strict" }), - ]); - expect(tree.has("api/openapi.json")).toBe(true); - const schemas = [...tree.keys()].filter((p) => p.endsWith(".json") && p !== "api/openapi.json"); + test("the strict profile writes only .strict files", async () => { + const tree = await emit(corpusCase("blog"), [jsonSchemaFile({ profile: "strict" })]); + const schemas = [...tree.keys()].filter((p) => p.endsWith(".json")); expect(schemas.length).toBeGreaterThan(0); for (const p of schemas) expect(p.endsWith(".strict.schema.json"), p).toBe(true); }); diff --git a/server/typescript/packages/codegen-ts/test/metaobjects-config.test.ts b/server/typescript/packages/codegen-ts/test/metaobjects-config.test.ts index 78b919964c..ad1288b8b1 100644 --- a/server/typescript/packages/codegen-ts/test/metaobjects-config.test.ts +++ b/server/typescript/packages/codegen-ts/test/metaobjects-config.test.ts @@ -75,7 +75,7 @@ describe("defineConfig", () => { test("type-level: MetaobjectsGenConfig embeds ResolvedGenConfig's non-DB fields exactly (#194 — dbImport/dialect are OPTIONAL here, filled by the runner)", () => { // Everything except dbImport/dialect matches ResolvedGenConfig field-for-field. expectTypeOf>() - .toEqualTypeOf>(); + .toEqualTypeOf>(); // extStyle is OPTIONAL too: the config `meta init` scaffolds omits it and must still // typecheck; the runner fills DEFAULT_EXT_STYLE. expectTypeOf().toEqualTypeOf(); From 53bdbc5e17d6a3cfcacd4b6787d6eaa4dd668d82 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sat, 31 Oct 2026 03:54:09 -0400 Subject: [PATCH 6/6] no-mistakes(document): Fixed three stale OpenAPI/JSON Schema doc claims and regenerated agent-context fixtures. Other docs already matched the change. Edits left uncommitted. --- agent-context/skills/metaobjects-codegen/SKILL.md | 2 +- agent-context/templates/always-on.md.mustache | 2 +- docs/features/codegen-data-shapes.md | 2 +- .../expected/.claude/skills/metaobjects-codegen/SKILL.md | 2 +- .../java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md | 2 +- .../java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md | 2 +- .../expected/.claude/skills/metaobjects-codegen/SKILL.md | 2 +- .../java-react/expected/.metaobjects/AGENTS.md | 2 +- .../java-react/expected/.metaobjects/CLAUDE.md | 2 +- .../python/expected/.claude/skills/metaobjects-codegen/SKILL.md | 2 +- .../python/expected/.metaobjects/AGENTS.md | 2 +- .../python/expected/.metaobjects/CLAUDE.md | 2 +- .../expected/.claude/skills/metaobjects-codegen/SKILL.md | 2 +- .../ts-react-tanstack/expected/.metaobjects/AGENTS.md | 2 +- .../ts-react-tanstack/expected/.metaobjects/CLAUDE.md | 2 +- .../expected/.claude/skills/metaobjects-codegen/SKILL.md | 2 +- .../ts-requirements/expected/.metaobjects/AGENTS.md | 2 +- .../ts-requirements/expected/.metaobjects/CLAUDE.md | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index 3014906daf..a030b08545 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -1,6 +1,6 @@ --- name: metaobjects-codegen -description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. +description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. --- # MetaObjects code generation diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index 09cf315d86..260d02e3f6 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -62,7 +62,7 @@ from. - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/docs/features/codegen-data-shapes.md b/docs/features/codegen-data-shapes.md index 0da632291f..8841b258ff 100644 --- a/docs/features/codegen-data-shapes.md +++ b/docs/features/codegen-data-shapes.md @@ -218,7 +218,7 @@ section MUST appear in every entity's docs output. Optional fields use ## Adding new template families -To add a new shape (e.g. for an HTML docs site, an OpenAPI spec, or a +To add a new shape (e.g. for an HTML docs site, an AsyncAPI spec, or a Mermaid diagram): 1. Export a typed `interface XxxData` from a sibling module of diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 3014906daf..a030b08545 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -1,6 +1,6 @@ --- name: metaobjects-codegen -description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. +description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. --- # MetaObjects code generation diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md index f1fd92f374..d34c780c8f 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md index f1fd92f374..d34c780c8f 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index 3014906daf..a030b08545 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -1,6 +1,6 @@ --- name: metaobjects-codegen -description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. +description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. --- # MetaObjects code generation diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md index 129bbcd196..60118bd559 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md index 129bbcd196..60118bd559 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index 3014906daf..a030b08545 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -1,6 +1,6 @@ --- name: metaobjects-codegen -description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. +description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. --- # MetaObjects code generation diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index f405af6956..49fe8148af 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -37,7 +37,7 @@ from. - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md index f405af6956..49fe8148af 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md @@ -37,7 +37,7 @@ from. - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 3014906daf..a030b08545 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -1,6 +1,6 @@ --- name: metaobjects-codegen -description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. +description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. --- # MetaObjects code generation diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md index da8f6cc1c2..a296481115 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md index da8f6cc1c2..a296481115 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md index 3014906daf..a030b08545 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -1,6 +1,6 @@ --- name: metaobjects-codegen -description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. +description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration. --- # MetaObjects code generation diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md index ea69ee556f..29d77fc749 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md index ea69ee556f..29d77fc749 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md @@ -40,7 +40,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. - **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. - **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours.