Skip to content
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

fix: generate same title one types #516

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ See [server demo](example) and [browser demo](https://github.com/bcherny/json-sc
| unreachableDefinitions | boolean | `false` | Generates code for `$defs` that aren't referenced by the schema. |
| strictIndexSignatures | boolean | `false` | Append all index signatures with `\| undefined` so that they are strictly typed. |
| $refOptions | object | `{}` | [$RefParser](https://github.com/BigstickCarpet/json-schema-ref-parser) Options, used when resolving `$ref`s |
| sameExplicitTitle | boolean | `false` | Generate same title one types. If two schemas are (a) identical and (b) have the same explicit title, then emit just one type to represent the schema |
## CLI

A CLI utility is provided with this package.
Expand Down
71 changes: 55 additions & 16 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,54 +29,79 @@ export function generate(ast: AST, options = DEFAULT_OPTIONS): string {
) // trailing newline
}

function declareEnums(ast: AST, options: Options, processed = new Set<AST>()): string {
function declareEnums(ast: AST, options: Options, processed = new Set<AST>(), usedNames = new Set<string>()): string {
if (processed.has(ast)) {
return ''
}

if (options.sameExplicitTitle && typeof ast.standaloneName !== 'undefined') {
if (usedNames.has(ast.standaloneName)) {
return ''
}

usedNames.add(ast.standaloneName)
}

processed.add(ast)
let type = ''

switch (ast.type) {
case 'ENUM':
return generateStandaloneEnum(ast, options) + '\n'
case 'ARRAY':
return declareEnums(ast.params, options, processed)
return declareEnums(ast.params, options, processed, usedNames)
case 'UNION':
case 'INTERSECTION':
return ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '')
return ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed, usedNames), '')
case 'TUPLE':
type = ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '')
type = ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed, usedNames), '')
if (ast.spreadParam) {
type += declareEnums(ast.spreadParam, options, processed)
type += declareEnums(ast.spreadParam, options, processed, usedNames)
}
return type
case 'INTERFACE':
return getSuperTypesAndParams(ast).reduce((prev, ast) => prev + declareEnums(ast, options, processed), '')
return getSuperTypesAndParams(ast).reduce(
(prev, ast) => prev + declareEnums(ast, options, processed, usedNames),
''
)
default:
return ''
}
}

function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string, processed = new Set<AST>()): string {
function declareNamedInterfaces(
ast: AST,
options: Options,
rootASTName: string,
processed = new Set<AST>(),
usedNames = new Set<string>()
): string {
if (processed.has(ast)) {
return ''
}

if (options.sameExplicitTitle && typeof ast.standaloneName !== 'undefined') {
if (usedNames.has(ast.standaloneName)) {
return ''
}

usedNames.add(ast.standaloneName)
}

processed.add(ast)
let type = ''

switch (ast.type) {
case 'ARRAY':
type = declareNamedInterfaces((ast as TArray).params, options, rootASTName, processed)
type = declareNamedInterfaces((ast as TArray).params, options, rootASTName, processed, usedNames)
break
case 'INTERFACE':
type = [
hasStandaloneName(ast) &&
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
generateStandaloneInterface(ast, options),
getSuperTypesAndParams(ast)
.map(ast => declareNamedInterfaces(ast, options, rootASTName, processed))
.map(ast => declareNamedInterfaces(ast, options, rootASTName, processed, usedNames))
.filter(Boolean)
.join('\n')
]
Expand All @@ -87,11 +112,11 @@ function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string,
case 'TUPLE':
case 'UNION':
type = ast.params
.map(_ => declareNamedInterfaces(_, options, rootASTName, processed))
.map(_ => declareNamedInterfaces(_, options, rootASTName, processed, usedNames))
.filter(Boolean)
.join('\n')
if (ast.type === 'TUPLE' && ast.spreadParam) {
type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed)
type += declareNamedInterfaces(ast.spreadParam, options, rootASTName, processed, usedNames)
}
break
default:
Expand All @@ -101,17 +126,31 @@ function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string,
return type
}

function declareNamedTypes(ast: AST, options: Options, rootASTName: string, processed = new Set<AST>()): string {
function declareNamedTypes(
ast: AST,
options: Options,
rootASTName: string,
processed = new Set<AST>(),
usedNames = new Set<string>()
): string {
if (processed.has(ast)) {
return ''
}

if (options.sameExplicitTitle && typeof ast.standaloneName !== 'undefined') {
if (usedNames.has(ast.standaloneName)) {
return ''
}

usedNames.add(ast.standaloneName)
}

processed.add(ast)

switch (ast.type) {
case 'ARRAY':
return [
declareNamedTypes(ast.params, options, rootASTName, processed),
declareNamedTypes(ast.params, options, rootASTName, processed, usedNames),
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined
]
.filter(Boolean)
Expand All @@ -123,7 +162,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
.map(
ast =>
(ast.standaloneName === rootASTName || options.declareExternallyReferenced) &&
declareNamedTypes(ast, options, rootASTName, processed)
declareNamedTypes(ast, options, rootASTName, processed, usedNames)
)
.filter(Boolean)
.join('\n')
Expand All @@ -133,11 +172,11 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
return [
hasStandaloneName(ast) ? generateStandaloneType(ast, options) : undefined,
ast.params
.map(ast => declareNamedTypes(ast, options, rootASTName, processed))
.map(ast => declareNamedTypes(ast, options, rootASTName, processed, usedNames))
.filter(Boolean)
.join('\n'),
'spreadParam' in ast && ast.spreadParam
? declareNamedTypes(ast.spreadParam, options, rootASTName, processed)
? declareNamedTypes(ast.spreadParam, options, rootASTName, processed, usedNames)
: undefined
]
.filter(Boolean)
Expand Down
15 changes: 11 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ export interface Options {
* Generate unknown type instead of any
*/
unknownAny: boolean
/**
* Generate same title one types
*
* If two schemas are (a) identical and (b) have the same explicit title, then emit just one type to represent the schema
*/
sameExplicitTitle: boolean
}

export const DEFAULT_OPTIONS: Options = {
Expand Down Expand Up @@ -104,10 +110,11 @@ export const DEFAULT_OPTIONS: Options = {
useTabs: false
},
unreachableDefinitions: false,
unknownAny: true
unknownAny: true,
sameExplicitTitle: false
}

export function compileFromFile (filename: string, options: Partial<Options> = DEFAULT_OPTIONS): Promise<string> {
export function compileFromFile(filename: string, options: Partial<Options> = DEFAULT_OPTIONS): Promise<string> {
const contents = Try(
() => readFileSync(filename),
() => {
Expand All @@ -123,13 +130,13 @@ export function compileFromFile (filename: string, options: Partial<Options> = D
return compile(schema, stripExtension(filename), {cwd: dirname(filename), ...options})
}

export async function compile (schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> {
export async function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> {
validateOptions(options)

const _options = merge({}, DEFAULT_OPTIONS, options)

const start = Date.now()
function time () {
function time() {
return `(${Date.now() - start}ms)`
}

Expand Down
45 changes: 23 additions & 22 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ function parseNonLiteral(
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: schema.allOf!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'INTERSECTION'
}
Expand All @@ -136,38 +136,38 @@ function parseNonLiteral(
...(options.unknownAny ? T_UNKNOWN : T_ANY),
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames)
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle)
}
case 'ANY_OF':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: schema.anyOf!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'UNION'
}
case 'BOOLEAN':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'BOOLEAN'
}
case 'CUSTOM_TYPE':
return {
comment: schema.description,
keyName,
params: schema.tsType!,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'CUSTOM_TYPE'
}
case 'NAMED_ENUM':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames)!,
standaloneName: standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames, options.sameExplicitTitle)!,
params: schema.enum!.map((_, n) => ({
ast: parseLiteral(_, undefined),
ast: parse(_, options, undefined, processed, usedNames),
keyName: schema.tsEnumNames![n]
})),
type: 'ENUM'
Expand All @@ -178,28 +178,28 @@ function parseNonLiteral(
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'NULL'
}
case 'NUMBER':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'NUMBER'
}
case 'OBJECT':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'OBJECT'
}
case 'ONE_OF':
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: schema.oneOf!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'UNION'
}
Expand All @@ -209,7 +209,7 @@ function parseNonLiteral(
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'STRING'
}
case 'TYPED_ARRAY':
Expand All @@ -222,7 +222,7 @@ function parseNonLiteral(
keyName,
maxItems,
minItems,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: schema.items.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'TUPLE'
}
Expand All @@ -236,7 +236,7 @@ function parseNonLiteral(
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: parse(schema.items!, options, undefined, processed, usedNames),
type: 'ARRAY'
}
Expand All @@ -245,7 +245,7 @@ function parseNonLiteral(
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: (schema.type as JSONSchema4TypeName[]).map(type => {
const member: LinkedJSONSchema = {...omit(schema, '$id', 'description', 'title'), type}
return parse(maybeStripDefault(member as any), options, undefined, processed, usedNames)
Expand All @@ -256,8 +256,8 @@ function parseNonLiteral(
return {
comment: schema.description,
keyName,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
params: schema.enum!.map(_ => parseLiteral(_, undefined)),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
params: schema.enum!.map(_ => parse(_, options, undefined, processed, usedNames)),
type: 'UNION'
}
case 'UNNAMED_SCHEMA':
Expand All @@ -277,7 +277,7 @@ function parseNonLiteral(
params: Array(Math.max(maxItems, minItems) || 0).fill(params),
// if there is no maximum, then add a spread item to collect the rest
spreadParam: maxItems >= 0 ? undefined : params,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'TUPLE'
}
}
Expand All @@ -286,7 +286,7 @@ function parseNonLiteral(
comment: schema.description,
keyName,
params,
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle),
type: 'ARRAY'
}
}
Expand All @@ -298,11 +298,12 @@ function parseNonLiteral(
function standaloneName(
schema: LinkedJSONSchema,
keyNameFromDefinition: string | undefined,
usedNames: UsedNames
usedNames: UsedNames,
sameExplicitTitle: boolean
): string | undefined {
const name = schema.title || schema.$id || keyNameFromDefinition
if (name) {
return generateName(name, usedNames)
return generateName(name, usedNames, sameExplicitTitle)
}
}

Expand All @@ -314,7 +315,7 @@ function newInterface(
keyName?: string,
keyNameFromDefinition?: string
): TInterface {
const name = standaloneName(schema, keyNameFromDefinition, usedNames)!
const name = standaloneName(schema, keyNameFromDefinition, usedNames, options.sameExplicitTitle)!
return {
comment: schema.description,
keyName,
Expand Down
Loading