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`,