From c5809231fd8f6800c0cf66b1d97597f4e5e7a1c7 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Mon, 7 Sep 2026 14:23:04 +0700 Subject: [PATCH] refactor: strip comments and simplify the downgrader in orpc style Code in packages/ now follows the middleapi/orpc convention of letting names explain the code: file headers, internal JSDoc, SAFETY notes on casts, and inline restatements of spec rules (already covered by the README) are gone from the downgrader source and tests, and the types package loses its file headers and vocabulary divider comments while keeping the per-field spec JSDoc. The downgrader is also simplified: the convertSpec wrappers are inlined into the exported functions, path item inlining merges the referenced item before converting instead of merging two converted outputs, the mutualTLS set is replaced by a lookup on the scheme-type map, and literal-key writes use plain assignment. --- packages/downgrader/src/shared.test.ts | 9 +- packages/downgrader/src/shared.ts | 51 +--- packages/downgrader/src/v3.1-to-v3.0.test.ts | 3 - packages/downgrader/src/v3.1-to-v3.0.ts | 286 ++++--------------- packages/downgrader/src/v3.2-to-v3.1.test.ts | 1 - packages/downgrader/src/v3.2-to-v3.1.ts | 206 +++---------- packages/downgrader/tests/corpus.test.ts | 22 +- packages/downgrader/tests/e2e.test.ts | 17 -- packages/downgrader/tests/helpers.ts | 1 - packages/types/src/v3.0.ts | 10 - packages/types/src/v3.1.test-d.ts | 2 - packages/types/src/v3.1.ts | 31 -- packages/types/src/v3.2.ts | 31 -- 13 files changed, 95 insertions(+), 575 deletions(-) diff --git a/packages/downgrader/src/shared.test.ts b/packages/downgrader/src/shared.test.ts index accaf0b..ba3d0fb 100644 --- a/packages/downgrader/src/shared.test.ts +++ b/packages/downgrader/src/shared.test.ts @@ -16,7 +16,6 @@ function identity(value: T): T { return value } -/** A converter that recurses into `self`, so a self-referencing node re-enters convertRecord. */ function convertNode(value: unknown): unknown { return convertRecord(value, { name: () => 'converted', @@ -277,7 +276,6 @@ describe('mapRecord', () => { const calls: [unknown, string][] = [] const result = mapRecord({ a: 1, b: 2 }, (item, key) => { calls.push([item, key]) - // SAFETY: the test input only contains numbers. return (item as number) * 10 }) expect(result).toEqual({ a: 10, b: 20 }) @@ -315,12 +313,7 @@ describe('mapRecord', () => { describe('mapArray', () => { it('applies the converter to every element', () => { - const result = mapArray( - [1, 2, 3], - item => - // SAFETY: the test input only contains numbers. - (item as number) + 1, - ) + const result = mapArray([1, 2, 3], item => (item as number) + 1) expect(result).toEqual([2, 3, 4]) }) diff --git a/packages/downgrader/src/shared.ts b/packages/downgrader/src/shared.ts index fb7654b..721d285 100644 --- a/packages/downgrader/src/shared.ts +++ b/packages/downgrader/src/shared.ts @@ -1,29 +1,9 @@ -/** - * Internal helpers shared by the version converters. Everything is defensive: - * converters never throw on malformed input, they pass unconvertible parts - * through unchanged. - */ - -/** - * Returned by a converter to remove its entry from the surrounding object or - * array: the single signal for constructs the target version cannot express. - */ export const DROP = Symbol('drop') -/** - * Converts one field of a record. The whole source record is passed along - * for decisions that depend on sibling fields. - */ export type FieldConverter = (item: unknown, source: Record) => unknown -/** - * What happens to each known field of a record: a converter, or `DROP` to - * remove the field. Fields not listed (unknown keys, `x-` extensions) are - * deep-cloned as they are. - */ export type FieldTable = Readonly> -/** The Path Item operation fields of OpenAPI 3.0 and 3.1; 3.2 adds `query`. */ export const HTTP_METHODS_UP_TO_V31 = [ 'delete', 'get', @@ -39,11 +19,6 @@ export function operationFields(convert: FieldConverter): FieldTable { return Object.fromEntries(HTTP_METHODS_UP_TO_V31.map(method => [method, convert])) } -/** - * Whether the value is a plain object (the only shape the converters walk - * into). Arrays, class instances, and primitives are handled by reference or - * by dedicated array helpers. - */ export function isRecord(value: unknown): value is Record { if (typeof value !== 'object' || value === null) { return false @@ -52,10 +27,6 @@ export function isRecord(value: unknown): value is Record { return proto === Object.prototype || proto === null } -/** - * Sets a key as an own data property. `__proto__` is defined rather than - * assigned, so it becomes a plain property instead of replacing the prototype. - */ export function setOwn(object: object, key: PropertyKey, value: unknown): void { if (key === '__proto__') { Object.defineProperty(object, key, { @@ -66,7 +37,6 @@ export function setOwn(object: object, key: PropertyKey, value: unknown): void { }) } else { - // SAFETY: only widens the index signature. (object as Record)[key] = value } } @@ -95,33 +65,15 @@ function cloneValue(value: unknown, seen: WeakMap): unknown { return out } -/** - * A JSON-oriented deep clone that never throws: non-plain values (class - * instances, functions, ...) are kept by reference, hostile keys like - * `__proto__` are copied as own data properties instead of being assigned, - * and cyclic or shared references are preserved in the clone instead of - * recursing forever. - */ export function deepClone(value: T): T { if (!(Array.isArray(value) || isRecord(value))) { return value } - // SAFETY: cloneValue preserves the runtime shape of its input. return cloneValue(value, new WeakMap()) as T } -/** Objects being converted up the call stack, mapped to their output records. */ const converting = new WeakMap>() -/** - * Rebuilds a plain object field by field: each key goes through its entry in - * `fields` (or is deep-cloned when it has none), entries mapped to or - * returning `DROP` are left out, and `finish` receives the result together - * with the source for fix-ups that depend on several fields. Non-object input - * is deep-cloned unchanged, and a cyclic reference back to an object still - * being converted yields that object's output record, so `finish` should - * mutate and return `out` rather than replace it. - */ export function convertRecord(value: unknown, fields: FieldTable, finish?: (out: Record, source: Record) => unknown): unknown { if (!isRecord(value)) { return deepClone(value) @@ -138,8 +90,7 @@ export function convertRecord(value: unknown, fields: FieldTable, finish?: (out: if (convert === DROP) { continue } - const converted - = convert === undefined ? deepClone(item) : convert(item, value) + const converted = convert === undefined ? deepClone(item) : convert(item, value) if (converted !== DROP) { setOwn(out, key, converted) } diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index a0e109b..b7f7462 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -5,7 +5,6 @@ import { downgradeSchemaV31ToV30, downgradeSpecV31ToV30 } from './v3.1-to-v3.0' const info = { title: 't', version: '1' } -/** The smallest valid 3.1 document and its 3.0 counterpart. */ const base = { info, openapi: '3.1.0', paths: {} } const converted = { info, openapi: '3.0.4', paths: {} } @@ -110,8 +109,6 @@ describe('downgradeSpecV31ToV30', () => { convertSpec({ paths: { '/a': { get: { summary: 's' } }, - // Path-item-shaped on purpose: cloning must NOT convert it, so - // no responses may be synthesized inside. 'x-note': { get: { summary: 's' } }, }, }).paths, diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index 2fea1e8..4736ff0 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -1,19 +1,3 @@ -/** - * Converts OpenAPI 3.1 documents and schemas to OpenAPI 3.0 (targeting the - * latest patch release, 3.0.4). - * - * The conversion never throws: parts that do not match the expected shape - * are deep-copied through unchanged, a subtree that cycles back into an - * ancestor object points at that ancestor's converted form, and existing - * specification extensions (`x-` keys) as well as unknown keys are always - * preserved. Constructs 3.0 cannot express are - * converted where an equivalent exists and removed otherwise — the converter - * never invents `x-` keys of its own. The README lists every mapping. - * - * @see {@link https://spec.openapis.org/oas/v3.1.2.html} - * @see {@link https://spec.openapis.org/oas/v3.0.4.html} - */ - import type { OpenAPIV3_0, OpenAPIV3_1 } from '@openapi-spec/types' import type { FieldConverter, FieldTable } from './shared' @@ -26,34 +10,26 @@ import { mapArray, mapRecord, operationFields, - setOwn, } from './shared' -/** - * 3.0 references stand alone: a Reference Object is reduced to its `$ref` - * (no `summary`/`description` overrides), anything else is converted. - */ function convertRefOr(value: unknown, convert: (item: unknown) => unknown): unknown { const ref = getRef(value) return ref === undefined ? convert(value) : { $ref: ref } } function refMap(convert: (item: unknown) => unknown): FieldConverter { - return item => - mapRecord(item, entry => convertRefOr(entry, convert)) + return item => mapRecord(item, entry => convertRefOr(entry, convert)) } function refList(convert: (item: unknown) => unknown): FieldConverter { - return item => - mapArray(item, entry => convertRefOr(entry, convert)) + return item => mapArray(item, entry => convertRefOr(entry, convert)) } function applyTypes(types: string[], schema: Record, out: Record): void { const nullable = types.includes('null') const rest = types.filter(item => item !== 'null') if (rest.length === 1) { - const [single] = rest - out.type = single + out.type = rest[0] if (nullable) { out.nullable = true } @@ -61,42 +37,30 @@ function applyTypes(types: string[], schema: Record, out: Recor } if (rest.length === 0) { if (!nullable) { - // `type: []` allows nothing 3.0 can express; drop it. return } - // `type: "null"` alone: 3.0's `nullable` needs a sibling `type`, so a - // single-value `enum` is the closest expressible form. Sibling `enum`/ - // `const` values intersect with the null type — only null can survive, - // and a sibling that excludes null leaves a schema matching nothing. out.nullable = true if ('const' in schema) { - // convertConst emits the single-value enum; a non-null const - // contradicts the null type, so nothing may validate. if (schema.const !== null) { out.not = {} } - return } - if (Array.isArray(schema.enum)) { + else if (Array.isArray(schema.enum)) { if (schema.enum.includes(null)) { out.enum = [null] } else { out.not = {} } - return } - if (!('enum' in schema)) { + else if (!('enum' in schema)) { out.enum = [null] } return } - // Multiple non-null types: 3.0 only allows a single `type`, so the type - // union moves into `anyOf` branches. const variants = rest.map((item) => { const variant: Record = { type: item } if (item === 'array') { - // 3.0 requires `items` whenever `type` is "array". variant.items = out.items === undefined ? {} : deepClone(out.items) } if (nullable) { @@ -108,13 +72,8 @@ function applyTypes(types: string[], schema: Record, out: Recor out.anyOf = variants } else if (out.allOf === undefined || Array.isArray(out.allOf)) { - out.allOf = [ - ...(Array.isArray(out.allOf) ? out.allOf : []), - { anyOf: variants }, - ] + out.allOf = [...(Array.isArray(out.allOf) ? out.allOf : []), { anyOf: variants }] } - // With both anyOf occupied and a malformed allOf, the inexpressible type - // union is dropped rather than clobbering the passed-through allOf. } function convertType(schema: Record, out: Record): void { @@ -128,15 +87,12 @@ function convertType(schema: Record, out: Record typeof item === 'string'))] - if (types.length === 0 && type.length > 0) { - // Only malformed entries: pass the array through unchanged. - setOwn(out, 'type', deepClone(type)) + if (types.length > 0 || type.length === 0) { + applyTypes(types, schema, out) return } - applyTypes(types, schema, out) - return } - setOwn(out, 'type', deepClone(type)) + out.type = deepClone(type) } function convertConst(schema: Record, out: Record): void { @@ -150,79 +106,47 @@ function convertConst(schema: Record, out: Record, out: Record): void { - // 3.0 only has the singular `example`; the first entry wins, unless an - // explicit `example` already exists. The rest have no 3.0 home. - if ( - Array.isArray(schema.examples) - && schema.examples.length > 0 - && !('example' in schema) - ) { + if (Array.isArray(schema.examples) && schema.examples.length > 0 && !('example' in schema)) { out.example = deepClone(schema.examples[0]) } } function convertExclusiveBounds(schema: Record, out: Record): void { const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = schema - // 3.1's numeric exclusive bounds become 3.0's bound + boolean pairs. When - // an inclusive bound is also present, the tighter constraint wins. - if ( - typeof exclusiveMinimum === 'number' - && !(typeof minimum === 'number' && minimum > exclusiveMinimum) - ) { + if (typeof exclusiveMinimum === 'number' && !(typeof minimum === 'number' && minimum > exclusiveMinimum)) { out.minimum = exclusiveMinimum out.exclusiveMinimum = true } - if ( - typeof exclusiveMaximum === 'number' - && !(typeof maximum === 'number' && maximum < exclusiveMaximum) - ) { + if (typeof exclusiveMaximum === 'number' && !(typeof maximum === 'number' && maximum < exclusiveMaximum)) { out.maximum = exclusiveMaximum out.exclusiveMaximum = true } } function convertContentKeywords(schema: Record, out: Record): void { - // 3.1 replaced 3.0's `format: "byte"` / `format: "binary"` with the JSON - // Schema content keywords; reconstruct the formats when unambiguous. if (out.format !== undefined) { return } if (schema.contentEncoding === 'base64') { out.format = 'byte' - return } - if ( - schema.contentEncoding === undefined - && schema.contentMediaType === 'application/octet-stream' - ) { + else if (schema.contentEncoding === undefined && schema.contentMediaType === 'application/octet-stream') { out.format = 'binary' } } -/** - * 3.1 documents produced from 3.2 ones may carry the 3.2 `nodeType` field - * in XML Objects (tolerated by the standard 3.1 document schema, though - * the OAS base-vocabulary meta-schema closes XML Objects to `x-` extras; - * 3.0 forbids it outright): it maps back to the `attribute`/`wrapped` - * flags where expressible and is removed. - */ function convertXml(value: unknown, schemaType: unknown): unknown { return convertRecord(value, { nodeType: DROP }, (out, xml) => { if (xml.nodeType === 'attribute') { out.attribute = true } - else if ( - xml.nodeType === 'element' - && (schemaType === 'array' - || (Array.isArray(schemaType) && schemaType.includes('array'))) - ) { + else if (xml.nodeType === 'element' && (schemaType === 'array' || (Array.isArray(schemaType) && schemaType.includes('array')))) { out.wrapped = true } return out }) } -/** Converts the keywords whose 3.0 form depends on several 3.1 keywords at once. */ function finishSchema(out: Record, schema: Record): Record { convertType(schema, out) convertConst(schema, out) @@ -230,22 +154,14 @@ function finishSchema(out: Record, schema: Record (typeof item === 'string' ? DROP : deepClone(item)), $schema: DROP, $vocabulary: DROP, - // With `patternProperties` dropped, `additionalProperties` would also - // constrain the previously pattern-matched keys, so it is dropped alongside - // (removing a constraint is the safe direction). Booleans are valid in 3.0. additionalProperties: (item, schema) => { if ('patternProperties' in schema) { return DROP @@ -295,22 +197,12 @@ const SCHEMA_FIELDS: FieldTable = { dependentRequired: DROP, dependentSchemas: DROP, else: DROP, - // 3.0 requires at least one enum entry; an empty enum only constrains, so - // removing it is the safe direction. - enum: item => - Array.isArray(item) && item.length === 0 ? DROP : deepClone(item), + enum: item => (Array.isArray(item) && item.length === 0 ? DROP : deepClone(item)), examples: DROP, - // Numeric bounds are rewritten by finishSchema; 3.0-style booleans (invalid - // in 3.1, but accepted gracefully) pass through. - exclusiveMaximum: item => - typeof item === 'number' ? DROP : deepClone(item), - exclusiveMinimum: item => - typeof item === 'number' ? DROP : deepClone(item), + exclusiveMaximum: item => (typeof item === 'number' ? DROP : deepClone(item)), + exclusiveMinimum: item => (typeof item === 'number' ? DROP : deepClone(item)), if: DROP, - // With `prefixItems` dropped, a trailing `items` would wrongly constrain - // every item, so it is dropped alongside. - items: (item, schema) => - 'prefixItems' in schema ? DROP : convertSchema(item), + items: (item, schema) => ('prefixItems' in schema ? DROP : convertSchema(item)), maxContains: DROP, minContains: DROP, not: convertSchema, @@ -319,13 +211,11 @@ const SCHEMA_FIELDS: FieldTable = { prefixItems: DROP, properties: item => mapRecord(item, convertSchema), propertyNames: DROP, - // 3.0 requires the array to be non-empty with unique entries. required: (item) => { if (!Array.isArray(item)) { return deepClone(item) } - const unique = [...new Set(item)] - return unique.length === 0 ? DROP : deepClone(unique) + return item.length === 0 ? DROP : deepClone([...new Set(item)]) }, then: DROP, type: DROP, @@ -341,43 +231,25 @@ function convertSchema(schema: unknown): unknown { if (schema === false) { return { not: {} } } - if ( - isRecord(schema) - && typeof schema.$ref === 'string' - && Object.keys(schema).length === 1 - ) { + if (isRecord(schema) && typeof schema.$ref === 'string' && Object.keys(schema).length === 1) { return { $ref: schema.$ref } } return convertRecord(schema, SCHEMA_FIELDS, finishSchema) } -/** - * Converts an OpenAPI 3.1 Schema Object to its OpenAPI 3.0 form. A schema - * consisting solely of `$ref` becomes a 3.0 Reference Object; a `$ref` with - * sibling keywords is wrapped in `allOf`. See the README for the full - * keyword mapping. - */ export function downgradeSchemaV31ToV30(schema: OpenAPIV3_1.SchemaObject): OpenAPIV3_0.ReferenceObject | OpenAPIV3_0.SchemaObject { - const converted: unknown = convertSchema(schema) - // SAFETY: convertSchema rewrites every 3.1-only keyword into its 3.0 form. - return converted as OpenAPIV3_0.ReferenceObject | OpenAPIV3_0.SchemaObject + return convertSchema(schema) as OpenAPIV3_0.ReferenceObject | OpenAPIV3_0.SchemaObject } const PATH_ITEMS_REF_PREFIX = '#/components/pathItems/' const SECURITY_SCHEMES_REF_PREFIX = '#/components/securitySchemes/' interface Context { - /** `components.pathItems` entries being inlined up the call stack. */ inlining: Set - mutualTls: ReadonlySet pathItems: Record | undefined schemeTypes: ReadonlyMap } -/** - * Resolves the declared `type` of a security scheme, following local - * reference aliases with cycle protection. - */ function resolveSchemeType(name: string, schemes: Record, seen: Set): string | undefined { if (seen.has(name) || !Object.hasOwn(schemes, name)) { return undefined @@ -404,65 +276,47 @@ function createContext(spec: unknown): Context { const components = isRecord(spec) ? spec.components : undefined const pathItems = isRecord(components) ? components.pathItems : undefined const schemes = isRecord(components) ? components.securitySchemes : undefined - const mutualTls = new Set() const schemeTypes = new Map() if (isRecord(schemes)) { for (const name of Object.keys(schemes)) { const type = resolveSchemeType(name, schemes, new Set()) if (type !== undefined) { schemeTypes.set(name, type) - if (type === 'mutualTLS') { - // Reference aliases of mutualTLS schemes are removed as well, so - // no dangling references survive. - mutualTls.add(name) - } } } } return { inlining: new Set(), - mutualTls, pathItems: isRecord(pathItems) ? pathItems : undefined, schemeTypes, } } +function isMutualTls(name: string, context: Context): boolean { + return context.schemeTypes.get(name) === 'mutualTLS' +} + function convertRequirement(value: unknown, context: Context): unknown { if (!isRecord(value)) { return deepClone(value) } const entries = Object.entries(value) - const kept = entries.filter(([name]) => !context.mutualTls.has(name)) + const kept = entries.filter(([name]) => !isMutualTls(name, context)) if (kept.length === 0 && entries.length > 0) { - // A requirement that only referenced mutualTLS schemes disappears; an - // originally empty `{}` (optional security) is kept. return DROP } - return Object.fromEntries( - kept.map(([name, scopes]) => { - // 3.0 allows roles only on OAuth-family schemes; roles on unknown - // schemes are left alone. - const type = context.schemeTypes.get(name) - const scoped - = type === undefined || type === 'oauth2' || type === 'openIdConnect' - return [name, Array.isArray(scopes) && !scoped ? [] : deepClone(scopes)] - }), - ) + return Object.fromEntries(kept.map(([name, scopes]) => { + const type = context.schemeTypes.get(name) + const scoped = type === undefined || type === 'oauth2' || type === 'openIdConnect' + return [name, Array.isArray(scopes) && !scoped ? [] : deepClone(scopes)] + })) } -/** - * Converts a `security` list. When mutualTLS removal empties a previously - * non-empty list, the whole field is dropped: an explicit empty `security` - * array means "no security required" and, on an operation, would override - * the root declaration and silently make the operation public. - */ function convertSecurity(value: unknown, context: Context): unknown { if (!Array.isArray(value)) { return deepClone(value) } - const out = value - .map(item => convertRequirement(item, context)) - .filter(item => item !== DROP) + const out = value.map(item => convertRequirement(item, context)).filter(item => item !== DROP) return value.length > 0 && out.length === 0 ? DROP : out } @@ -473,19 +327,16 @@ function convertInfo(value: unknown): unknown { }) } -/** Parameter Objects and Header Objects share every field this converter touches. */ function convertParameterOrHeader(value: unknown): unknown { return convertRecord( value, { - content: item => mapRecord(item, convertMediaType), + content: convertContent, examples: refMap(deepClone), schema: convertSchema, }, (out, parameter) => { if (parameter.in === 'path') { - // 3.0 requires `required: true` on every path parameter; 3.1 only - // structurally enforces it for schema-based ones. out.required = true } return out @@ -522,10 +373,7 @@ function convertResponse(value: unknown): unknown { } function convertResponses(item: unknown): unknown { - return mapRecord(item, (entry, key) => - key.startsWith('x-') - ? deepClone(entry) - : convertRefOr(entry, convertResponse)) + return mapRecord(item, (entry, key) => key.startsWith('x-') ? deepClone(entry) : convertRefOr(entry, convertResponse)) } function convertOperation(value: unknown, context: Context): unknown { @@ -540,8 +388,6 @@ function convertOperation(value: unknown, context: Context): unknown { }, (out) => { if (out.responses === undefined) { - // Required and non-empty in 3.0, optional in 3.1: a minimal default - // response keeps the output valid against the official 3.0 schema. out.responses = { default: { description: '' } } } return out @@ -550,57 +396,38 @@ function convertOperation(value: unknown, context: Context): unknown { } function convertCallback(value: unknown, context: Context): unknown { - return mapRecord(value, (item, key) => - key.startsWith('x-') ? deepClone(item) : convertPathItem(item, context)) + return mapRecord(value, (item, key) => key.startsWith('x-') ? deepClone(item) : convertPathItem(item, context)) } -/** - * The `components.pathItems` entry a Path Item's `$ref` names, unless it is - * unknown, malformed, or already being inlined. - */ function resolvePathItemRef(value: Record, context: Context): [name: string, target: Record] | undefined { const ref = getRef(value) if (ref === undefined || !ref.startsWith(PATH_ITEMS_REF_PREFIX)) { return undefined } const name = ref.slice(PATH_ITEMS_REF_PREFIX.length) - if ( - name === '' - || name.includes('/') - || context.inlining.has(name) - || context.pathItems === undefined - || !Object.hasOwn(context.pathItems, name) - ) { + if (name === '' || name.includes('/') || context.inlining.has(name) || context.pathItems === undefined || !Object.hasOwn(context.pathItems, name)) { return undefined } const target = context.pathItems[name] return isRecord(target) ? [name, target] : undefined } -/** - * 3.0 has no `components.pathItems`, so a reference into it is inlined, the - * referencing object's own fields winning over the referenced ones. - */ function convertPathItem(value: unknown, context: Context): unknown { if (!isRecord(value)) { return deepClone(value) } - const fields: FieldTable = { - ...operationFields(item => convertOperation(item, context)), - parameters: refList(convertParameterOrHeader), - } const resolved = resolvePathItemRef(value, context) if (resolved === undefined) { - return convertRecord(value, fields) + return convertRecord(value, { + ...operationFields(item => convertOperation(item, context)), + parameters: refList(convertParameterOrHeader), + }) } const [name, target] = resolved + const { $ref: _, ...own } = value context.inlining.add(name) try { - // SAFETY: both convert a plain object into a plain object. - const inlined = convertPathItem(target, context) as Record - const own = convertRecord(value, { ...fields, $ref: DROP }) as Record - // Spread, unlike Object.assign, defines own properties without running setters. - return { ...inlined, ...own } + return convertPathItem({ ...target, ...own }, context) } finally { context.inlining.delete(name) @@ -608,8 +435,7 @@ function convertPathItem(value: unknown, context: Context): unknown { } function convertPaths(value: unknown, context: Context): unknown { - return mapRecord(value, (item, key) => - key.startsWith('/') ? convertPathItem(item, context) : deepClone(item)) + return mapRecord(value, (item, key) => key.startsWith('/') ? convertPathItem(item, context) : deepClone(item)) } function convertComponents(value: unknown, context: Context): unknown { @@ -623,13 +449,11 @@ function convertComponents(value: unknown, context: Context): unknown { requestBodies: refMap(convertRequestBody), responses: refMap(convertResponse), schemas: item => mapRecord(item, convertSchema), - securitySchemes: item => - mapRecord(item, (scheme, name) => - context.mutualTls.has(name) ? DROP : convertRefOr(scheme, deepClone)), + securitySchemes: item => mapRecord(item, (scheme, name) => isMutualTls(name, context) ? DROP : convertRefOr(scheme, deepClone)), }) } -function convertSpec(spec: unknown): unknown { +export function downgradeSpecV31ToV30(spec: OpenAPIV3_1.OpenAPIObject): OpenAPIV3_0.OpenAPIObject { const context = createContext(spec) return convertRecord( spec, @@ -644,21 +468,9 @@ function convertSpec(spec: unknown): unknown { (out) => { out.openapi = '3.0.4' if (out.paths === undefined) { - // Required in 3.0; an empty Paths Object is valid. out.paths = {} } return out }, - ) -} - -/** - * Converts an OpenAPI 3.1 document to OpenAPI 3.0.4. The input is never - * mutated, unknown keys and specification extensions are preserved, and - * malformed parts are copied through unchanged instead of throwing. - */ -export function downgradeSpecV31ToV30(spec: OpenAPIV3_1.OpenAPIObject): OpenAPIV3_0.OpenAPIObject { - const converted: unknown = convertSpec(spec) - // SAFETY: convertSpec rewrites every 3.1-only construct into its 3.0 form. - return converted as OpenAPIV3_0.OpenAPIObject + ) as OpenAPIV3_0.OpenAPIObject } diff --git a/packages/downgrader/src/v3.2-to-v3.1.test.ts b/packages/downgrader/src/v3.2-to-v3.1.test.ts index f53e682..ec73a91 100644 --- a/packages/downgrader/src/v3.2-to-v3.1.test.ts +++ b/packages/downgrader/src/v3.2-to-v3.1.test.ts @@ -20,7 +20,6 @@ function convertComponent(kind: string, value: unknown, components: Record = {}): unknown { return dig( convertPathItem( diff --git a/packages/downgrader/src/v3.2-to-v3.1.ts b/packages/downgrader/src/v3.2-to-v3.1.ts index 615798a..d2a66a2 100644 --- a/packages/downgrader/src/v3.2-to-v3.1.ts +++ b/packages/downgrader/src/v3.2-to-v3.1.ts @@ -1,19 +1,3 @@ -/** - * Converts OpenAPI 3.2 documents and schemas to OpenAPI 3.1 (targeting the - * latest patch release, 3.1.2). - * - * The conversion never throws: parts that do not match the expected shape - * are deep-copied through unchanged, a subtree that cycles back into an - * ancestor object points at that ancestor's converted form, and existing - * specification extensions (`x-` keys) as well as unknown keys are always - * preserved. Constructs 3.1 cannot express are - * converted where an equivalent exists and removed otherwise — the converter - * never invents `x-` keys of its own. The README lists every mapping. - * - * @see {@link https://spec.openapis.org/oas/v3.2.0.html} - * @see {@link https://spec.openapis.org/oas/v3.1.2.html} - */ - import type { OpenAPIV3_1, OpenAPIV3_2 } from '@openapi-spec/types' import type { FieldConverter } from './shared' @@ -41,29 +25,16 @@ interface Context { removedParameterRefs: ReadonlySet } -/** - * Converts an OpenAPI 3.2 Schema Object to its OpenAPI 3.1 form: a deep - * clone, as the 3.2 Schema Object keyword set is identical to 3.1's. The - * 3.2-only `defaultMapping` and `nodeType` fields are deliberately retained; - * the README explains the trade-off. - */ export function downgradeSchemaV32ToV31(schema: OpenAPIV3_2.SchemaObject): OpenAPIV3_1.SchemaObject { - const converted: unknown = deepClone(schema) - // SAFETY: every 3.2 Schema Object is already a structurally valid 3.1 one. - return converted as OpenAPIV3_1.SchemaObject + return deepClone(schema) as OpenAPIV3_1.SchemaObject } -/** - * References stay references in 3.1 (including their `summary`/`description` - * overrides); everything else is converted. - */ -function convertRefOr(value: unknown, context: Context, convert: (item: unknown, innerContext: Context) => unknown): unknown { +function convertRefOr(value: unknown, context: Context, convert: (item: unknown, context: Context) => unknown): unknown { return getRef(value) === undefined ? convert(value, context) : deepClone(value) } -function refMap(context: Context, convert: (item: unknown, innerContext: Context) => unknown): FieldConverter { - return item => - mapRecord(item, entry => convertRefOr(entry, context, convert)) +function refMap(context: Context, convert: (item: unknown, context: Context) => unknown): FieldConverter { + return item => mapRecord(item, entry => convertRefOr(entry, context, convert)) } function convertServer(value: unknown): unknown { @@ -87,24 +58,18 @@ function convertSecurityScheme(value: unknown): unknown { } function convertExample(value: unknown): unknown { - return convertRecord( - value, - { dataValue: DROP, serializedValue: DROP }, - (out, example) => { - // dataValue/serializedValue fill 3.1's `value` slot when it is free - // and no externalValue competes. - if ('value' in example || 'externalValue' in example) { - return out - } - if ('dataValue' in example) { - out.value = deepClone(example.dataValue) - } - else if ('serializedValue' in example) { - out.value = deepClone(example.serializedValue) - } + return convertRecord(value, { dataValue: DROP, serializedValue: DROP }, (out, example) => { + if ('value' in example || 'externalValue' in example) { return out - }, - ) + } + if ('dataValue' in example) { + out.value = deepClone(example.dataValue) + } + else if ('serializedValue' in example) { + out.value = deepClone(example.serializedValue) + } + return out + }) } function isQuerystringParameter(value: unknown): boolean { @@ -116,48 +81,33 @@ function isRemovedRef(value: unknown, removed: ReadonlySet): boolean { return ref !== undefined && removed.has(ref) } -/** Parameter Objects and Header Objects share every field this converter touches. */ function convertParameterOrHeader(value: unknown, context: Context): unknown { return convertRecord( value, { - // 3.2 broadened allowReserved beyond query parameters; 3.1 only - // defines it there. - allowReserved: (item, parameter) => - !('in' in parameter) || parameter.in === 'query' - ? deepClone(item) - : DROP, + allowReserved: (item, parameter) => (!('in' in parameter) || parameter.in === 'query' ? deepClone(item) : DROP), content: item => convertContentMap(item, context), examples: refMap(context, convertExample), - // "cookie" is not a 3.1 style; removing it lets the 3.1 default - // (`form`) take over. style: item => (item === 'cookie' ? DROP : deepClone(item)), }, (out, parameter) => { - const lostContent - = isRecord(parameter.content) - && Object.keys(parameter.content).length > 0 - && isRecord(out.content) - && Object.keys(out.content).length === 0 - // 3.1 requires exactly one content entry on parameters and headers, so - // one whose entire content could not be inlined is removed. + const lostContent = isRecord(parameter.content) + && Object.keys(parameter.content).length > 0 + && isRecord(out.content) + && Object.keys(out.content).length === 0 return lostContent ? DROP : out }, ) } function convertParameterEntry(value: unknown, context: Context): unknown { - return isQuerystringParameter(value) - || isRemovedRef(value, context.removedParameterRefs) + return isQuerystringParameter(value) || isRemovedRef(value, context.removedParameterRefs) ? DROP : convertRefOr(value, context, convertParameterOrHeader) } function convertHeaderMap(value: unknown, context: Context): unknown { - return mapRecord(value, item => - isRemovedRef(item, context.removedHeaderRefs) - ? DROP - : convertRefOr(item, context, convertParameterOrHeader)) + return mapRecord(value, item => isRemovedRef(item, context.removedHeaderRefs) ? DROP : convertRefOr(item, context, convertParameterOrHeader)) } function convertEncoding(value: unknown, context: Context): unknown { @@ -174,8 +124,7 @@ function convertMediaType(value: unknown, context: Context): unknown { value, { description: DROP, - encoding: item => - mapRecord(item, entry => convertEncoding(entry, context)), + encoding: item => mapRecord(item, entry => convertEncoding(entry, context)), examples: refMap(context, convertExample), itemEncoding: DROP, itemSchema: DROP, @@ -183,7 +132,6 @@ function convertMediaType(value: unknown, context: Context): unknown { }, (out, mediaType) => { if ('itemSchema' in mediaType && out.schema === undefined) { - // The 3.2 sequential media type data model maps streams to arrays. out.schema = { items: deepClone(mediaType.itemSchema), type: 'array' } } return out @@ -191,12 +139,6 @@ function convertMediaType(value: unknown, context: Context): unknown { ) } -/** - * Follows a content-map entry's `components.mediaTypes` reference chain to - * the Media Type Object it names (non-reference entries stand for - * themselves), or to `DROP` when it cannot be inlined: an external, unknown, - * or cyclic target. - */ function resolveMediaType(value: unknown, mediaTypes: Record | undefined, seen: Set): unknown { const ref = getRef(value) if (ref === undefined) { @@ -206,24 +148,13 @@ function resolveMediaType(value: unknown, mediaTypes: Record | return DROP } const name = ref.slice(MEDIA_TYPES_REF_PREFIX.length) - if ( - name === '' - || name.includes('/') - || mediaTypes === undefined - || !Object.hasOwn(mediaTypes, name) - || seen.has(name) - ) { + if (name === '' || name.includes('/') || mediaTypes === undefined || !Object.hasOwn(mediaTypes, name) || seen.has(name)) { return DROP } seen.add(name) return resolveMediaType(mediaTypes[name], mediaTypes, seen) } -/** - * Converts a content map, inlining `components.mediaTypes` references (3.1 - * content maps hold Media Type Objects only, never references) and removing - * entries whose reference cannot be inlined. - */ function convertContentMap(value: unknown, context: Context): unknown { return mapRecord(value, (item) => { const target = resolveMediaType(item, context.mediaTypes, new Set()) @@ -246,9 +177,7 @@ function convertResponse(value: unknown, context: Context): unknown { }, (out, response) => { if (out.description === undefined) { - // Required in 3.1, optional in 3.2. - out.description - = typeof response.summary === 'string' ? response.summary : '' + out.description = typeof response.summary === 'string' ? response.summary : '' } return out }, @@ -256,17 +185,13 @@ function convertResponse(value: unknown, context: Context): unknown { } function convertResponses(value: unknown, context: Context): unknown { - return mapRecord(value, (item, key) => - key.startsWith('x-') - ? deepClone(item) - : convertRefOr(item, context, convertResponse)) + return mapRecord(value, (item, key) => key.startsWith('x-') ? deepClone(item) : convertRefOr(item, context, convertResponse)) } function convertOperation(value: unknown, context: Context): unknown { return convertRecord(value, { callbacks: refMap(context, convertCallback), - parameters: item => - mapArray(item, entry => convertParameterEntry(entry, context)), + parameters: item => mapArray(item, entry => convertParameterEntry(entry, context)), requestBody: item => convertRefOr(item, context, convertRequestBody), responses: item => convertResponses(item, context), servers: item => mapArray(item, convertServer), @@ -274,24 +199,21 @@ function convertOperation(value: unknown, context: Context): unknown { } function convertCallback(value: unknown, context: Context): unknown { - return mapRecord(value, (item, key) => - key.startsWith('x-') ? deepClone(item) : convertPathItem(item, context)) + return mapRecord(value, (item, key) => key.startsWith('x-') ? deepClone(item) : convertPathItem(item, context)) } function convertPathItem(value: unknown, context: Context): unknown { return convertRecord(value, { ...operationFields(item => convertOperation(item, context)), additionalOperations: DROP, - parameters: item => - mapArray(item, entry => convertParameterEntry(entry, context)), + parameters: item => mapArray(item, entry => convertParameterEntry(entry, context)), query: DROP, servers: item => mapArray(item, convertServer), }) } function convertPaths(value: unknown, context: Context): unknown { - return mapRecord(value, (item, key) => - key.startsWith('/') ? convertPathItem(item, context) : deepClone(item)) + return mapRecord(value, (item, key) => key.startsWith('/') ? convertPathItem(item, context) : deepClone(item)) } function convertComponents(value: unknown, context: Context): unknown { @@ -300,12 +222,9 @@ function convertComponents(value: unknown, context: Context): unknown { examples: refMap(context, convertExample), headers: item => convertHeaderMap(item, context), links: refMap(context, convertLink), - // Inlined at use sites; 3.1 has no reusable media types. mediaTypes: DROP, - parameters: item => - mapRecord(item, entry => convertParameterEntry(entry, context)), - pathItems: item => - mapRecord(item, entry => convertPathItem(entry, context)), + parameters: item => mapRecord(item, entry => convertParameterEntry(entry, context)), + pathItems: item => mapRecord(item, entry => convertPathItem(entry, context)), requestBodies: refMap(context, convertRequestBody), responses: refMap(context, convertResponse), securitySchemes: refMap(context, convertSecurityScheme), @@ -317,20 +236,9 @@ function losesEntireContent(value: unknown, mediaTypes: Record return false } const entries = Object.values(value.content) - return ( - entries.length > 0 - && entries.every( - item => resolveMediaType(item, mediaTypes, new Set()) === DROP, - ) - ) + return entries.length > 0 && entries.every(item => resolveMediaType(item, mediaTypes, new Set()) === DROP) } -/** - * Collects the `$ref` strings of component entries conversion removes - * (querystring parameters, and parameters or headers losing their entire - * `content`), iterated to a fixpoint so chains of reference aliases are - * removed with their targets. - */ function indexRemovedComponentRefs(map: unknown, prefix: string, mediaTypes: Record | undefined, isDirectlyRemoved: (item: unknown) => boolean): Set { const removed = new Set() if (!isRecord(map)) { @@ -346,11 +254,7 @@ function indexRemovedComponentRefs(map: unknown, prefix: string, mediaTypes: Rec continue } const target = getRef(item) - if ( - isDirectlyRemoved(item) - || losesEntireContent(item, mediaTypes) - || (target !== undefined && removed.has(target)) - ) { + if (isDirectlyRemoved(item) || losesEntireContent(item, mediaTypes) || (target !== undefined && removed.has(target))) { removed.add(selfRef) changed = true } @@ -361,35 +265,19 @@ function indexRemovedComponentRefs(map: unknown, prefix: string, mediaTypes: Rec function createContext(spec: unknown): Context { const components = isRecord(spec) ? spec.components : undefined - const mediaTypes - = isRecord(components) && isRecord(components.mediaTypes) - ? components.mediaTypes - : undefined + const mediaTypes = isRecord(components) && isRecord(components.mediaTypes) ? components.mediaTypes : undefined return { mediaTypes, - removedHeaderRefs: indexRemovedComponentRefs( - isRecord(components) ? components.headers : undefined, - HEADERS_REF_PREFIX, - mediaTypes, - () => false, - ), - removedParameterRefs: indexRemovedComponentRefs( - isRecord(components) ? components.parameters : undefined, - PARAMETERS_REF_PREFIX, - mediaTypes, - isQuerystringParameter, - ), + removedHeaderRefs: indexRemovedComponentRefs(isRecord(components) ? components.headers : undefined, HEADERS_REF_PREFIX, mediaTypes, () => false), + removedParameterRefs: indexRemovedComponentRefs(isRecord(components) ? components.parameters : undefined, PARAMETERS_REF_PREFIX, mediaTypes, isQuerystringParameter), } } -/** The 3.2 OAS dialect only extends the 3.1 base vocabulary; other dialects pass through. */ function convertJsonSchemaDialect(value: unknown): unknown { - return typeof value === 'string' && value.startsWith(V32_DIALECT_PREFIX) - ? V31_DIALECT - : deepClone(value) + return typeof value === 'string' && value.startsWith(V32_DIALECT_PREFIX) ? V31_DIALECT : deepClone(value) } -function convertSpec(spec: unknown): unknown { +export function downgradeSpecV32ToV31(spec: OpenAPIV3_2.OpenAPIObject): OpenAPIV3_1.OpenAPIObject { const context = createContext(spec) return convertRecord( spec, @@ -400,23 +288,11 @@ function convertSpec(spec: unknown): unknown { paths: item => convertPaths(item, context), servers: item => mapArray(item, convertServer), tags: item => mapArray(item, convertTag), - webhooks: item => - mapRecord(item, entry => convertPathItem(entry, context)), + webhooks: item => mapRecord(item, entry => convertPathItem(entry, context)), }, (out) => { out.openapi = '3.1.2' return out }, - ) -} - -/** - * Converts an OpenAPI 3.2 document to OpenAPI 3.1.2. The input is never - * mutated, unknown keys and existing specification extensions are preserved, - * and malformed parts are copied through unchanged instead of throwing. - */ -export function downgradeSpecV32ToV31(spec: OpenAPIV3_2.OpenAPIObject): OpenAPIV3_1.OpenAPIObject { - const converted: unknown = convertSpec(spec) - // SAFETY: convertSpec rewrites every 3.2-only construct into its 3.1 form. - return converted as OpenAPIV3_1.OpenAPIObject + ) as OpenAPIV3_1.OpenAPIObject } diff --git a/packages/downgrader/tests/corpus.test.ts b/packages/downgrader/tests/corpus.test.ts index 2ca2091..a4de396 100644 --- a/packages/downgrader/tests/corpus.test.ts +++ b/packages/downgrader/tests/corpus.test.ts @@ -77,18 +77,8 @@ import { doc as webhookExampleV32 } from '../../types/tests/schema-tests-3.2/web import { downgradeSpecV31ToV30, downgradeSpecV32ToV31 } from '../src/index' import { expectValidAs } from './helpers' -/** - * Excluded 3.1 fixtures: - * - * - `security-scheme-object-examples`: contains a `$ref` to an external URL, - * which the validator cannot resolve ("only internal refs are supported") — - * a validator limitation, not a conversion defect. - * - `style-defaults`: it carries `x-comment` inside an Encoding Object, - * which the converter rightly preserves but the official 3.0 schema - * rejects — its Encoding definition is `additionalProperties: false` - * with no `^x-` carve-out, an upstream schema strictness (the 3.0 prose - * declares the Encoding Object extensible). - */ +// Excluded: security-scheme-object-examples (external $ref the validator cannot resolve) +// and style-defaults (x-comment in an Encoding Object, rejected by the official 3.0 schema). const corpus31: readonly (readonly [ name: string, doc: OpenAPIV3_1.OpenAPIObject, @@ -137,13 +127,7 @@ const corpus31: readonly (readonly [ ['webhook-example', webhookExampleV31], ] -/** - * Excluded 3.2 fixtures: - * - * - `security-scheme-object-examples`: contains a `$ref` to an external URL, - * which the validator cannot resolve ("only internal refs are supported") — - * a validator limitation, not a conversion defect. - */ +// Excluded: security-scheme-object-examples (external $ref the validator cannot resolve). const corpus32: readonly (readonly [ name: string, doc: OpenAPIV3_2.OpenAPIObject, diff --git a/packages/downgrader/tests/e2e.test.ts b/packages/downgrader/tests/e2e.test.ts index 4fcfc40..e88b5d0 100644 --- a/packages/downgrader/tests/e2e.test.ts +++ b/packages/downgrader/tests/e2e.test.ts @@ -41,8 +41,6 @@ describe('3.1 example documents downgraded to 3.0', () => { expect(converted.paths).toMatchObject({ '/users': { get: { security: [{ bearerAuth: [] }] } }, }) - // The source operation has no responses; the synthesized minimal default - // response keeps the document valid. expect(converted.paths?.['/users']?.get?.responses).toEqual({ default: { description: '' }, }) @@ -65,8 +63,6 @@ describe('3.1 example documents downgraded to 3.0', () => { expect(converted.components).not.toHaveProperty('pathItems') expect(converted.components).not.toHaveProperty('x-pathItems') expect(converted.components?.securitySchemes).toEqual({}) - // The only reference into components.pathItems lived in the removed - // webhooks, so no trace of the reusable path items remains. expect(JSON.stringify(converted)).not.toContain('#/components/pathItems/') await expectValidAs(converted, '3.0') expect(converted).toMatchSnapshot() @@ -130,8 +126,6 @@ describe('3.1 example documents downgraded to 3.0', () => { } as any const before = structuredClone(doc) const converted = downgradeSpecV31ToV30(doc) - // defaultMapping is not a schema keyword the 3.0 converter touches, and - // the official 3.0 schema allows extra discriminator fields. expect(converted).toHaveProperty( ['components', 'schemas', 'Pet', 'discriminator'], { @@ -150,8 +144,6 @@ describe('3.2 example documents downgraded to 3.1 and chained to 3.0', () => { const before = structuredClone(queryExample) const v31 = downgradeSpecV32ToV31(queryExample) expect(v31.openapi).toBe('3.1.2') - // The QUERY operation has no 3.1 equivalent; an empty Path Item Object - // is legal in both 3.1 and 3.0. expect(v31.paths?.['/flights/search']).toEqual({}) expect(JSON.stringify(v31)).not.toContain('x-additionalOperations') await expectValidAs(v31, '3.1') @@ -209,8 +201,6 @@ describe('3.2 example documents downgraded to 3.1 and chained to 3.0', () => { 'schema', 'discriminator', ] - // Schema Objects pass through unchanged in 3.2 -> 3.1, so the 3.2-only - // discriminator defaultMapping survives as an extra JSON Schema keyword. expect(v31).toHaveProperty( [...megaDiscriminatorPath, 'defaultMapping'], 'Bar', @@ -228,8 +218,6 @@ describe('3.2 example documents downgraded to 3.1 and chained to 3.0', () => { const v30 = downgradeSpecV31ToV30(v31) expect(v30.openapi).toBe('3.0.4') - // The discriminator lives in components.pathItems, which 3.0 cannot - // express, so it disappears together with its host in this hop. expect(v30.components).not.toHaveProperty('pathItems') expect(v30).not.toHaveProperty('webhooks') await expectValidAs(v30, '3.0') @@ -374,8 +362,6 @@ describe('kitchen-sink 3.2 document chained down to 3.0', () => { expect(v31.servers).toEqual([{ url: 'https://api.example.com' }]) expect(v31.components).not.toHaveProperty('mediaTypes') expect(JSON.stringify(v31)).not.toContain('#/components/mediaTypes/') - // The querystring component is removed outright; the deviceAuthorization - // flow, oauth2MetadataUrl, and deprecated have no 3.1 equivalent either. expect(v31.components?.parameters).toEqual({ page: { in: 'query', name: 'page', schema: { type: 'integer' } }, }) @@ -402,9 +388,6 @@ describe('kitchen-sink 3.2 document chained down to 3.0', () => { }, 204: { description: '' }, }) - // The querystring parameter is removed from the list; the cookie style - // is removed from the remaining parameter, and its examples promote - // dataValue/serializedValue into free value slots only. expect(v31.paths?.['/search']?.get?.parameters).toEqual([ { examples: { diff --git a/packages/downgrader/tests/helpers.ts b/packages/downgrader/tests/helpers.ts index 7e202a7..0099c95 100644 --- a/packages/downgrader/tests/helpers.ts +++ b/packages/downgrader/tests/helpers.ts @@ -1,7 +1,6 @@ import { Validator } from '@seriousme/openapi-schema-validator' import { expect } from 'vitest' -/** Walks converter output along `path`; the surrounding assertions pin down its shape. */ export function dig(value: unknown, ...path: string[]): unknown { let current: unknown = value for (const key of path) { diff --git a/packages/types/src/v3.0.ts b/packages/types/src/v3.0.ts index c6495c3..c1f9777 100644 --- a/packages/types/src/v3.0.ts +++ b/packages/types/src/v3.0.ts @@ -1,13 +1,3 @@ -/** - * TypeScript types for the OpenAPI Specification v3.0, authored against the - * latest patch release 3.0.4. - * - * Type names follow the specification's section names, and every field - * carries its specification description as JSDoc. - * - * @see {@link https://spec.openapis.org/oas/v3.0.4.html} - */ - /** * While the OpenAPI Specification tries to accommodate most use cases, * additional data can be added to extend the specification at certain points. diff --git a/packages/types/src/v3.1.test-d.ts b/packages/types/src/v3.1.test-d.ts index eca39d6..81512da 100644 --- a/packages/types/src/v3.1.test-d.ts +++ b/packages/types/src/v3.1.test-d.ts @@ -129,7 +129,6 @@ export const referenceOverrides = { paths: { '/pets': { get: { - // `responses` is no longer REQUIRED in 3.1. parameters: [ { $ref: '#/components/parameters/limit', @@ -149,7 +148,6 @@ export const wrongVersion = { paths: {}, } satisfies OpenAPIObject -// Boolean schemas are valid Schema Objects in OpenAPI 3.1. export const booleanSchema = true satisfies SchemaObject export const numericExclusiveBounds = { diff --git a/packages/types/src/v3.1.ts b/packages/types/src/v3.1.ts index 94c7b6e..a4e2422 100644 --- a/packages/types/src/v3.1.ts +++ b/packages/types/src/v3.1.ts @@ -1,18 +1,3 @@ -/** - * TypeScript types for the OpenAPI Specification v3.1, authored against the - * latest patch release 3.1.2. - * - * Types that are structurally identical to OpenAPI 3.0 (including everything - * they reference) are re-exported from `./v3.0`; every other type is - * redefined here. The most significant difference is that the 3.1 Schema - * Object is a superset of JSON Schema Draft 2020-12, so `nullable` is gone - * (use `type` arrays including `"null"`), `$ref` is a plain schema keyword - * (no more `Schema Object | Reference Object` unions), and boolean schemas - * are valid Schema Objects. - * - * @see {@link https://spec.openapis.org/oas/v3.1.2.html} - */ - import type { ApiKeySecuritySchemeObject, ContactObject, @@ -1035,8 +1020,6 @@ export interface SchemaObjectFields { */ [keyword: string]: unknown - // JSON Schema Core vocabulary - /** * The URI of the dialect (meta-schema) this schema conforms to. MAY be * present in any schema resource root, and if present MUST be used to @@ -1089,8 +1072,6 @@ export interface SchemaObjectFields { */ $comment?: string - // JSON Schema Applicator vocabulary - /** * An instance is valid against this keyword if it is valid against all * subschemas in this array. `allOf` offers model composition; with @@ -1167,8 +1148,6 @@ export interface SchemaObjectFields { */ propertyNames?: SchemaObject - // JSON Schema Unevaluated vocabulary - /** * A subschema applied to array items not successfully evaluated by any * `prefixItems`, `items`, or `contains` in this schema or its subschemas. @@ -1181,8 +1160,6 @@ export interface SchemaObjectFields { */ unevaluatedProperties?: SchemaObject - // JSON Schema Validation vocabulary - /** * The data type of the schema: a string or an array of unique strings. * `"null"` is a first-class type value (replacing OpenAPI 3.0's `nullable` @@ -1293,8 +1270,6 @@ export interface SchemaObjectFields { */ dependentRequired?: Record - // JSON Schema Meta-Data vocabulary - /** * A short title for the schema. */ @@ -1341,8 +1316,6 @@ export interface SchemaObjectFields { */ examples?: T[] - // JSON Schema Format-Annotation vocabulary - /** * The format of the data type. While relying on JSON Schema's defined * formats, the OAS offers a few additional predefined formats: `"int32"`, @@ -1354,8 +1327,6 @@ export interface SchemaObjectFields { */ format?: string - // JSON Schema Content vocabulary - /** * The encoding (e.g. `base64`, `base64url`) used to represent binary data * as a string instance, replacing the OpenAPI 3.0 `format: "byte"` usage. @@ -1377,8 +1348,6 @@ export interface SchemaObjectFields { */ contentSchema?: SchemaObject - // OAS base vocabulary - /** * Adds support for polymorphism. The discriminator is used to determine * which of a set of schemas a payload is expected to satisfy. The diff --git a/packages/types/src/v3.2.ts b/packages/types/src/v3.2.ts index e07e4c3..0f015d8 100644 --- a/packages/types/src/v3.2.ts +++ b/packages/types/src/v3.2.ts @@ -1,18 +1,3 @@ -/** - * TypeScript types for the OpenAPI Specification v3.2, authored against - * release 3.2.0. - * - * Types that are structurally identical to OpenAPI 3.1 (including everything - * they reference) are re-exported from `./v3.1`; every other type is - * redefined here. Highlights of 3.2: the `$self` document URI, tag hierarchy - * (`parent`/`kind`), the QUERY HTTP method and `additionalOperations`, the - * `querystring` parameter location, streaming media types (`itemSchema`, - * `itemEncoding`, `prefixEncoding`), reusable media types in components, the - * OAuth2 Device Authorization flow, and the XML `nodeType` model. - * - * @see {@link https://spec.openapis.org/oas/v3.2.0.html} - */ - import type { AuthorizationCodeOAuthFlowObject, ClientCredentialsOAuthFlowObject, @@ -1133,8 +1118,6 @@ export interface SchemaObjectFields { */ [keyword: string]: unknown - // JSON Schema Core vocabulary - /** * The URI of the dialect (meta-schema) this schema conforms to. MAY be * present in any schema resource root, and if present MUST be used to @@ -1189,8 +1172,6 @@ export interface SchemaObjectFields { */ $comment?: string - // JSON Schema Applicator vocabulary - /** * An instance is valid against this keyword if it is valid against all * subschemas in this array. `allOf` offers model composition; with @@ -1267,8 +1248,6 @@ export interface SchemaObjectFields { */ propertyNames?: SchemaObject - // JSON Schema Unevaluated vocabulary - /** * A subschema applied to array items not successfully evaluated by any * `prefixItems`, `items`, or `contains` in this schema or its subschemas. @@ -1281,8 +1260,6 @@ export interface SchemaObjectFields { */ unevaluatedProperties?: SchemaObject - // JSON Schema Validation vocabulary - /** * The data type of the schema: a string or an array of unique strings. * `"null"` is a first-class type value. Note that keywords and formats do @@ -1396,8 +1373,6 @@ export interface SchemaObjectFields { */ dependentRequired?: Record - // JSON Schema Meta-Data vocabulary - /** * A short title for the schema. */ @@ -1443,8 +1418,6 @@ export interface SchemaObjectFields { */ examples?: T[] - // JSON Schema Format-Annotation vocabulary - /** * The format of the data type. While relying on JSON Schema's defined * formats, the OAS offers a few additional predefined formats: `"int32"`, @@ -1456,8 +1429,6 @@ export interface SchemaObjectFields { */ format?: string - // JSON Schema Content vocabulary - /** * The encoding (`base64`, `base64url`, or another encoding) used to * represent binary data as a string instance. Unrelated to the HTTP @@ -1481,8 +1452,6 @@ export interface SchemaObjectFields { */ contentSchema?: SchemaObject - // OAS base vocabulary - /** * Provides a hint about which of a set of schemas a payload is expected to * satisfy. Legal only when using one of the composite keywords `oneOf`,