Skip to content

Commit

Permalink
replace legacy mechanism for looking up key name from definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
bcherny committed Jun 26, 2024
1 parent 44793a4 commit 6908266
Show file tree
Hide file tree
Showing 5 changed files with 57,637 additions and 84,404 deletions.
120 changes: 96 additions & 24 deletions src/annotator.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,113 @@
import {isPlainObject} from 'lodash'
import {DereferencedPaths} from './resolver'
import {AnnotatedJSONSchema, JSONSchema, Parent, isAnnotated} from './types/JSONSchema'
import {AnnotatedJSONSchema, JSONSchema, Parent, KeyNameFromDefinition, isAnnotated} from './types/JSONSchema'

/**
* Traverses over the schema, assigning to each
* node metadata that will be used downstream.
*/
export function annotate(
schema: JSONSchema,
dereferencedPaths: DereferencedPaths,
parent: JSONSchema | null = null,
): AnnotatedJSONSchema {
if (!Array.isArray(schema) && !isPlainObject(schema)) {
return schema as AnnotatedJSONSchema
const annotators = new Set<
(schema: JSONSchema, parent: JSONSchema | null, dereferencedPaths: DereferencedPaths) => void
>()

// note: this must run first, since it is used by the next annotator
annotators.add(function annotateParent(schema, parent) {
Object.defineProperty(schema, Parent, {
enumerable: false,
value: parent,
writable: false,
})
})

annotators.add(function annotateKeyNameFromDefinition(schema) {
const keyNameFromSchema = getDefinitionKeyNameFromParent(schema as AnnotatedJSONSchema) // todo
if (!keyNameFromSchema) {
return
}

// Handle cycles
if (isAnnotated(schema)) {
return schema
Object.defineProperty(schema, KeyNameFromDefinition, {
enumerable: false,
value: keyNameFromSchema,
writable: false,
})
})

annotators.add(function annotateKeyNameFrom$Ref(schema, _, dereferencedPaths) {
if (schema.hasOwnProperty(KeyNameFromDefinition)) {
return
}

// Add a reference to this schema's parent
Object.defineProperty(schema, Parent, {
const ref = dereferencedPaths.get(schema)
if (!ref) {
return
}

const keyNameFromRef = getDefinitionKeyNameFromRef(ref)
if (!keyNameFromRef) {
return
}

Object.defineProperty(schema, KeyNameFromDefinition, {
enumerable: false,
value: parent,
value: keyNameFromRef,
writable: false,
})
})

// Arrays
if (Array.isArray(schema)) {
schema.forEach(child => annotate(child, dereferencedPaths, schema))
/**
* If this schema came from a `definitions` block, returns the key name for the definition.
*/
function getDefinitionKeyNameFromParent(schema: AnnotatedJSONSchema): string | undefined {
const parent = schema[Parent]
if (!parent) {
return
}
const grandparent = parent[Parent]
if (!grandparent) {
return
}
if (Object.keys(grandparent).find(_ => grandparent[_] === parent) !== 'definitions') {
return
}
return Object.keys(parent).find(_ => parent[_] === schema)
}

// Objects
for (const key in schema) {
annotate(schema[key], dereferencedPaths, schema)
function getDefinitionKeyNameFromRef(ref: string): string | undefined {
const parts = ref.split('/')
if (parts[parts.length - 2] !== 'definitions') {
return
}
return parts[parts.length - 1]
}

/**
* Traverses over the schema, assigning to each
* node metadata that will be used downstream.
*/
export function annotate(schema: JSONSchema, dereferencedPaths: DereferencedPaths): AnnotatedJSONSchema {
function go(s: JSONSchema, parent: JSONSchema | null): void {
if (!Array.isArray(s) && !isPlainObject(s)) {
return
}

// Handle cycles
if (isAnnotated(s)) {
return
}

// Run annotators
annotators.forEach(f => {
f(s, parent, dereferencedPaths)
})

// Handle arrays
if (Array.isArray(s)) {
s.forEach(_ => go(_, s))
}

// Handle objects
for (const key in s) {
go(s[key], s)
}
}

go(schema, null)

return schema as AnnotatedJSONSchema
}
52 changes: 3 additions & 49 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {JSONSchema4Type, JSONSchema4TypeName} from 'json-schema'
import {findKey, includes, isPlainObject, map, memoize, omit} from 'lodash'
import {includes, map, omit} from 'lodash'
import {format} from 'util'
import {Options} from './'
import {typesOfSchema} from './typesOfSchema'
Expand All @@ -17,10 +17,9 @@ import {
} from './types/AST'
import {
EnumJSONSchema,
getRootSchema,
isBoolean,
isPrimitive,
JSONSchemaWithDefinitions,
KeyNameFromDefinition,
NormalizedJSONSchema,
Parent,
SchemaSchema,
Expand Down Expand Up @@ -143,8 +142,7 @@ function parseNonLiteral(
processed: Processed,
usedNames: UsedNames,
): AST {
const definitions = getDefinitionsMemoized(getRootSchema(schema as any)) // TODO
const keyNameFromDefinition = findKey(definitions, _ => _ === schema)
const keyNameFromDefinition = schema[KeyNameFromDefinition]

switch (type) {
case 'ALL_OF':
Expand Down Expand Up @@ -487,47 +485,3 @@ via the \`definition\` "${key}".`
})
}
}

type Definitions = {[k: string]: NormalizedJSONSchema}

function getDefinitions(
schema: NormalizedJSONSchema,
isSchema = true,
processed = new Set<NormalizedJSONSchema>(),
): Definitions {
if (processed.has(schema)) {
return {}
}
processed.add(schema)
if (Array.isArray(schema)) {
return schema.reduce(
(prev, cur) => ({
...prev,
...getDefinitions(cur, false, processed),
}),
{},
)
}
if (isPlainObject(schema)) {
return {
...(isSchema && hasDefinitions(schema) ? schema.$defs : {}),
...Object.keys(schema).reduce<Definitions>(
(prev, cur) => ({
...prev,
...getDefinitions(schema[cur], false, processed),
}),
{},
),
}
}
return {}
}

const getDefinitionsMemoized = memoize(getDefinitions)

/**
* TODO: Reduce rate of false positives
*/
function hasDefinitions(schema: NormalizedJSONSchema): schema is JSONSchemaWithDefinitions {
return '$defs' in schema
}
21 changes: 6 additions & 15 deletions src/types/JSONSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {JSONSchema4, JSONSchema4Type, JSONSchema4TypeName} from 'json-schema'
import {isPlainObject, memoize} from 'lodash'
import {isPlainObject} from 'lodash'

export type SchemaType =
| 'ALL_OF'
Expand Down Expand Up @@ -40,10 +40,15 @@ export interface JSONSchema extends JSONSchema4 {
deprecated?: boolean
}

export const KeyNameFromDefinition = Symbol('KeyNameFromDefinition')
export const Parent = Symbol('Parent')

export interface AnnotatedJSONSchema extends JSONSchema {
[Parent]: AnnotatedJSONSchema
/**
* The name of the definition from the original $ref that was dereferenced, if there was one.
*/
[KeyNameFromDefinition]?: string

additionalItems?: boolean | AnnotatedJSONSchema
additionalProperties?: boolean | AnnotatedJSONSchema
Expand Down Expand Up @@ -112,24 +117,10 @@ export interface SchemaSchema extends NormalizedJSONSchema {
required: string[]
}

export interface JSONSchemaWithDefinitions extends NormalizedJSONSchema {
$defs: {
[k: string]: NormalizedJSONSchema
}
}

export interface CustomTypeJSONSchema extends NormalizedJSONSchema {
tsType: string
}

export const getRootSchema = memoize((schema: NormalizedJSONSchema): NormalizedJSONSchema => {
const parent = schema[Parent]
if (!parent) {
return schema
}
return getRootSchema(parent)
})

export function isBoolean(schema: AnnotatedJSONSchema | JSONSchemaType): schema is boolean {
return schema === true || schema === false
}
Expand Down
141,848 changes: 57,532 additions & 84,316 deletions test/__snapshots__/test/test.ts.md

Large diffs are not rendered by default.

Binary file modified test/__snapshots__/test/test.ts.snap
Binary file not shown.

0 comments on commit 6908266

Please sign in to comment.