Skip to content

Commit 8ecf50b

Browse files
committed
replace legacy mechanism for looking up key name from definitions
1 parent e241c01 commit 8ecf50b

File tree

14 files changed

+267
-449918
lines changed

14 files changed

+267
-449918
lines changed

src/annotator.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,57 @@
11
import {isPlainObject} from 'lodash'
2-
import {DereferencedPaths} from './resolver'
3-
import {AnnotatedJSONSchema, JSONSchema, Parent, isAnnotated} from './types/JSONSchema'
2+
import {AnnotatedJSONSchema, IsSchema, JSONSchema, Parent, isAnnotated} from './types/JSONSchema'
3+
import {isSchemaLike} from './utils'
44

5-
/**
6-
* Traverses over the schema, assigning to each
7-
* node metadata that will be used downstream.
8-
*/
9-
export function annotate(
10-
schema: JSONSchema,
11-
dereferencedPaths: DereferencedPaths,
12-
parent: JSONSchema | null = null,
13-
): AnnotatedJSONSchema {
14-
if (!Array.isArray(schema) && !isPlainObject(schema)) {
15-
return schema as AnnotatedJSONSchema
16-
}
5+
const annotators = new Set<(schema: JSONSchema, parent: JSONSchema | null) => void>()
176

18-
// Handle cycles
19-
if (isAnnotated(schema)) {
20-
return schema
21-
}
22-
23-
// Add a reference to this schema's parent
7+
annotators.add(function annotateParent(schema, parent) {
248
Object.defineProperty(schema, Parent, {
259
enumerable: false,
2610
value: parent,
2711
writable: false,
2812
})
13+
})
2914

30-
// Arrays
31-
if (Array.isArray(schema)) {
32-
schema.forEach(child => annotate(child, dereferencedPaths, schema))
33-
}
15+
annotators.add(function annotateSchemas(schema) {
16+
Object.defineProperty(schema, IsSchema, {
17+
enumerable: false,
18+
value: isSchemaLike(schema),
19+
writable: false,
20+
})
21+
})
22+
23+
/**
24+
* Traverses over the schema, assigning to each
25+
* node metadata that will be used downstream.
26+
*/
27+
export function annotate(schema: JSONSchema): AnnotatedJSONSchema {
28+
function go(s: JSONSchema, parent: JSONSchema | null): void {
29+
if (!Array.isArray(s) && !isPlainObject(s)) {
30+
return
31+
}
32+
33+
// Handle cycles
34+
if (isAnnotated(s)) {
35+
return
36+
}
37+
38+
// Run annotators
39+
annotators.forEach(f => {
40+
f(s, parent)
41+
})
3442

35-
// Objects
36-
for (const key in schema) {
37-
annotate(schema[key], dereferencedPaths, schema)
43+
// Handle arrays
44+
if (Array.isArray(s)) {
45+
s.forEach(_ => go(_, s))
46+
}
47+
48+
// Handle objects
49+
for (const key in s) {
50+
go(s[key], s)
51+
}
3852
}
3953

54+
go(schema, null)
55+
4056
return schema as AnnotatedJSONSchema
4157
}

src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface Options {
3535
/**
3636
* Custom function to provide a type name for a given schema
3737
*/
38-
customName?: (schema: AnnotatedJSONSchema, keyNameFromDefinition: string | undefined) => string | undefined
38+
customName?: (schema: AnnotatedJSONSchema) => string | undefined
3939
/**
4040
* Root directory for resolving [`$ref`](https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html)s.
4141
*/
@@ -150,7 +150,7 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
150150
// Initial clone to avoid mutating the input
151151
const _schema = cloneDeep(schema)
152152

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

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

176-
const normalized = normalize(annotated, dereferencedPaths, name, _options)
176+
const normalized = normalize(annotated, name, _options)
177177
log('yellow', 'normalizer', time(), '✅ Result:', normalized)
178178

179179
const parsed = parse(normalized, _options)

src/normalizer.ts

Lines changed: 83 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import {JSONSchemaTypeName, AnnotatedJSONSchema, NormalizedJSONSchema, JSONSchema, Parent} from './types/JSONSchema'
2-
import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils'
1+
import {
2+
JSONSchemaTypeName,
3+
AnnotatedJSONSchema,
4+
NormalizedJSONSchema,
5+
JSONSchema,
6+
Parent,
7+
Ref,
8+
IsSchema,
9+
} from './types/JSONSchema'
10+
import {appendToDescription, escapeBlockComment, justName, toSafeString, traverse} from './utils'
311
import {Options} from './'
4-
import {DereferencedPaths} from './resolver'
512
import {isDeepStrictEqual} from 'util'
13+
import {typesOfSchema} from './typesOfSchema'
614

7-
type Rule = (
8-
schema: AnnotatedJSONSchema,
9-
fileName: string,
10-
options: Options,
11-
key: string | null,
12-
dereferencedPaths: DereferencedPaths,
13-
) => void
15+
type Rule = (schema: AnnotatedJSONSchema, fileName: string, options: Options, key: string | null) => void
1416
const rules = new Map<string, Rule>()
1517

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

6466
rules.set('Transform id to $id', (schema, fileName) => {
65-
if (!isSchemaLike(schema)) {
67+
if (!schema[IsSchema]) {
6668
return
6769
}
70+
6871
if (schema.id && schema.$id && schema.id !== schema.$id) {
6972
throw ReferenceError(
7073
`Schema must define either id or $id, not both. Given id=${schema.id}, $id=${schema.$id} in ${fileName}`,
@@ -76,32 +79,28 @@ rules.set('Transform id to $id', (schema, fileName) => {
7679
}
7780
})
7881

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

84-
// Top-level schema
85-
if (!schema.$id && !schema[Parent]) {
86-
schema.$id = toSafeString(justName(fileName))
87+
if (!schema[IsSchema]) {
8788
return
8889
}
8990

90-
// Sub-schemas with references
91-
if (!isArrayType(schema) && !isObjectType(schema)) {
91+
schema.$id = toSafeString(justName(fileName))
92+
})
93+
94+
rules.set('Add an $id to each referenced schema', schema => {
95+
if (schema.$id) {
9296
return
9397
}
9498

95-
// We'll infer from $id and title downstream
96-
// TODO: Normalize upstream
97-
const dereferencedName = dereferencedPaths.get(schema)
98-
if (!schema.$id && !schema.title && dereferencedName) {
99-
schema.$id = toSafeString(justName(dereferencedName))
99+
if (!schema[Ref]) {
100+
return
100101
}
101102

102-
if (dereferencedName) {
103-
dereferencedPaths.delete(schema)
104-
}
103+
schema.$id = toSafeString(justName(schema[Ref]))
105104
})
106105

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

220+
rules.set(
221+
"Add an $id to each $def that doesn't have one, if unreachableDefinitions is enabled",
222+
(schema, _, options) => {
223+
if (!options.unreachableDefinitions) {
224+
return
225+
}
226+
227+
if (schema.$id) {
228+
return
229+
}
230+
231+
const parent = schema[Parent]
232+
if (!parent) {
233+
return
234+
}
235+
236+
const grandparent = parent[Parent]
237+
if (!grandparent) {
238+
return
239+
}
240+
241+
if (Object.keys(grandparent).find(_ => grandparent[_] === parent) !== '$defs') {
242+
return
243+
}
244+
245+
schema.$id = toSafeString(Object.keys(parent).find(_ => parent[_] === schema)!)
246+
},
247+
)
248+
221249
rules.set('Transform const to singleton enum', schema => {
222250
if (schema.const !== undefined) {
223251
schema.enum = [schema.const]
@@ -231,12 +259,34 @@ rules.set('Add tsEnumNames to enum types', (schema, _, options) => {
231259
}
232260
})
233261

234-
export function normalize(
235-
rootSchema: AnnotatedJSONSchema,
236-
dereferencedPaths: DereferencedPaths,
237-
filename: string,
238-
options: Options,
239-
): NormalizedJSONSchema {
240-
rules.forEach(rule => traverse(rootSchema, (schema, key) => rule(schema, filename, options, key, dereferencedPaths)))
262+
rules.set('Add an $id to each named enum', schema => {
263+
if (!schema[IsSchema]) {
264+
return
265+
}
266+
267+
if (schema.$id) {
268+
return
269+
}
270+
271+
if (!typesOfSchema(schema).includes('NAMED_ENUM')) {
272+
return
273+
}
274+
275+
const parent = schema[Parent]
276+
const keyName = Object.keys(parent).find(_ => parent[_] === schema)
277+
278+
// Special case: generate nicer names for additionalProperties enums
279+
if (parent[IsSchema] && keyName === 'additionalProperties') {
280+
const grandparent = parent[Parent]
281+
const parentKeyName = Object.keys(grandparent).find(_ => grandparent[_] === parent)!
282+
schema.$id = toSafeString(parentKeyName) + toSafeString(keyName)
283+
return
284+
}
285+
286+
schema.$id = toSafeString(justName(keyName))
287+
})
288+
289+
export function normalize(rootSchema: AnnotatedJSONSchema, filename: string, options: Options): NormalizedJSONSchema {
290+
rules.forEach(rule => traverse(rootSchema, (schema, key) => rule(schema, filename, options, key)))
241291
return rootSchema as NormalizedJSONSchema
242292
}

0 commit comments

Comments
 (0)