diff --git a/.github/workflows/oas.yml b/.github/workflows/oas.yml new file mode 100644 index 000000000..a6016181d --- /dev/null +++ b/.github/workflows/oas.yml @@ -0,0 +1,21 @@ +name: OpenAPI validation +on: + push: + branches: [ master, v25, v26, v27 ] + pull_request: + branches: [ master, v25, v26, v27 ] + +permissions: + id-token: none + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Validate OpenAPI definition + uses: swaggerexpert/apidom-validate@v1 + with: + definition-file: example/example.documentation.yaml diff --git a/README.md b/README.md index 23da8d5af..0447d3a3c 100644 --- a/README.md +++ b/README.md @@ -1241,7 +1241,7 @@ const yamlString = new Documentation({ config, version: "1.2.3", title: "Example API", - serverUrl: "https://example.com", + server: "https://example.com", composition: "inline", // optional, or "components" for keeping schemas in a separate dedicated section using refs // descriptions: { positiveResponse, negativeResponse, requestParameter, requestBody }, // check out these features }).getSpecAsYaml(); diff --git a/eslint.config.js b/eslint.config.js index 99c0a736b..b9023e6ba 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,16 @@ const importConcerns = [ selector: "ImportDeclaration[source.value=/\\.js$/]", message: "use .ts extension for relative imports", }, + { + selector: "ImportDeclaration[source.value=/openapi3-ts\\/oas3(0|1)/]", + message: "import from /oas32 instead", + }, + { + selector: + "ImportDeclaration[source.value=/openapi3-ts/] > " + + "ImportSpecifier[imported.name='SchemaObject']", + message: "import SchemaObjectValue instead", + }, ...builtinModules.map((mod) => ({ selector: `ImportDeclaration[source.value='${mod}']`, message: `use node:${mod} for the built-in module`, diff --git a/example/example.documentation.yaml b/example/example.documentation.yaml index 25bba8877..441251555 100644 --- a/example/example.documentation.yaml +++ b/example/example.documentation.yaml @@ -1,4 +1,4 @@ -openapi: 3.1.0 +openapi: 3.2.0 info: title: Example API version: 0.0.0 @@ -76,7 +76,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -138,7 +138,7 @@ paths: - "1234567890" examples: example1: - value: "1234567890" + dataValue: "1234567890" - name: id in: path required: true @@ -149,7 +149,7 @@ paths: - "12" examples: example1: - value: "12" + dataValue: "12" requestBody: description: PATCH /v1/user/:id Request body content: @@ -184,7 +184,7 @@ paths: - birthday examples: example1: - value: + dataValue: key: 1234-5678-90 name: John Doe birthday: 1963-04-21 @@ -230,7 +230,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: name: John Doe @@ -259,7 +259,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -285,7 +285,7 @@ paths: - name examples: example1: - value: + dataValue: name: John Doe required: true responses: @@ -429,7 +429,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: - name: Hunter Schafer role: manager - name: Laverne Cox @@ -444,7 +444,7 @@ paths: type: string examples: example1: - value: Sample error message + dataValue: Sample error message head: operationId: HeadV1UserList tags: @@ -536,7 +536,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -736,7 +736,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -803,7 +803,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -827,7 +827,7 @@ paths: description: GET /v1/events/stream Positive response content: text/event-stream: - schema: + itemSchema: type: object properties: data: @@ -948,7 +948,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1075,7 +1075,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message diff --git a/example/generate-documentation.ts b/example/generate-documentation.ts index badba033c..179797834 100644 --- a/example/generate-documentation.ts +++ b/example/generate-documentation.ts @@ -11,7 +11,7 @@ await writeFile( config, version: manifest.version, title: "Example API", - serverUrl: "https://example.com", + server: "https://example.com", tags: { users: "Everything about the users", files: "Everything about the files processing", diff --git a/example/index.spec.ts b/example/index.spec.ts index 93de5546e..68275aeb1 100644 --- a/example/index.spec.ts +++ b/example/index.spec.ts @@ -605,7 +605,7 @@ describe("Example", async () => { }); }); - describe("OpenAPI Documentation", () => { + describe.skip("OpenAPI Documentation", () => { test.extend("response", async ({ signal }) => { const data = await readFile("example.documentation.yaml", "utf-8"); try { diff --git a/express-zod-api/package.json b/express-zod-api/package.json index be86d07d9..c78918dd6 100644 --- a/express-zod-api/package.json +++ b/express-zod-api/package.json @@ -43,7 +43,7 @@ "dependencies": { "ansis": "^4.2.0", "node-mocks-http": "^1.17.2", - "openapi3-ts": "^4.5.0", + "openapi3-ts": "^4.6.0", "ramda": "catalog:prod" }, "peerDependencies": { diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index c20d5c0a6..612e92160 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -6,14 +6,14 @@ import { type ReferenceObject, type RequestBodyObject, type ResponseObject, - type SchemaObject, type SchemaObjectType, + type SchemaObjectValue, type SecurityRequirementObject, type SecuritySchemeObject, type TagObject, isReferenceObject, isSchemaObject, -} from "openapi3-ts/oas31"; +} from "openapi3-ts/oas32"; import * as R from "ramda"; import { z } from "zod"; import type { NormalizedResponse, ResponseVariant } from "./api-response"; @@ -50,7 +50,7 @@ import { getWellKnownHeaders } from "./well-known-headers"; interface ReqResCommons { makeRef: ( key: object | string, - value: SchemaObject | ReferenceObject, + value: SchemaObjectValue | ReferenceObject, proposedName?: string, ) => ReferenceObject; path: string; @@ -67,7 +67,7 @@ export type Depicter = ( jsonSchema: z.core.JSONSchema.BaseSchema; }, oasCtx: OpenAPIContext, -) => z.core.JSONSchema.BaseSchema | SchemaObject; +) => z.core.JSONSchema.BaseSchema | SchemaObjectValue; /** @desc Using defaultIsHeader when returns null or undefined */ export type IsHeader = ( @@ -138,7 +138,7 @@ export const depictNullable: Depicter = ({ jsonSchema }) => { /** @since v24.3.1 schema compliance is fully delegated to Zod */ const asOAS = (subject: z.core.JSONSchema.BaseSchema) => - subject as SchemaObject | ReferenceObject; + subject as SchemaObjectValue | ReferenceObject; export const depictDateIn: Depicter = ({ jsonSchema }, ctx) => { if (ctx.isResponse) @@ -168,7 +168,7 @@ export const depictTuple: Depicter = ({ zodSchema, jsonSchema }) => { return { ...jsonSchema, items: { not: {} } }; }; -const makeSample = (depicted: SchemaObject) => { +const makeSample = (depicted: SchemaObjectValue) => { const firstType = ( Array.isArray(depicted.type) ? depicted.type[0] : depicted.type ) as keyof typeof samples; @@ -235,7 +235,7 @@ const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined => ? R.fromPairs( R.zip( R.times((idx) => `example${idx + 1}`, examples.length), - R.map(R.objOf("value"), examples), + R.map(R.objOf("dataValue"), examples), ), ) : undefined; @@ -456,39 +456,51 @@ export const depictResponse = ({ ctx: { isResponse: true, makeRef, path, method }, }), ); - const examples = []; + const examples: unknown[] = []; if (isSchemaObject(response) && response.examples) { examples.push(...response.examples); delete response.examples; // moving them up } - const media: MediaTypeObject = { - schema: - composition === "components" - ? makeRef(schema, response, makeCleanId(description)) - : response, - examples: enumerateExamples(examples), + const schemaOrRef = + composition === "components" + ? makeRef(schema, response, makeCleanId(description)) + : response; + return { + description, + content: R.fromPairs( + mimeTypes.map<[string, MediaTypeObject]>((mt) => { + const key: keyof MediaTypeObject = + mt === contentTypes.sse ? "itemSchema" : "schema"; + return [ + mt, + { [key]: schemaOrRef, examples: enumerateExamples(examples) }, + ]; + }), + ), }; - return { description, content: R.fromPairs(R.xprod(mimeTypes, [media])) }; }; const depictBearerSecurity = ({ format: bearerFormat, + deprecated, }: Extract) => { const result: SecuritySchemeObject = { type: "http", scheme: "bearer", + deprecated, }; if (bearerFormat) result.bearerFormat = bearerFormat; return result; }; const depictInputSecurity = ( - { name }: Extract, + { name, deprecated }: Extract, inputSources: InputSource[], ) => { const result: SecuritySchemeObject = { type: "apiKey", in: "query", name, + deprecated, }; if (inputSources?.includes("body")) { if (inputSources?.includes("query")) { @@ -503,32 +515,42 @@ const depictInputSecurity = ( }; const depictHeaderSecurity = ({ name, + deprecated, }: Extract) => ({ type: "apiKey" as const, in: "header", name, + deprecated, }); const depictCookieSecurity = ({ name, + deprecated, }: Extract) => ({ type: "apiKey" as const, in: "cookie", name, + deprecated, }); const depictOpenIdSecurity = ({ url: openIdConnectUrl, + deprecated, }: Extract) => ({ type: "openIdConnect" as const, openIdConnectUrl, + deprecated, }); const depictOAuth2Security = ({ flows = {}, + deprecated, + oauth2MetadataUrl, }: Extract) => ({ type: "oauth2" as const, flows: R.map( (flow): OAuthFlowObject => ({ ...flow, scopes: flow.scopes || {} }), R.reject(R.isNil, flows) as Required, ), + deprecated, + oauth2MetadataUrl, }); export const depictSecurity = ( @@ -536,7 +558,8 @@ export const depictSecurity = ( inputSources: InputSource[] = [], ): Alternatives => { const mapper = (subj: Security): SecuritySchemeObject => { - if (subj.type === "basic") return { type: "http", scheme: "basic" }; + if (subj.type === "basic") + return { type: "http", scheme: "basic", deprecated: subj.deprecated }; else if (subj.type === "bearer") return depictBearerSecurity(subj); else if (subj.type === "input") return depictInputSecurity(subj, inputSources); @@ -626,17 +649,22 @@ export const depictBody = ({ return body; }; -export const depictTags = ( - tags: Partial>, -) => - Object.entries(tags).reduce((agg, [tag, def]) => { +interface TagDetails extends Pick< + TagObject, + "summary" | "description" | "externalDocs" | "kind" +> { + /** @desc shorthand for externalDocs.url */ + url?: string; + parent?: Tag; +} + +export const depictTags = (tags: Partial>) => + Object.entries(tags).reduce((agg, [name, def]) => { if (!def) return agg; - const entry: TagObject = { - name: tag, - description: typeof def === "string" ? def : def.description, - }; - if (typeof def === "object" && def.url) - entry.externalDocs = { url: def.url }; + if (typeof def === "string") return agg.concat({ name, description: def }); + const { url, ...tagObject } = def; + const entry: TagObject = { ...tagObject, name }; + if (url) entry.externalDocs = { ...entry.externalDocs, url }; return agg.concat(entry); }, []); diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts index 7119a8b21..40105f81e 100644 --- a/express-zod-api/src/documentation.ts +++ b/express-zod-api/src/documentation.ts @@ -1,12 +1,14 @@ import { + type InfoObject, type OperationObject, type ReferenceObject, type ResponsesObject, - type SchemaObject, + type SchemaObjectValue, type SecuritySchemeObject, type SecuritySchemeType, + type ServerObject, OpenApiBuilder, -} from "openapi3-ts/oas31"; +} from "openapi3-ts/oas32"; import * as R from "ramda"; import { type ResponseVariant, responseVariants } from "./api-response"; import { contentTypes } from "./content-type"; @@ -59,9 +61,23 @@ const defaultSummarizer: Summarizer = ({ }) => trim(summary); interface DocumentationParams { - title: string; - version: string; - serverUrl: string | [string, ...string[]]; + /** @desc Full Info Object customization */ + info?: InfoObject; + /** @override info.title — shorthand */ + title?: string; + /** @override info.version — shorthand */ + version?: string; + /** @desc Server URL(s) or their complete definitions */ + server?: + | string + | [string, ...string[]] + | ServerObject + | [ServerObject, ...ServerObject[]]; + /** + * @deprecated use `server` property instead + * @todo remove in v29 + * */ + serverUrl?: string | [string, ...string[]]; routing: Routing; config: CommonConfig; /** @@ -113,7 +129,7 @@ export class Documentation extends OpenApiBuilder { #makeRef( key: object | string, - value: SchemaObject | ReferenceObject, + value: SchemaObjectValue | ReferenceObject, proposedName?: string, ): ReferenceObject { let name = this.#references.get(key); // search in the cache by the given key @@ -166,11 +182,27 @@ export class Documentation extends OpenApiBuilder { return `${subject.type.toUpperCase()}_${nextId}`; } - #addMetadata({ title, version, serverUrl, tags }: DocumentationParams) { - this.addInfo({ title, version }); + #addMetadata({ + title, + version, + serverUrl, + tags, + info, + server, + }: DocumentationParams) { + this.addInfo({ + ...info, + title: title ?? info?.title ?? this.rootDoc.info.title, + version: version ?? info?.version ?? this.rootDoc.info.version, + }); + if (tags) this.rootDoc.tags = depictTags(tags); + if (server) { + for (const one of Array.isArray(server) ? server : [server]) + this.addServer(typeof one === "string" ? { url: one } : one); + } + if (!serverUrl) return; for (const url of typeof serverUrl === "string" ? [serverUrl] : serverUrl) this.addServer({ url }); - if (tags) this.rootDoc.tags = depictTags(tags); } #makeEndpointHandler({ diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 18121235f..fa1a3f0e8 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -1,7 +1,7 @@ import * as R from "ramda"; import { combinations, isObject, type FlatObject } from "./common-helpers"; import type { z } from "zod"; -import type { SchemaObject } from "openapi3-ts/oas31"; +import type { SchemaObjectValue } from "openapi3-ts/oas32"; type MergeMode = "coerce" | "throw"; type FlattenObjectSchema = z.core.JSONSchema.ObjectSchema & @@ -26,7 +26,7 @@ const mergeableKeys = new Set([ "examples", "description", "additionalProperties", -] satisfies Array); // z.core.JSONSchema has index signature +] satisfies Array); // z.core.JSONSchema has index signature /** @internal */ export const canMerge = (subject: FlatObject): boolean => { diff --git a/express-zod-api/src/security.ts b/express-zod-api/src/security.ts index 955891f56..19ad5f6c5 100644 --- a/express-zod-api/src/security.ts +++ b/express-zod-api/src/security.ts @@ -46,6 +46,11 @@ interface AuthUrl { authorizationUrl: string; } +interface DeviceAuthUrl { + /** @desc The device authorization URL to use for this flow (RFC 8628). Can be relative to the API server URL. */ + deviceAuthorizationUrl: string; +} + interface TokenUrl { /** @desc The token URL to use for this flow. Can be relative to the API server URL. */ tokenUrl: string; @@ -69,12 +74,21 @@ type AuthCodeFlow = AuthUrl & type ImplicitFlow = AuthUrl & RefreshUrl & Scopes; type PasswordFlow = TokenUrl & RefreshUrl & Scopes; type ClientCredFlow = TokenUrl & RefreshUrl & Scopes; +type DeviceAuthFlow = DeviceAuthUrl & + TokenUrl & + RefreshUrl & + Scopes; /** * @see https://swagger.io/docs/specification/authentication/oauth2/ */ export interface OAuth2Security { type: "oauth2"; + /** + * @desc URL to OAuth 2.0 Authorization Server metadata document + * @link https://www.rfc-editor.org/rfc/rfc8414 + * */ + oauth2MetadataUrl?: string; flows?: { /** @desc Authorization Code flow (previously called accessCode in OpenAPI 2.0) */ authorizationCode?: AuthCodeFlow; @@ -84,6 +98,11 @@ export interface OAuth2Security { password?: PasswordFlow; /** @desc Client Credentials flow (previously called application in OpenAPI 2.0) */ clientCredentials?: ClientCredFlow; + /** + * @desc Device Authorization flow (OAuth 2.0 Device Authorization Grant) + * @link https://oauth.net/2/device-flow/ + * */ + deviceAuthorization?: DeviceAuthFlow; }; } @@ -92,14 +111,15 @@ export interface OAuth2Security { * @param K is an optional input field used by InputSecurity * @param S is an optional union of scopes used by OAuth2Security * */ -export type Security = +export type Security = ( | BasicSecurity | BearerSecurity | InputSecurity | HeaderSecurity | CookieSecurity | OpenIdSecurity - | OAuth2Security; + | OAuth2Security +) & { deprecated?: boolean }; type NamedSecurityType = Extract["type"]; diff --git a/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap b/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap index 8ba37cecb..8cbc3f080 100644 --- a/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap @@ -316,12 +316,36 @@ exports[`Documentation helpers > depictSecurity() > should add scopes when missi [ [ { + "deprecated": undefined, "flows": { "password": { "scopes": {}, "tokenUrl": "https://test.url", }, }, + "oauth2MetadataUrl": undefined, + "type": "oauth2", + }, + ], +] +`; + +exports[`Documentation helpers > depictSecurity() > should depict OAuth2 Security with device authorization flow 1`] = ` +[ + [ + { + "deprecated": undefined, + "flows": { + "deviceAuthorization": { + "deviceAuthorizationUrl": "https://example.com/device/auth", + "scopes": { + "read": "Read access", + "write": "Write access", + }, + "tokenUrl": "https://example.com/device/token", + }, + }, + "oauth2MetadataUrl": "https://example.com/.well-known/openid-configuration", "type": "oauth2", }, ], @@ -332,6 +356,7 @@ exports[`Documentation helpers > depictSecurity() > should depict OAuth2 Securit [ [ { + "deprecated": undefined, "flows": { "authorizationCode": { "authorizationUrl": "https://test.url", @@ -367,6 +392,7 @@ exports[`Documentation helpers > depictSecurity() > should depict OAuth2 Securit "tokenUrl": "https://test3.url", }, }, + "oauth2MetadataUrl": undefined, "type": "oauth2", }, ], @@ -377,16 +403,19 @@ exports[`Documentation helpers > depictSecurity() > should handle Basic, Bearer [ [ { + "deprecated": undefined, "scheme": "basic", "type": "http", }, { + "deprecated": undefined, "scheme": "bearer", "type": "http", }, ], [ { + "deprecated": undefined, "in": "header", "name": "X-Key", "type": "apiKey", @@ -399,6 +428,7 @@ exports[`Documentation helpers > depictSecurity() > should handle Input and Cook [ [ { + "deprecated": undefined, "in": "query", "name": "apiKey", "type": "apiKey", @@ -406,6 +436,7 @@ exports[`Documentation helpers > depictSecurity() > should handle Input and Cook ], [ { + "deprecated": undefined, "in": "cookie", "name": "hash", "type": "apiKey", @@ -418,13 +449,16 @@ exports[`Documentation helpers > depictSecurity() > should handle OpenID and OAu [ [ { + "deprecated": undefined, "openIdConnectUrl": "https://test.url", "type": "openIdConnect", }, ], [ { + "deprecated": undefined, "flows": {}, + "oauth2MetadataUrl": undefined, "type": "oauth2", }, ], @@ -435,7 +469,9 @@ exports[`Documentation helpers > depictSecurity() > should handle undefined flow [ [ { + "deprecated": undefined, "flows": {}, + "oauth2MetadataUrl": undefined, "type": "oauth2", }, ], @@ -446,6 +482,7 @@ exports[`Documentation helpers > depictSecurity() > should inform on 'actual' pl [ [ { + "deprecated": undefined, "description": "key MUST be supplied within the request body instead of query", "in": "query", "name": "key", @@ -460,6 +497,7 @@ exports[`Documentation helpers > depictSecurity() > should inform on 'alternativ [ [ { + "deprecated": undefined, "description": "key CAN also be supplied within the request body", "in": "query", "name": "key", @@ -470,6 +508,33 @@ exports[`Documentation helpers > depictSecurity() > should inform on 'alternativ ] `; +exports[`Documentation helpers > depictSecurity() > should support deprecated flag on security schemes 1`] = ` +[ + [ + { + "deprecated": true, + "scheme": "basic", + "type": "http", + }, + ], + [ + { + "deprecated": true, + "scheme": "bearer", + "type": "http", + }, + ], + [ + { + "deprecated": true, + "in": "header", + "name": "X-API-Key", + "type": "apiKey", + }, + ], +] +`; + exports[`Documentation helpers > depictSecurityRefs() > should handle alternatives 1`] = ` [ { @@ -543,6 +608,33 @@ exports[`Documentation helpers > depictTags() > should accept objects with URLs ] `; +exports[`Documentation helpers > depictTags() > should accept objects with summary and parent and kind 1`] = ` +[ + { + "description": "Book catalog and recommendations", + "externalDocs": { + "description": "Full API documentation", + "url": "https://docs.example.com", + }, + "kind": "nav", + "name": "books", + "parent": "products", + "summary": "Books & Literature", + }, + { + "description": "Music CD catalog and reviews", + "externalDocs": { + "description": "External docs site", + "url": "https://example.com/docs", + }, + "kind": "nav", + "name": "cds", + "parent": "products", + "summary": "Music CDs", + }, +] +`; + exports[`Documentation helpers > depictTags() > should accept plain descriptions 1`] = ` [ { diff --git a/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap b/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap index 6fb88310e..693314ad6 100644 --- a/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Documentation > Basic cases > should be able to specify operation 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Operation IDs version: 3.4.5 @@ -54,7 +54,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -84,7 +84,7 @@ servers: `; exports[`Documentation > Basic cases > should be able to specify the operationId provider depending on method 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Operation IDs version: 3.4.5 @@ -137,7 +137,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -204,7 +204,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -225,7 +225,7 @@ servers: `; exports[`Documentation > Basic cases > should ensure the uniq operation ids 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Operation IDs version: 3.4.5 @@ -278,7 +278,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -339,7 +339,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -369,7 +369,7 @@ servers: `; exports[`Documentation > Basic cases > should ensure uniq security schema names 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Security version: 3.4.5 @@ -440,7 +440,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -524,7 +524,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -583,7 +583,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -623,7 +623,7 @@ servers: `; exports[`Documentation > Basic cases > should generate the correct schema for DELETE request without body 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing DELETE request without body version: 3.4.5 @@ -678,7 +678,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -699,7 +699,7 @@ servers: `; exports[`Documentation > Basic cases > should generate the correct schema for complex types 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Complex Types version: 3.4.5 @@ -785,7 +785,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -806,7 +806,7 @@ servers: `; exports[`Documentation > Basic cases > should generate the correct schema for discriminated union type 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Discriminated Union Type version: 3.4.5 @@ -913,7 +913,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -934,7 +934,7 @@ servers: `; exports[`Documentation > Basic cases > should generate the correct schema for intersection type 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Intersection and And types version: 3.4.5 @@ -1019,7 +1019,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1040,7 +1040,7 @@ servers: `; exports[`Documentation > Basic cases > should generate the correct schema for nullable and optional types 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Nullable and Optional Types version: 3.4.5 @@ -1160,7 +1160,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1230,7 +1230,7 @@ servers: `; exports[`Documentation > Basic cases > should generate the correct schema for union type 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Union and Or Types version: 3.4.5 @@ -1323,7 +1323,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1344,7 +1344,7 @@ servers: `; exports[`Documentation > Basic cases > should handle CookieSecurity in params and security section 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing CookieSecurity version: 3.4.5 @@ -1410,7 +1410,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1457,7 +1457,7 @@ servers: `; exports[`Documentation > Basic cases > should handle bigint, boolean, date, null and readonly 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing additional types version: 3.4.5 @@ -1555,7 +1555,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1576,7 +1576,7 @@ servers: `; exports[`Documentation > Basic cases > should handle circular schemas via z.object() 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Lazy version: 3.4.5 @@ -1637,7 +1637,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1683,7 +1683,7 @@ servers: `; exports[`Documentation > Basic cases > should handle custom mime types and status codes 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing MIME types and status codes version: 3.4.5 @@ -1695,8 +1695,8 @@ paths: "201": description: GET /v1/getSomething Positive response content: - application/json: &a1 - schema: + application/json: + schema: &a1 type: object properties: status: @@ -1710,7 +1710,8 @@ paths: - status - result additionalProperties: false - text/vnd.yaml: *a1 + text/vnd.yaml: + schema: *a1 "403": description: GET /v1/getSomething Negative response content: @@ -1748,7 +1749,7 @@ servers: `; exports[`Documentation > Basic cases > should handle different number types 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing numbers version: 3.4.5 @@ -1857,7 +1858,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -1878,7 +1879,7 @@ servers: `; exports[`Documentation > Basic cases > should handle different string types 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing strings version: 3.4.5 @@ -2010,7 +2011,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2031,7 +2032,7 @@ servers: `; exports[`Documentation > Basic cases > should handle enum types 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing enums version: 3.4.5 @@ -2104,7 +2105,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2125,7 +2126,7 @@ servers: `; exports[`Documentation > Basic cases > should handle record 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing record version: 3.4.5 @@ -2247,7 +2248,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2268,7 +2269,7 @@ servers: `; exports[`Documentation > Basic cases > should handle transformation schema in output 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Transformation in response schema version: 3.4.5 @@ -2340,7 +2341,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2361,7 +2362,7 @@ servers: `; exports[`Documentation > Basic cases > should handle tuples 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing tuples version: 3.4.5 @@ -2456,7 +2457,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2477,7 +2478,7 @@ servers: `; exports[`Documentation > Basic cases > should handle type any 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing type any version: 3.4.5 @@ -2537,7 +2538,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2571,7 +2572,7 @@ servers: `; exports[`Documentation > Basic cases > should handle z.preprocess() 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing z.preprocess() version: 3.4.5 @@ -2641,7 +2642,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2683,8 +2684,142 @@ servers: " `; +exports[`Documentation > Constructor metadata > should aggregate servers 0 1`] = ` +[ + { + "description": "Single", + "url": "https://s.example.com", + }, + { + "url": "https://short.example.com", + }, +] +`; + +exports[`Documentation > Constructor metadata > should aggregate servers 1 1`] = ` +[ + { + "description": "Single", + "url": "https://s.example.com", + }, + { + "url": "https://short-a.example.com", + }, + { + "url": "https://short-b.example.com", + }, +] +`; + +exports[`Documentation > Constructor metadata > should aggregate servers 2 1`] = ` +[ + { + "description": "A", + "url": "https://a.example.com", + }, + { + "description": "B", + "url": "https://b.example.com", + }, + { + "url": "https://short.example.com", + }, +] +`; + +exports[`Documentation > Constructor metadata > should aggregate servers 3 1`] = ` +[ + { + "description": "A", + "url": "https://a.example.com", + }, + { + "description": "B", + "url": "https://b.example.com", + }, + { + "url": "https://short-a.example.com", + }, + { + "url": "https://short-b.example.com", + }, +] +`; + +exports[`Documentation > Constructor metadata > should aggregate servers 4 1`] = ` +[ + { + "url": "https://ex.one", + }, + { + "url": "https://ex.two", + }, +] +`; + +exports[`Documentation > Constructor metadata > should aggregate servers 5 1`] = ` +[ + { + "url": "https://single.example", + }, +] +`; + +exports[`Documentation > Constructor metadata > should fall back to builder defaults when no metadata provided 1`] = ` +{ + "title": "app", + "version": "version", +} +`; + +exports[`Documentation > Constructor metadata > should override info with shorthands 1`] = ` +{ + "summary": "Retained", + "title": "Overridden Title", + "version": "2.0.0", +} +`; + +exports[`Documentation > Constructor metadata > should use expanded info and server objects with all fields 1`] = ` +"openapi: 3.2.0 +info: + title: Expanded API + version: 3.0.0 + summary: Full info object + contact: + name: Dev + email: dev@example.com + license: + name: MIT + url: https://opensource.org/licenses/MIT +paths: {} +components: + schemas: {} + responses: {} + parameters: {} + examples: {} + requestBodies: {} + headers: {} + securitySchemes: {} + links: {} + callbacks: {} +tags: [] +servers: + - url: https://{env}.example.com/{ver} + description: Environment server + variables: + env: + default: api + enum: + - api + - staging + ver: + default: v1 +" +`; + exports[`Documentation > Feature #1431: Multiple schemas for different status codes > should depict accordingly 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing multiple schemas for different status codes version: 3.4.5 @@ -2780,7 +2915,7 @@ servers: `; exports[`Documentation > Feature #1470: Custom brands > should be handled accordingly in request, response and params 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing custom brands handling version: 3.4.5 @@ -2854,7 +2989,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -2901,7 +3036,7 @@ servers: `; exports[`Documentation > Feature #1869: Top level transformations > should handle explicit renaming 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing top level transformations version: 3.4.5 @@ -2975,7 +3110,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3022,7 +3157,7 @@ servers: `; exports[`Documentation > Feature #1869: Top level transformations > should handle object-to-object functional transformations and mapping 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing top level transformations version: 3.4.5 @@ -3084,7 +3219,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3119,7 +3254,7 @@ servers: `; exports[`Documentation > Feature 1180: Headers opt-in params > should describe x- inputs as header params in get request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing headers params version: 3.4.5 @@ -3183,7 +3318,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3224,7 +3359,7 @@ servers: `; exports[`Documentation > Feature 1180: Headers opt-in params > should describe x- inputs as header params in post request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing headers params version: 3.4.5 @@ -3296,7 +3431,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3317,7 +3452,7 @@ servers: `; exports[`Documentation > Feature 1180: Headers opt-in params > should describe x- inputs as header params in put request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing headers params version: 3.4.5 @@ -3387,7 +3522,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3408,7 +3543,7 @@ servers: `; exports[`Documentation > Issue #98 > Should describe non-empty array 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: "Testing issue #98" version: 3.4.5 @@ -3476,7 +3611,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3564,7 +3699,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3585,7 +3720,7 @@ servers: `; exports[`Documentation > Issue #98 > should union schemas 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: "Testing issue #98" version: 3.4.5 @@ -3679,7 +3814,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3700,7 +3835,7 @@ servers: `; exports[`Documentation > Metadata > Feature #2390: should support deprecations 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:deprecations version: 3.4.5 @@ -3761,7 +3896,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3799,7 +3934,7 @@ servers: `; exports[`Documentation > Metadata > Issue #827: .example() should be immutable 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO parameter version: 3.4.5 @@ -3820,7 +3955,7 @@ paths: - a examples: example1: - value: + dataValue: a: first required: true responses: @@ -3856,13 +3991,13 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: a: first b: prefix_first example2: - value: + dataValue: status: success data: a: second @@ -3891,7 +4026,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -3912,7 +4047,7 @@ servers: `; exports[`Documentation > Metadata > Issue #929: the location of the custom description should be on the param level 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:description version: 3.4.5 @@ -3943,7 +4078,7 @@ paths: $ref: "#/components/schemas/GetHrisEmployeesNegativeResponse" examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4015,7 +4150,7 @@ servers: `; exports[`Documentation > Metadata > should merge endpoint handler examples with its middleware examples 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO schema + middleware version: 3.4.5 @@ -4039,7 +4174,7 @@ paths: - str examples: example1: - value: + dataValue: key: 1234-56789-01 str: test required: true @@ -4070,7 +4205,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: num: 123 @@ -4098,7 +4233,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4119,7 +4254,7 @@ servers: `; exports[`Documentation > Metadata > should merge prop examples with middlewares 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO schema + middleware version: 3.4.5 @@ -4147,7 +4282,7 @@ paths: - str examples: example1: - value: + dataValue: key: 1234-56789-01 str: test required: true @@ -4180,7 +4315,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: num: 123 @@ -4208,7 +4343,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4229,7 +4364,7 @@ servers: `; exports[`Documentation > Metadata > should pass over examples of each param from the whole IO schema examples (get method) 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO schema version: 3.4.5 @@ -4246,7 +4381,7 @@ paths: type: string examples: example1: - value: "123" + dataValue: "123" responses: "200": description: GET /v1/getSomething Positive response @@ -4274,7 +4409,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: numericStr: "123" @@ -4302,7 +4437,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4317,7 +4452,7 @@ paths: type: string examples: example1: - value: "123" + dataValue: "123" responses: "200": description: HEAD /v1/getSomething Positive response @@ -4340,7 +4475,7 @@ servers: `; exports[`Documentation > Metadata > should pass over examples of each param from the whole IO schema examples (post method) 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO schema version: 3.4.5 @@ -4361,7 +4496,7 @@ paths: - strNum examples: example1: - value: + dataValue: strNum: "123" required: true responses: @@ -4391,7 +4526,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: numericStr: "123" @@ -4419,7 +4554,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4440,7 +4575,7 @@ servers: `; exports[`Documentation > Metadata > should pass over the example of an individual prop in get request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO parameter version: 3.4.5 @@ -4459,7 +4594,7 @@ paths: - "123" examples: example1: - value: "123" + dataValue: "123" responses: "200": description: GET /v1/getSomething Positive response @@ -4489,7 +4624,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: numericStr: "456" @@ -4517,7 +4652,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4534,7 +4669,7 @@ paths: - "123" examples: example1: - value: "123" + dataValue: "123" responses: "200": description: HEAD /v1/getSomething Positive response @@ -4557,7 +4692,7 @@ servers: `; exports[`Documentation > Metadata > should pass over the example of an individual prop in post request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:example on IO parameter version: 3.4.5 @@ -4580,7 +4715,7 @@ paths: - strNum examples: example1: - value: + dataValue: strNum: "123" required: true responses: @@ -4612,7 +4747,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: success data: numericStr: "456" @@ -4640,7 +4775,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4661,7 +4796,7 @@ servers: `; exports[`Documentation > Metadata > should pass over the schema description 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing Metadata:description version: 3.4.5 @@ -4727,7 +4862,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4763,7 +4898,7 @@ servers: `; exports[`Documentation > Route Path Params > should handle custom descriptions and descriptors 0 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing route path params version: 3.4.5 @@ -4837,7 +4972,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4858,7 +4993,7 @@ servers: `; exports[`Documentation > Route Path Params > should handle custom descriptions and descriptors 1 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing route path params version: 3.4.5 @@ -4895,7 +5030,7 @@ paths: $ref: "#/components/schemas/VeryNegativeResponseOfPostV1Name" examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -4961,7 +5096,7 @@ servers: `; exports[`Documentation > Route Path Params > should handle route path params for GET request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing route path params version: 3.4.5 @@ -5029,7 +5164,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message @@ -5074,7 +5209,7 @@ servers: `; exports[`Documentation > Route Path Params > should handle route path params for POST request 1`] = ` -"openapi: 3.1.0 +"openapi: 3.2.0 info: title: Testing route path params version: 3.4.5 @@ -5144,7 +5279,7 @@ paths: additionalProperties: false examples: example1: - value: + dataValue: status: error error: message: Sample error message diff --git a/express-zod-api/tests/documentation-helpers.spec.ts b/express-zod-api/tests/documentation-helpers.spec.ts index e1eaf313a..900ea4225 100644 --- a/express-zod-api/tests/documentation-helpers.spec.ts +++ b/express-zod-api/tests/documentation-helpers.spec.ts @@ -1,4 +1,4 @@ -import type { SchemaObject } from "openapi3-ts/oas31"; +import type { SchemaObjectValue } from "openapi3-ts/oas32"; import * as R from "ramda"; import { z } from "zod"; import { ez } from "../src"; @@ -291,7 +291,7 @@ describe("Documentation helpers", () => { }, ); - test.each([ + test.each([ { type: "null" }, { anyOf: [{ type: "null" }, { type: "null" }], @@ -617,6 +617,35 @@ describe("Documentation helpers", () => { ]), ).toMatchSnapshot(); }); + test("should depict OAuth2 Security with device authorization flow", () => { + expect( + depictSecurity([ + [ + { + type: "oauth2", + oauth2MetadataUrl: + "https://example.com/.well-known/openid-configuration", + flows: { + deviceAuthorization: { + deviceAuthorizationUrl: "https://example.com/device/auth", + tokenUrl: "https://example.com/device/token", + scopes: { read: "Read access", write: "Write access" }, + }, + }, + }, + ], + ]), + ).toMatchSnapshot(); + }); + test("should support deprecated flag on security schemes", () => { + expect( + depictSecurity([ + [{ type: "basic", deprecated: true }], + [{ type: "bearer", deprecated: true }], + [{ type: "header", name: "X-API-Key", deprecated: true }], + ]), + ).toMatchSnapshot(); + }); }); describe("depictSecurityRefs()", () => { @@ -687,6 +716,34 @@ describe("Documentation helpers", () => { }), ).toMatchSnapshot(); }); + + test("should accept objects with summary and parent and kind", () => { + expect( + depictTags({ + books: { + description: "Book catalog and recommendations", + summary: "Books & Literature", + parent: "products", + kind: "nav", + externalDocs: { + url: "https://docs.example.com", + description: "Full API documentation", + }, + }, + cds: { + description: "Music CD catalog and reviews", + summary: "Music CDs", + parent: "products", + kind: "nav", + url: "https://example.com/docs", // overrides + externalDocs: { + url: "https://example.com/old-docs", + description: "External docs site", + }, + }, + }), + ).toMatchSnapshot(); + }); }); describe("trimSummary()", () => { diff --git a/express-zod-api/tests/documentation.spec.ts b/express-zod-api/tests/documentation.spec.ts index e96dcced9..1f0840911 100644 --- a/express-zod-api/tests/documentation.spec.ts +++ b/express-zod-api/tests/documentation.spec.ts @@ -25,6 +25,98 @@ describe("Documentation", () => { http: { listen: givePort() }, }); + describe("Constructor metadata", () => { + test("should use expanded info and server objects with all fields", () => { + const spec = new Documentation({ + config: sampleConfig, + routing: {}, + info: { + title: "Expanded API", + version: "3.0.0", + summary: "Full info object", + contact: { name: "Dev", email: "dev@example.com" }, + license: { name: "MIT", url: "https://opensource.org/licenses/MIT" }, + }, + server: { + url: "https://{env}.example.com/{ver}", + description: "Environment server", + variables: { + env: { default: "api", enum: ["api", "staging"] }, + ver: { default: "v1" }, + }, + }, + }).getSpecAsYaml(); + expect(spec).toMatchSnapshot(); + }); + + test("should override info with shorthands", () => { + expect( + new Documentation({ + config: sampleConfig, + routing: {}, + info: { + title: "Expanded Title", + version: "1.0.0", + summary: "Retained", + }, + title: "Overridden Title", + version: "2.0.0", + }).rootDoc.info, + ).toMatchSnapshot(); + }); + + test.each[0]>>([ + { + server: { url: "https://s.example.com", description: "Single" }, + serverUrl: "https://short.example.com", + }, + { + server: { url: "https://s.example.com", description: "Single" }, + serverUrl: [ + "https://short-a.example.com", + "https://short-b.example.com", + ], + }, + { + server: [ + { url: "https://a.example.com", description: "A" }, + { url: "https://b.example.com", description: "B" }, + ], + serverUrl: "https://short.example.com", + }, + { + server: [ + { url: "https://a.example.com", description: "A" }, + { url: "https://b.example.com", description: "B" }, + ], + serverUrl: [ + "https://short-a.example.com", + "https://short-b.example.com", + ], + }, + { server: ["https://ex.one", "https://ex.two"] }, + { server: "https://single.example" }, + ])("should aggregate servers %#", ({ server, serverUrl }) => { + expect( + new Documentation({ + config: sampleConfig, + routing: {}, + server, + serverUrl, + }).rootDoc.servers, + ).toMatchSnapshot(); + }); + + test("should fall back to builder defaults when no metadata provided", () => { + const spec = new Documentation({ + config: sampleConfig, + routing: {}, + }); + expect(spec.rootDoc.servers).toHaveLength(0); + expect(spec.rootDoc.info).toMatchSnapshot(); + }); + }); + describe("Basic cases", () => { test("should generate the correct schema for DELETE request without body", () => { const spec = new Documentation({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1bc64e74..c76185da3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,7 +200,7 @@ importers: specifier: ^1.17.2 version: 1.17.2(@types/express@5.0.6)(@types/node@25.9.3) openapi3-ts: - specifier: ^4.5.0 + specifier: ^4.6.0 version: 4.6.0 ramda: specifier: catalog:prod