Skip to content

Commit

Permalink
Fix cycles during generating (fix #376, fix #323)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boris Cherny committed Jun 27, 2022
1 parent e65ad1f commit 86f398d
Show file tree
Hide file tree
Showing 12 changed files with 1,492 additions and 1,448 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
},
"homepage": "https://github.com/bcherny/json-schema-to-typescript#readme",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "https://github.com/bcherny/json-schema-ref-parser.git#1979495",
"@types/json-schema": "^7.0.11",
"@types/lodash": "^4.14.182",
"@types/prettier": "^2.6.1",
Expand All @@ -54,7 +55,6 @@
"glob": "^8.0.3",
"glob-promise": "^4.2.2",
"is-glob": "^4.0.3",
"json-schema-ref-parser": "^9.0.9",
"lodash": "^4.17.21",
"minimist": "^1.2.6",
"mkdirp": "^1.0.4",
Expand Down Expand Up @@ -91,8 +91,7 @@
"ignoredByWatcher": [
"./src"
],
"snapshotDir": "./test/__snapshots__",
"vebose": true
"snapshotDir": "./test/__snapshots__"
},
"browserify": {
"transform": [
Expand Down
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {readFileSync} from 'fs'
import {JSONSchema4} from 'json-schema'
import {Options as $RefOptions} from 'json-schema-ref-parser'
import {Options as $RefOptions} from '@apidevtools/json-schema-ref-parser'
import {cloneDeep, endsWith, merge} from 'lodash'
import {dirname} from 'path'
import {Options as PrettierOptions} from 'prettier'
Expand Down 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
}
18 changes: 15 additions & 3 deletions src/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import $RefParser = require('json-schema-ref-parser')
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, $refOptions)
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
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

0 comments on commit 86f398d

Please sign in to comment.