Skip to content

Commit

Permalink
Merge pull request #452 from bcherny/436
Browse files Browse the repository at this point in the history
Bugfix: Add support for $id in sub-schemas
  • Loading branch information
bcherny authored May 22, 2022
2 parents b78a616 + 7aa353d commit e65ad1f
Show file tree
Hide file tree
Showing 32 changed files with 314 additions and 122 deletions.
18 changes: 6 additions & 12 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,34 +107,30 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
}

processed.add(ast)
let type = ''

switch (ast.type) {
case 'ARRAY':
type = [
return [
declareNamedTypes(ast.params, options, rootASTName, processed),
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined
]
.filter(Boolean)
.join('\n')
break
case 'ENUM':
type = ''
break
return ''
case 'INTERFACE':
type = getSuperTypesAndParams(ast)
return getSuperTypesAndParams(ast)
.map(
ast =>
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
declareNamedTypes(ast, options, rootASTName, processed)
)
.filter(Boolean)
.join('\n')
break
case 'INTERSECTION':
case 'TUPLE':
case 'UNION':
type = [
return [
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined,
ast.params
.map(ast => declareNamedTypes(ast, options, rootASTName, processed))
Expand All @@ -146,14 +142,12 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
]
.filter(Boolean)
.join('\n')
break
default:
if (hasStandaloneName(ast)) {
type = generateStandaloneType(ast, options)
return generateStandaloneType(ast, options)
}
return ''
}

return type
}

function generateTypeUnmemoized(ast: AST, options: Options): string {
Expand Down
16 changes: 2 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,25 +165,13 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
}

const normalized = normalize(linked, name, _options)
if (process.env.VERBOSE) {
if (isDeepStrictEqual(linked, normalized)) {
log('yellow', 'normalizer', time(), '✅ No change')
} else {
log('yellow', 'normalizer', time(), '✅ Result:', normalized)
}
}
log('yellow', 'normalizer', time(), '✅ Result:', normalized)

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

const optimized = optimize(parsed, _options)
if (process.env.VERBOSE) {
if (isDeepStrictEqual(parsed, optimized)) {
log('cyan', 'optimizer', time(), '✅ No change')
} else {
log('cyan', 'optimizer', time(), '✅ Result:', optimized)
}
}
log('cyan', 'optimizer', time(), '✅ Result:', optimized)

const generated = generate(optimized, _options)
log('magenta', 'generator', time(), '✅ Result:', generated)
Expand Down
23 changes: 19 additions & 4 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema'
import {appendToDescription, escapeBlockComment, justName, toSafeString, traverse} from './utils'
import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils'
import {Options} from './'

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

rules.set('Default top level `id`', (schema, fileName) => {
rules.set('Transform id to $id', (schema, fileName) => {
if (!isSchemaLike(schema)) {
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}`
)
}
if (schema.id) {
schema.$id = schema.id
delete schema.id
}
})

rules.set('Default top level $id', (schema, fileName) => {
const isRoot = schema[Parent] === null
if (isRoot && !schema.id) {
schema.id = toSafeString(justName(fileName))
if (isRoot && !schema.$id) {
schema.$id = toSafeString(justName(fileName))
}
})

Expand Down
6 changes: 3 additions & 3 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ export function parse(
// so that it gets first pick for standalone name.
const ast = parseAsTypeWithCache(
{
$id: schema.$id,
allOf: [],
description: schema.description,
id: schema.id,
title: schema.title
},
'ALL_OF',
Expand Down Expand Up @@ -247,7 +247,7 @@ function parseNonLiteral(
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
params: (schema.type as JSONSchema4TypeName[]).map(type => {
const member: LinkedJSONSchema = {...omit(schema, 'description', 'id', 'title'), type}
const member: LinkedJSONSchema = {...omit(schema, '$id', 'description', 'title'), type}
return parse(maybeStripDefault(member as any), options, undefined, processed, usedNames)
}),
type: 'UNION'
Expand Down Expand Up @@ -300,7 +300,7 @@ function standaloneName(
keyNameFromDefinition: string | undefined,
usedNames: UsedNames
): string | undefined {
const name = schema.title || schema.id || keyNameFromDefinition
const name = schema.title || schema.$id || keyNameFromDefinition
if (name) {
return generateName(name, usedNames)
}
Expand Down
17 changes: 9 additions & 8 deletions src/types/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export interface NormalizedJSONSchema extends LinkedJSONSchema {
oneOf?: NormalizedJSONSchema[]
not?: NormalizedJSONSchema
required: string[]

// Removed by normalizer
id: never
}

export interface EnumJSONSchema extends NormalizedJSONSchema {
Expand Down Expand Up @@ -114,15 +117,13 @@ export interface CustomTypeJSONSchema extends NormalizedJSONSchema {
tsType: string
}

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

export function isPrimitive(schema: LinkedJSONSchema | JSONSchemaType): schema is JSONSchemaType {
return !isPlainObject(schema)
Expand Down
2 changes: 1 addition & 1 deletion src/typesOfSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const matchers: Record<SchemaType, (schema: JSONSchema) => boolean> = {
return 'enum' in schema && 'tsEnumNames' in schema
},
NAMED_SCHEMA(schema) {
return 'id' in schema && ('patternProperties' in schema || 'properties' in schema)
return '$id' in schema && ('patternProperties' in schema || 'properties' in schema)
},
NULL(schema) {
return schema.type === 'null'
Expand Down
40 changes: 33 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {deburr, isPlainObject, trim, upperFirst} from 'lodash'
import {basename, dirname, extname, join, normalize, sep} from 'path'
import {JSONSchema, LinkedJSONSchema} from './types/JSONSchema'
import {JSONSchema, LinkedJSONSchema, Parent} from './types/JSONSchema'

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

// console.log('key', key + '\n')

processed.add(schema)
callback(schema, key ?? null)

Expand Down Expand Up @@ -328,19 +326,19 @@ export function maybeStripDefault(schema: LinkedJSONSchema): LinkedJSONSchema {
}

/**
* Removes the schema's `id`, `name`, and `description` properties
* Removes the schema's `$id`, `name`, and `description` properties
* if they exist.
* Useful when parsing intersections.
*
* Mutates `schema`.
*/
export function maybeStripNameHints(schema: JSONSchema): JSONSchema {
if ('$id' in schema) {
delete schema.$id
}
if ('description' in schema) {
delete schema.description
}
if ('id' in schema) {
delete schema.id
}
if ('name' in schema) {
delete schema.name
}
Expand All @@ -353,3 +351,31 @@ export function appendToDescription(existingDescription: string | undefined, ...
}
return values.join('\n')
}

export function isSchemaLike(schema: LinkedJSONSchema) {
if (!isPlainObject(schema)) {
return false
}
const parent = schema[Parent]
if (parent === null) {
return true
}

const JSON_SCHEMA_KEYWORDS = [
'allOf',
'anyOf',
'dependencies',
'enum',
'oneOf',
'definitions',
'not',
'patternProperties',
'properties',
'required'
]
if (JSON_SCHEMA_KEYWORDS.some(_ => parent[_] === schema)) {
return false
}

return true
}
3 changes: 2 additions & 1 deletion test/__snapshots__/test/test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ Generated by [AVA](https://avajs.dev).
*/␊
export type LastName = string;␊
export type Height = number;␊
export interface ExampleSchema {␊
firstName: string;␊
Expand All @@ -705,7 +706,7 @@ Generated by [AVA](https://avajs.dev).
* Age in years␊
*/␊
age?: number;␊
height?: number;␊
height?: Height;␊
favoriteFoods?: unknown[];␊
likesDogs?: boolean;␊
[k: string]: unknown;␊
Expand Down
Binary file modified test/__snapshots__/test/test.ts.snap
Binary file not shown.
1 change: 1 addition & 0 deletions test/e2e/basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const input = {
minimum: 0
},
height: {
$id: 'height',
type: 'number'
},
favoriteFoods: {
Expand Down
12 changes: 7 additions & 5 deletions test/normalizer/addEmptyRequiredProperty.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
{
"name": "Add empty `required` property if none is defined",
"in": {
"id": "foo",
"type": ["object"],
"$id": "foo",
"type": [
"object"
],
"properties": {
"a": {
"type": "integer",
"id": "a"
"$id": "a"
}
},
"additionalProperties": true
},
"out": {
"id": "foo",
"$id": "foo",
"type": "object",
"properties": {
"a": {
"type": "integer",
"id": "a"
"$id": "a"
}
},
"additionalProperties": true,
Expand Down
8 changes: 5 additions & 3 deletions test/normalizer/constToEnum.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "Normalize const to singleton enum",
"in": {
"id": "foo",
"$id": "foo",
"const": "foobar"
},
"out": {
"id": "foo",
"enum": ["foobar"]
"$id": "foo",
"enum": [
"foobar"
]
}
}
8 changes: 4 additions & 4 deletions test/normalizer/defaultAdditionalProperties.2.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "Default additionalProperties to false",
"in": {
"id": "foo",
"$id": "foo",
"type": [
"object"
],
"properties": {
"a": {
"type": "integer",
"id": "a"
"$id": "a"
},
"b": {
"type": "object"
Expand All @@ -20,12 +20,12 @@
"additionalProperties": false
},
"out": {
"id": "foo",
"$id": "foo",
"type": "object",
"properties": {
"a": {
"type": "integer",
"id": "a"
"$id": "a"
},
"b": {
"additionalProperties": false,
Expand Down
12 changes: 7 additions & 5 deletions test/normalizer/defaultAdditionalProperties.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
{
"name": "Default additionalProperties to true",
"in": {
"id": "foo",
"type": ["object"],
"$id": "foo",
"type": [
"object"
],
"properties": {
"a": {
"type": "integer",
"id": "a"
"$id": "a"
}
},
"required": []
},
"out": {
"id": "foo",
"$id": "foo",
"type": "object",
"properties": {
"a": {
"type": "integer",
"id": "a"
"$id": "a"
}
},
"required": [],
Expand Down
Loading

0 comments on commit e65ad1f

Please sign in to comment.