Skip to content
This repository was archived by the owner on Jun 22, 2023. It is now read-only.

Commit e65ad1f

Browse files
authored
Merge pull request bcherny#452 from bcherny/436
Bugfix: Add support for $id in sub-schemas
2 parents b78a616 + 7aa353d commit e65ad1f

32 files changed

+314
-122
lines changed

src/generator.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -107,34 +107,30 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
107107
}
108108

109109
processed.add(ast)
110-
let type = ''
111110

112111
switch (ast.type) {
113112
case 'ARRAY':
114-
type = [
113+
return [
115114
declareNamedTypes(ast.params, options, rootASTName, processed),
116115
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined
117116
]
118117
.filter(Boolean)
119118
.join('\n')
120-
break
121119
case 'ENUM':
122-
type = ''
123-
break
120+
return ''
124121
case 'INTERFACE':
125-
type = getSuperTypesAndParams(ast)
122+
return getSuperTypesAndParams(ast)
126123
.map(
127124
ast =>
128125
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
129126
declareNamedTypes(ast, options, rootASTName, processed)
130127
)
131128
.filter(Boolean)
132129
.join('\n')
133-
break
134130
case 'INTERSECTION':
135131
case 'TUPLE':
136132
case 'UNION':
137-
type = [
133+
return [
138134
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined,
139135
ast.params
140136
.map(ast => declareNamedTypes(ast, options, rootASTName, processed))
@@ -146,14 +142,12 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
146142
]
147143
.filter(Boolean)
148144
.join('\n')
149-
break
150145
default:
151146
if (hasStandaloneName(ast)) {
152-
type = generateStandaloneType(ast, options)
147+
return generateStandaloneType(ast, options)
153148
}
149+
return ''
154150
}
155-
156-
return type
157151
}
158152

159153
function generateTypeUnmemoized(ast: AST, options: Options): string {

src/index.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -165,25 +165,13 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
165165
}
166166

167167
const normalized = normalize(linked, name, _options)
168-
if (process.env.VERBOSE) {
169-
if (isDeepStrictEqual(linked, normalized)) {
170-
log('yellow', 'normalizer', time(), '✅ No change')
171-
} else {
172-
log('yellow', 'normalizer', time(), '✅ Result:', normalized)
173-
}
174-
}
168+
log('yellow', 'normalizer', time(), '✅ Result:', normalized)
175169

176170
const parsed = parse(normalized, _options)
177171
log('blue', 'parser', time(), '✅ Result:', parsed)
178172

179173
const optimized = optimize(parsed, _options)
180-
if (process.env.VERBOSE) {
181-
if (isDeepStrictEqual(parsed, optimized)) {
182-
log('cyan', 'optimizer', time(), '✅ No change')
183-
} else {
184-
log('cyan', 'optimizer', time(), '✅ Result:', optimized)
185-
}
186-
}
174+
log('cyan', 'optimizer', time(), '✅ Result:', optimized)
187175

188176
const generated = generate(optimized, _options)
189177
log('magenta', 'generator', time(), '✅ Result:', generated)

src/normalizer.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema'
2-
import {appendToDescription, escapeBlockComment, justName, toSafeString, traverse} from './utils'
2+
import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils'
33
import {Options} from './'
44

55
type Rule = (schema: LinkedJSONSchema, fileName: string, options: Options) => void
@@ -50,10 +50,25 @@ rules.set('Default additionalProperties', (schema, _, options) => {
5050
}
5151
})
5252

53-
rules.set('Default top level `id`', (schema, fileName) => {
53+
rules.set('Transform id to $id', (schema, fileName) => {
54+
if (!isSchemaLike(schema)) {
55+
return
56+
}
57+
if (schema.id && schema.$id && schema.id !== schema.$id) {
58+
throw ReferenceError(
59+
`Schema must define either id or $id, not both. Given id=${schema.id}, $id=${schema.$id} in ${fileName}`
60+
)
61+
}
62+
if (schema.id) {
63+
schema.$id = schema.id
64+
delete schema.id
65+
}
66+
})
67+
68+
rules.set('Default top level $id', (schema, fileName) => {
5469
const isRoot = schema[Parent] === null
55-
if (isRoot && !schema.id) {
56-
schema.id = toSafeString(justName(fileName))
70+
if (isRoot && !schema.$id) {
71+
schema.$id = toSafeString(justName(fileName))
5772
}
5873
})
5974

src/parser.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export function parse(
5151
// so that it gets first pick for standalone name.
5252
const ast = parseAsTypeWithCache(
5353
{
54+
$id: schema.$id,
5455
allOf: [],
5556
description: schema.description,
56-
id: schema.id,
5757
title: schema.title
5858
},
5959
'ALL_OF',
@@ -247,7 +247,7 @@ function parseNonLiteral(
247247
keyName,
248248
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
249249
params: (schema.type as JSONSchema4TypeName[]).map(type => {
250-
const member: LinkedJSONSchema = {...omit(schema, 'description', 'id', 'title'), type}
250+
const member: LinkedJSONSchema = {...omit(schema, '$id', 'description', 'title'), type}
251251
return parse(maybeStripDefault(member as any), options, undefined, processed, usedNames)
252252
}),
253253
type: 'UNION'
@@ -300,7 +300,7 @@ function standaloneName(
300300
keyNameFromDefinition: string | undefined,
301301
usedNames: UsedNames
302302
): string | undefined {
303-
const name = schema.title || schema.id || keyNameFromDefinition
303+
const name = schema.title || schema.$id || keyNameFromDefinition
304304
if (name) {
305305
return generateName(name, usedNames)
306306
}

src/types/JSONSchema.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ export interface NormalizedJSONSchema extends LinkedJSONSchema {
8787
oneOf?: NormalizedJSONSchema[]
8888
not?: NormalizedJSONSchema
8989
required: string[]
90+
91+
// Removed by normalizer
92+
id: never
9093
}
9194

9295
export interface EnumJSONSchema extends NormalizedJSONSchema {
@@ -114,15 +117,13 @@ export interface CustomTypeJSONSchema extends NormalizedJSONSchema {
114117
tsType: string
115118
}
116119

117-
export const getRootSchema = memoize(
118-
(schema: LinkedJSONSchema): LinkedJSONSchema => {
119-
const parent = schema[Parent]
120-
if (!parent) {
121-
return schema
122-
}
123-
return getRootSchema(parent)
120+
export const getRootSchema = memoize((schema: LinkedJSONSchema): LinkedJSONSchema => {
121+
const parent = schema[Parent]
122+
if (!parent) {
123+
return schema
124124
}
125-
)
125+
return getRootSchema(parent)
126+
})
126127

127128
export function isPrimitive(schema: LinkedJSONSchema | JSONSchemaType): schema is JSONSchemaType {
128129
return !isPlainObject(schema)

src/typesOfSchema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const matchers: Record<SchemaType, (schema: JSONSchema) => boolean> = {
6565
return 'enum' in schema && 'tsEnumNames' in schema
6666
},
6767
NAMED_SCHEMA(schema) {
68-
return 'id' in schema && ('patternProperties' in schema || 'properties' in schema)
68+
return '$id' in schema && ('patternProperties' in schema || 'properties' in schema)
6969
},
7070
NULL(schema) {
7171
return schema.type === 'null'

src/utils.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {deburr, isPlainObject, trim, upperFirst} from 'lodash'
22
import {basename, dirname, extname, join, normalize, sep} from 'path'
3-
import {JSONSchema, LinkedJSONSchema} from './types/JSONSchema'
3+
import {JSONSchema, LinkedJSONSchema, Parent} from './types/JSONSchema'
44

55
// TODO: pull out into a separate package
66
export function Try<T>(fn: () => T, err: (e: Error) => any): T {
@@ -79,8 +79,6 @@ export function traverse(
7979
return
8080
}
8181

82-
// console.log('key', key + '\n')
83-
8482
processed.add(schema)
8583
callback(schema, key ?? null)
8684

@@ -328,19 +326,19 @@ export function maybeStripDefault(schema: LinkedJSONSchema): LinkedJSONSchema {
328326
}
329327

330328
/**
331-
* Removes the schema's `id`, `name`, and `description` properties
329+
* Removes the schema's `$id`, `name`, and `description` properties
332330
* if they exist.
333331
* Useful when parsing intersections.
334332
*
335333
* Mutates `schema`.
336334
*/
337335
export function maybeStripNameHints(schema: JSONSchema): JSONSchema {
336+
if ('$id' in schema) {
337+
delete schema.$id
338+
}
338339
if ('description' in schema) {
339340
delete schema.description
340341
}
341-
if ('id' in schema) {
342-
delete schema.id
343-
}
344342
if ('name' in schema) {
345343
delete schema.name
346344
}
@@ -353,3 +351,31 @@ export function appendToDescription(existingDescription: string | undefined, ...
353351
}
354352
return values.join('\n')
355353
}
354+
355+
export function isSchemaLike(schema: LinkedJSONSchema) {
356+
if (!isPlainObject(schema)) {
357+
return false
358+
}
359+
const parent = schema[Parent]
360+
if (parent === null) {
361+
return true
362+
}
363+
364+
const JSON_SCHEMA_KEYWORDS = [
365+
'allOf',
366+
'anyOf',
367+
'dependencies',
368+
'enum',
369+
'oneOf',
370+
'definitions',
371+
'not',
372+
'patternProperties',
373+
'properties',
374+
'required'
375+
]
376+
if (JSON_SCHEMA_KEYWORDS.some(_ => parent[_] === schema)) {
377+
return false
378+
}
379+
380+
return true
381+
}

test/__snapshots__/test/test.ts.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,7 @@ Generated by [AVA](https://avajs.dev).
697697
*/␊
698698
699699
export type LastName = string;␊
700+
export type Height = number;␊
700701
701702
export interface ExampleSchema {␊
702703
firstName: string;␊
@@ -705,7 +706,7 @@ Generated by [AVA](https://avajs.dev).
705706
* Age in years␊
706707
*/␊
707708
age?: number;␊
708-
height?: number;␊
709+
height?: Height;␊
709710
favoriteFoods?: unknown[];␊
710711
likesDogs?: boolean;␊
711712
[k: string]: unknown;␊

test/__snapshots__/test/test.ts.snap

13 Bytes
Binary file not shown.

test/e2e/basics.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const input = {
1515
minimum: 0
1616
},
1717
height: {
18+
$id: 'height',
1819
type: 'number'
1920
},
2021
favoriteFoods: {

test/normalizer/addEmptyRequiredProperty.json

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
{
22
"name": "Add empty `required` property if none is defined",
33
"in": {
4-
"id": "foo",
5-
"type": ["object"],
4+
"$id": "foo",
5+
"type": [
6+
"object"
7+
],
68
"properties": {
79
"a": {
810
"type": "integer",
9-
"id": "a"
11+
"$id": "a"
1012
}
1113
},
1214
"additionalProperties": true
1315
},
1416
"out": {
15-
"id": "foo",
17+
"$id": "foo",
1618
"type": "object",
1719
"properties": {
1820
"a": {
1921
"type": "integer",
20-
"id": "a"
22+
"$id": "a"
2123
}
2224
},
2325
"additionalProperties": true,

test/normalizer/constToEnum.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{
22
"name": "Normalize const to singleton enum",
33
"in": {
4-
"id": "foo",
4+
"$id": "foo",
55
"const": "foobar"
66
},
77
"out": {
8-
"id": "foo",
9-
"enum": ["foobar"]
8+
"$id": "foo",
9+
"enum": [
10+
"foobar"
11+
]
1012
}
1113
}

test/normalizer/defaultAdditionalProperties.2.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"name": "Default additionalProperties to false",
33
"in": {
4-
"id": "foo",
4+
"$id": "foo",
55
"type": [
66
"object"
77
],
88
"properties": {
99
"a": {
1010
"type": "integer",
11-
"id": "a"
11+
"$id": "a"
1212
},
1313
"b": {
1414
"type": "object"
@@ -20,12 +20,12 @@
2020
"additionalProperties": false
2121
},
2222
"out": {
23-
"id": "foo",
23+
"$id": "foo",
2424
"type": "object",
2525
"properties": {
2626
"a": {
2727
"type": "integer",
28-
"id": "a"
28+
"$id": "a"
2929
},
3030
"b": {
3131
"additionalProperties": false,

test/normalizer/defaultAdditionalProperties.json

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
{
22
"name": "Default additionalProperties to true",
33
"in": {
4-
"id": "foo",
5-
"type": ["object"],
4+
"$id": "foo",
5+
"type": [
6+
"object"
7+
],
68
"properties": {
79
"a": {
810
"type": "integer",
9-
"id": "a"
11+
"$id": "a"
1012
}
1113
},
1214
"required": []
1315
},
1416
"out": {
15-
"id": "foo",
17+
"$id": "foo",
1618
"type": "object",
1719
"properties": {
1820
"a": {
1921
"type": "integer",
20-
"id": "a"
22+
"$id": "a"
2123
}
2224
},
2325
"required": [],

0 commit comments

Comments
 (0)