Skip to content

Fix cycles during emission (fix #376, fix #323) #453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"homepage": "https://github.com/bcherny/json-schema-to-typescript#readme",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.9",
"@apidevtools/json-schema-ref-parser": "https://github.com/bcherny/json-schema-ref-parser.git#984282d3",
"@types/json-schema": "^7.0.11",
"@types/lodash": "^4.14.182",
"@types/prettier": "^2.6.1",
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,16 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
// Initial clone to avoid mutating the input
const _schema = cloneDeep(schema)

const dereferenced = await dereference(_schema, _options)
const {dereferencedPaths, dereferencedSchema} = await dereference(_schema, _options)
if (process.env.VERBOSE) {
if (isDeepStrictEqual(_schema, dereferenced)) {
if (isDeepStrictEqual(_schema, dereferencedSchema)) {
log('green', 'dereferencer', time(), '✅ No change')
} else {
log('green', 'dereferencer', time(), '✅ Result:', dereferenced)
log('green', 'dereferencer', time(), '✅ Result:', dereferencedSchema)
}
}

const linked = link(dereferenced)
const linked = link(dereferencedSchema)
if (process.env.VERBOSE) {
log('green', 'linker', time(), '✅ No change')
}
Expand All @@ -164,7 +164,7 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
log('green', 'validator', time(), '✅ No change')
}

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

const parsed = parse(normalized, _options)
Expand Down
47 changes: 40 additions & 7 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema'
import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils'
import {Options} from './'

type Rule = (schema: LinkedJSONSchema, fileName: string, options: Options) => void
import {DereferencedPaths} from './resolver'

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

function hasType(schema: LinkedJSONSchema, type: JSONSchemaTypeName) {
Expand Down Expand Up @@ -65,10 +72,31 @@ rules.set('Transform id to $id', (schema, fileName) => {
}
})

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

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

// Sub-schemas with references
if (!isArrayType(schema) && !isObjectType(schema)) {
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 (dereferencedName) {
dereferencedPaths.delete(schema)
}
})

Expand Down Expand Up @@ -188,7 +216,12 @@ rules.set('Transform const to singleton enum', schema => {
}
})

export function normalize(rootSchema: LinkedJSONSchema, filename: string, options: Options): NormalizedJSONSchema {
rules.forEach(rule => traverse(rootSchema, schema => rule(schema, filename, options)))
export function normalize(
rootSchema: LinkedJSONSchema,
dereferencedPaths: DereferencedPaths,
filename: string,
options: Options
): NormalizedJSONSchema {
rules.forEach(rule => traverse(rootSchema, (schema, key) => rule(schema, filename, options, key, dereferencedPaths)))
return rootSchema as NormalizedJSONSchema
}
16 changes: 14 additions & 2 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@ import $RefParser = require('@apidevtools/json-schema-ref-parser')
import {JSONSchema} from './types/JSONSchema'
import {log} from './utils'

export type DereferencedPaths = WeakMap<$RefParser.JSONSchemaObject, string>

export async function dereference(
schema: JSONSchema,
{cwd, $refOptions}: {cwd: string; $refOptions: $RefParser.Options}
): Promise<JSONSchema> {
): Promise<{dereferencedPaths: DereferencedPaths; dereferencedSchema: JSONSchema}> {
log('green', 'dereferencer', 'Dereferencing input schema:', cwd, schema)
const parser = new $RefParser()
return parser.dereference(cwd, schema as any, $refOptions) as any // TODO: fix types
const dereferencedPaths: DereferencedPaths = new WeakMap()
const dereferencedSchema = await parser.dereference(cwd, schema as any, {
...$refOptions,
dereference: {
...$refOptions.dereference,
onDereference($ref, schema) {
dereferencedPaths.set(schema, $ref)
}
}
}) as any // TODO: fix types
return {dereferencedPaths, dereferencedSchema}
}
1 change: 1 addition & 0 deletions src/typesOfSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const matchers: Record<SchemaType, (schema: JSONSchema) => boolean> = {
return 'enum' in schema && 'tsEnumNames' in schema
},
NAMED_SCHEMA(schema) {
// 8.2.1. The presence of "$id" in a subschema indicates that the subschema constitutes a distinct schema resource within a single schema document.
return '$id' in schema && ('patternProperties' in schema || 'properties' in schema)
},
NULL(schema) {
Expand Down
Loading