Skip to content
Merged
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
9 changes: 1 addition & 8 deletions packages/downgrader/src/shared.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ function identity<T>(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',
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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])
})

Expand Down
51 changes: 1 addition & 50 deletions packages/downgrader/src/shared.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => 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<Record<string, FieldConverter | typeof DROP>>

/** 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',
Expand All @@ -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<string, unknown> {
if (typeof value !== 'object' || value === null) {
return false
Expand All @@ -52,10 +27,6 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
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, {
Expand All @@ -66,7 +37,6 @@ export function setOwn(object: object, key: PropertyKey, value: unknown): void {
})
}
else {
// SAFETY: only widens the index signature.
(object as Record<PropertyKey, unknown>)[key] = value
}
}
Expand Down Expand Up @@ -95,33 +65,15 @@ function cloneValue(value: unknown, seen: WeakMap<object, unknown>): 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<T>(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<object, Record<string, unknown>>()

/**
* 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<string, unknown>, source: Record<string, unknown>) => unknown): unknown {
if (!isRecord(value)) {
return deepClone(value)
Expand All @@ -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)
}
Expand Down
3 changes: 0 additions & 3 deletions packages/downgrader/src/v3.1-to-v3.0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} }

Expand Down Expand Up @@ -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,
Expand Down
Loading