Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 71 additions & 6 deletions packages/downgrader/benches/__shared__/graphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,84 @@ export function callbackGraphV32(k: number): OpenAPIV3_2.OpenAPIObject {
}
}

/**
* The components `<name>0` … `<name><length>` of one kind, each a `$ref` to
* the next but the last, which is `end`.
*/
function refChain<T>(
kind: string,
name: string,
length: number,
end: T,
): Record<string, T | { $ref: string }> {
const chain: Record<string, T | { $ref: string }> = { [`${name}${length}`]: end }
for (let index = 0; index < length; index++) {
chain[`${name}${index}`] = { $ref: `#/components/${kind}/${name}${index + 1}` }
}
return chain
}

/**
* `length` Path Items in `components.pathItems`, which 3.0 lacks, each a
* `$ref` to the next, with a path entering the chain at every one of them.
*/
export function pathItemChainV31(length: number): OpenAPIV3_1.OpenAPIObject {
const pointer = (index: number): string => `#/components/pathItems/P${index}`
const pathItems: Record<string, OpenAPIV3_1.PathItemObject> = {
[`P${length}`]: { get: { responses: { 200: { description: 'ok' } } } },
}
const pathItems = refChain('pathItems', 'P', length, {
get: { responses: { 200: { description: 'ok' } } },
})
const paths: OpenAPIV3_1.PathsObject = {}
for (let index = 0; index < length; index++) {
pathItems[`P${index}`] = { $ref: pointer(index + 1) }
paths[`/p${index}`] = { $ref: pointer(index) }
paths[`/p${index}`] = { $ref: `#/components/pathItems/P${index}` }
}
return { openapi: '3.1.2', info: info(), components: { pathItems }, paths }
}

/**
* `length` Security Schemes, each a `$ref` to the next, ending at an HTTP
* scheme, with `length` operations requiring the first. The chain is entered
* at every scheme too, when the schemes themselves are converted.
*
* Every requirement names the same scheme: requirements naming `length`
* different ones would each give the empty object's hidden class its own
* transition, and growing that list slows every other bench in this file.
*/
export function securitySchemeChainV31(length: number): OpenAPIV3_1.OpenAPIObject {
const securitySchemes = refChain<OpenAPIV3_1.SecuritySchemeObject>(
'securitySchemes',
'S',
length,
{
type: 'http',
scheme: 'bearer',
},
)
const paths: OpenAPIV3_1.PathsObject = {}
for (let index = 0; index < length; index++) {
paths[`/p${index}`] = {
get: { security: [{ S0: [] }], responses: { 200: { description: 'ok' } } },
}
}
return { openapi: '3.1.2', info: info(), components: { securitySchemes }, paths }
}

/**
* `length` Parameters, each a `$ref` to the next, ending at a query
* parameter, with an operation using every one of them.
*/
export function parameterChainV32(length: number): OpenAPIV3_2.OpenAPIObject {
const parameters = refChain<OpenAPIV3_2.ParameterObject>('parameters', 'P', length, {
name: 'q',
in: 'query',
schema: { type: 'string' },
})
const paths: OpenAPIV3_2.PathsObject = {}
for (let index = 0; index < length; index++) {
paths[`/p${index}`] = {
get: {
parameters: [{ $ref: `#/components/parameters/P${index}` }],
responses: { 200: { description: 'ok' } },
},
}
}
return { openapi: '3.2.0', info: info(), components: { parameters }, paths }
}
6 changes: 6 additions & 0 deletions packages/downgrader/benches/v3.1-to-v3.0.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
pathItemChainV31,
referenceDiamondV31,
schemaDiamond,
securitySchemeChainV31,
} from './__shared__/graphs'
import { ORDER_SCHEMA, PROPERTY_SCHEMA } from './__shared__/schemas'

Expand All @@ -16,6 +17,7 @@ const API_100_RESOURCES = createApiV31(100)
const REFERENCE_DIAMOND = referenceDiamondV31(64)
const CALLBACK_GRAPH = callbackGraphV31(8)
const PATH_ITEM_CHAIN = pathItemChainV31(200)
const SECURITY_SCHEME_CHAIN = securitySchemeChainV31(1000)
const SCHEMA_DIAMOND = schemaDiamond(64)

describe('downgradeSpecV31ToV30', () => {
Expand Down Expand Up @@ -44,6 +46,10 @@ describe('downgradeSpecV31ToV30', () => {
bench('path item $ref chain in removed components, 200 hops', () => {
downgradeSpecV31ToV30(PATH_ITEM_CHAIN)
})

bench('security scheme $ref chain, 1000 hops', () => {
downgradeSpecV31ToV30(SECURITY_SCHEME_CHAIN)
})
})

describe('downgradeSchemaV31ToV30', () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/downgrader/benches/v3.2-to-v3.1.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { bench, describe } from 'vitest'

import { corpusV32 } from '../tests/corpus'
import { createApiV32 } from './__shared__/api'
import { callbackGraphV32, referenceDiamondV32, schemaDiamond } from './__shared__/graphs'
import {
callbackGraphV32,
parameterChainV32,
referenceDiamondV32,
schemaDiamond,
} from './__shared__/graphs'
import { ORDER_SCHEMA, PROPERTY_SCHEMA } from './__shared__/schemas'

const API_10_RESOURCES = createApiV32(10)
const API_100_RESOURCES = createApiV32(100)
const REFERENCE_DIAMOND = referenceDiamondV32(64)
const CALLBACK_GRAPH = callbackGraphV32(8)
const PARAMETER_CHAIN = parameterChainV32(1000)
const SCHEMA_DIAMOND = schemaDiamond(64)

describe('downgradeSpecV32ToV31', () => {
Expand All @@ -34,6 +40,10 @@ describe('downgradeSpecV32ToV31', () => {
bench('cyclic callback graph in a removed operation, 8 path items', () => {
downgradeSpecV32ToV31(CALLBACK_GRAPH)
})

bench('parameter $ref chain, 1000 hops', () => {
downgradeSpecV32ToV31(PARAMETER_CHAIN)
})
})

describe('downgradeSchemaV32ToV31', () => {
Expand Down
26 changes: 23 additions & 3 deletions packages/downgrader/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface Context {
readonly root: unknown
/** The target of each `$ref` resolved so far. */
readonly targets: Map<string, unknown>
/** Where the chain from each `$ref` followed so far ends, or `undefined` if it dangles or loops. */
readonly resolved: Map<string, unknown>
/** The output of each object converted so far, per field table, so a shared object is converted once and a cycle ends. */
readonly seen: Map<Fields, Map<object, Record<string, unknown>>>
/** The targets of the `$ref`s being inlined, so a reference back into one stops there. */
Expand Down Expand Up @@ -220,17 +222,29 @@ export function resolvePointer(ref: string, ctx: Context): unknown {
return target
}

/** Follows `ref`, and any `$ref` its target holds in turn, to the value they end at. */
/**
* Follows `ref`, and any `$ref` its target holds in turn, to the value they
* end at, and records that end for every `$ref` on the way, so a chain is
* walked once however many references lead into it.
*/
export function resolve(ref: string, ctx: Context): unknown {
const visited = new Set<string>()
let target: unknown
for (let next: string | undefined = ref; next !== undefined; next = getRef(target)) {
if (ctx.resolved.has(next)) {
target = ctx.resolved.get(next)
break
}
if (visited.has(next)) {
return undefined
target = undefined
break
}
visited.add(next)
target = resolvePointer(next, ctx)
}
for (const each of visited) {
ctx.resolved.set(each, target)
}
return target
}

Expand All @@ -251,5 +265,11 @@ export function inline(ref: string, ctx: Context, convert: Convert): unknown {
}

export function downgrade(root: unknown, convert: Convert): unknown {
return convert(root, { inlining: new Set(), root, seen: new Map(), targets: new Map() })
return convert(root, {
inlining: new Set(),
resolved: new Map(),
root,
seen: new Map(),
targets: new Map(),
})
}
24 changes: 24 additions & 0 deletions packages/downgrader/tests/v3.1-to-v3.0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,30 @@ describe('downgradeSpecV31ToV30', () => {
expect(out.paths['/a']?.put).toEqual({ responses })
expect(out.paths['/a']?.post?.security).toEqual([])
})

it('follows chains of $refs entered at any point, and keeps the names of those in a cycle', () => {
const ref = (name: string) => ({ $ref: `#/components/securitySchemes/${name}` })
const out = convert({
paths: {
'/a': {
get: { security: [{ c: [] }, { b: [], z: ['s'] }, { a: [] }, { x: ['s'] }], responses },
},
},
components: {
securitySchemes: {
a: ref('b'),
b: ref('c'),
c: ref('mtls'),
mtls: { type: 'mutualTLS' },
x: ref('y'),
y: ref('x'),
z: ref('y'),
},
},
})
expect(out.paths['/a']?.get?.security).toEqual([{ z: ['s'] }, { x: ['s'] }])
expect(out.components?.securitySchemes).toEqual({ x: ref('y'), y: ref('x'), z: ref('y') })
})
})

it('empties the scopes of apiKey and http requirements, which 3.0 allows only for OAuth2 and OpenID Connect', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/downgrader/tests/v3.2-to-v3.1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,30 @@ describe('downgradeSpecV32ToV31', () => {
expect(out.components?.parameters).toEqual({ Kept: { in: 'query', name: 'k' } })
})

it('follows chains of $refs entered at any point, and keeps those in a cycle', () => {
const ref = (name: string) => ({ $ref: `#/components/parameters/${name}` })
const out = convert({
paths: {
'/a': {
get: { parameters: [ref('C'), ref('B'), ref('Z'), ref('A'), ref('X')], responses: {} },
},
},
components: {
parameters: {
A: ref('B'),
B: ref('C'),
C: ref('Q'),
Q: { in: 'querystring', name: 'q', content: { 'text/plain': {} } },
X: ref('Y'),
Y: ref('X'),
Z: ref('Y'),
},
},
})
expect(out.paths?.['/a']?.get?.parameters).toEqual([ref('Z'), ref('X')])
expect(out.components?.parameters).toEqual({ X: ref('Y'), Y: ref('X'), Z: ref('Y') })
})

it('keeps allowReserved only on query parameters', () => {
const out = convertOperation({
parameters: [
Expand Down
Loading