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 e241c01 commit 8ecf50b
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 449,918 deletions.
70 changes: 43 additions & 27 deletions src/annotator.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,57 @@
import {isPlainObject} from 'lodash'
import {DereferencedPaths} from './resolver'
import {AnnotatedJSONSchema, JSONSchema, Parent, isAnnotated} from './types/JSONSchema'
import {AnnotatedJSONSchema, IsSchema, JSONSchema, Parent, isAnnotated} from './types/JSONSchema'
import {isSchemaLike} from './utils'

/**
* 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) => void>()

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

// Add a reference to this schema's parent
annotators.add(function annotateParent(schema, parent) {
Object.defineProperty(schema, Parent, {
enumerable: false,
value: parent,
writable: false,
})
})

// Arrays
if (Array.isArray(schema)) {
schema.forEach(child => annotate(child, dereferencedPaths, schema))
}
annotators.add(function annotateSchemas(schema) {
Object.defineProperty(schema, IsSchema, {
enumerable: false,
value: isSchemaLike(schema),
writable: false,
})
})

/**
* Traverses over the schema, assigning to each
* node metadata that will be used downstream.
*/
export function annotate(schema: JSONSchema): 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)
})

// Objects
for (const key in schema) {
annotate(schema[key], dereferencedPaths, schema)
// 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
}
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface Options {
/**
* Custom function to provide a type name for a given schema
*/
customName?: (schema: AnnotatedJSONSchema, keyNameFromDefinition: string | undefined) => string | undefined
customName?: (schema: AnnotatedJSONSchema) => string | undefined
/**
* Root directory for resolving [`$ref`](https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html)s.
*/
Expand Down Expand Up @@ -150,7 +150,7 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
// Initial clone to avoid mutating the input
const _schema = cloneDeep(schema)

const {dereferencedPaths, dereferencedSchema} = await dereference(_schema, _options)
const dereferencedSchema = await dereference(_schema, _options)
if (process.env.VERBOSE) {
if (isDeepStrictEqual(_schema, dereferencedSchema)) {
log('green', 'dereferencer', time(), '✅ No change')
Expand All @@ -159,7 +159,7 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
}
}

const annotated = annotate(dereferencedSchema, dereferencedPaths)
const annotated = annotate(dereferencedSchema)
if (process.env.VERBOSE) {
log('green', 'annotater', time(), '✅ No change')
}
Expand All @@ -173,7 +173,7 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
log('green', 'validator', time(), '✅ No change')
}

const normalized = normalize(annotated, dereferencedPaths, name, _options)
const normalized = normalize(annotated, name, _options)
log('yellow', 'normalizer', time(), '✅ Result:', normalized)

const parsed = parse(normalized, _options)
Expand Down
116 changes: 83 additions & 33 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {JSONSchemaTypeName, AnnotatedJSONSchema, NormalizedJSONSchema, JSONSchema, Parent} from './types/JSONSchema'
import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils'
import {
JSONSchemaTypeName,
AnnotatedJSONSchema,
NormalizedJSONSchema,
JSONSchema,
Parent,
Ref,
IsSchema,
} from './types/JSONSchema'
import {appendToDescription, escapeBlockComment, justName, toSafeString, traverse} from './utils'
import {Options} from './'
import {DereferencedPaths} from './resolver'
import {isDeepStrictEqual} from 'util'
import {typesOfSchema} from './typesOfSchema'

type Rule = (
schema: AnnotatedJSONSchema,
fileName: string,
options: Options,
key: string | null,
dereferencedPaths: DereferencedPaths,
) => void
type Rule = (schema: AnnotatedJSONSchema, fileName: string, options: Options, key: string | null) => void
const rules = new Map<string, Rule>()

function hasType(schema: JSONSchema, type: JSONSchemaTypeName) {
Expand Down Expand Up @@ -62,9 +64,10 @@ rules.set('Default additionalProperties', (schema, _, options) => {
})

rules.set('Transform id to $id', (schema, fileName) => {
if (!isSchemaLike(schema)) {
if (!schema[IsSchema]) {
return
}

if (schema.id && schema.$id && schema.id !== schema.$id) {
throw ReferenceError(
`Schema must define either id or $id, not both. Given id=${schema.id}, $id=${schema.$id} in ${fileName}`,
Expand All @@ -76,32 +79,28 @@ rules.set('Transform id to $id', (schema, fileName) => {
}
})

rules.set('Add an $id to anything that needs it', (schema, fileName, _options, _key, dereferencedPaths) => {
if (!isSchemaLike(schema)) {
rules.set('Add an $id to each top-level schema', (schema, fileName) => {
if (schema.$id || schema[Parent]) {
return
}

// Top-level schema
if (!schema.$id && !schema[Parent]) {
schema.$id = toSafeString(justName(fileName))
if (!schema[IsSchema]) {
return
}

// Sub-schemas with references
if (!isArrayType(schema) && !isObjectType(schema)) {
schema.$id = toSafeString(justName(fileName))
})

rules.set('Add an $id to each referenced schema', schema => {
if (schema.$id) {
return
}

// We'll infer from $id and title downstream
// TODO: Normalize upstream
const dereferencedName = dereferencedPaths.get(schema)
if (!schema.$id && !schema.title && dereferencedName) {
schema.$id = toSafeString(justName(dereferencedName))
if (!schema[Ref]) {
return
}

if (dereferencedName) {
dereferencedPaths.delete(schema)
}
schema.$id = toSafeString(justName(schema[Ref]))
})

rules.set('Escape closing JSDoc comment', schema => {
Expand Down Expand Up @@ -218,6 +217,35 @@ rules.set('Transform definitions to $defs', (schema, fileName) => {
}
})

rules.set(
"Add an $id to each $def that doesn't have one, if unreachableDefinitions is enabled",
(schema, _, options) => {
if (!options.unreachableDefinitions) {
return
}

if (schema.$id) {
return
}

const parent = schema[Parent]
if (!parent) {
return
}

const grandparent = parent[Parent]
if (!grandparent) {
return
}

if (Object.keys(grandparent).find(_ => grandparent[_] === parent) !== '$defs') {
return
}

schema.$id = toSafeString(Object.keys(parent).find(_ => parent[_] === schema)!)
},
)

rules.set('Transform const to singleton enum', schema => {
if (schema.const !== undefined) {
schema.enum = [schema.const]
Expand All @@ -231,12 +259,34 @@ rules.set('Add tsEnumNames to enum types', (schema, _, options) => {
}
})

export function normalize(
rootSchema: AnnotatedJSONSchema,
dereferencedPaths: DereferencedPaths,
filename: string,
options: Options,
): NormalizedJSONSchema {
rules.forEach(rule => traverse(rootSchema, (schema, key) => rule(schema, filename, options, key, dereferencedPaths)))
rules.set('Add an $id to each named enum', schema => {
if (!schema[IsSchema]) {
return
}

if (schema.$id) {
return
}

if (!typesOfSchema(schema).includes('NAMED_ENUM')) {
return
}

const parent = schema[Parent]
const keyName = Object.keys(parent).find(_ => parent[_] === schema)

// Special case: generate nicer names for additionalProperties enums
if (parent[IsSchema] && keyName === 'additionalProperties') {
const grandparent = parent[Parent]
const parentKeyName = Object.keys(grandparent).find(_ => grandparent[_] === parent)!
schema.$id = toSafeString(parentKeyName) + toSafeString(keyName)
return
}

schema.$id = toSafeString(justName(keyName))
})

export function normalize(rootSchema: AnnotatedJSONSchema, filename: string, options: Options): NormalizedJSONSchema {
rules.forEach(rule => traverse(rootSchema, (schema, key) => rule(schema, filename, options, key)))
return rootSchema as NormalizedJSONSchema
}
Loading

0 comments on commit 8ecf50b

Please sign in to comment.