diff --git a/packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts b/packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts index 227a46da75..a19a885bd0 100644 --- a/packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts +++ b/packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts @@ -3,7 +3,11 @@ import { iterableFirst, mapFirst } from "collection-utils"; import { addDescriptionToSchema } from "../../attributes/Description.js"; import { ConvenienceRenderer } from "../../ConvenienceRenderer.js"; import type { Name, Namer } from "../../Naming.js"; -import { defined, panic } from "../../support/Support.js"; +import type { RenderContext } from "../../Renderer.js"; +import type { OptionValues } from "../../RendererOptions/index.js"; +import type { Sourcelike } from "../../Source.js"; +import { assert, defined, panic } from "../../support/Support.js"; +import type { TargetLanguage } from "../../TargetLanguage.js"; import { type EnumType, type ObjectType, @@ -13,6 +17,7 @@ import { } from "../../Type/index.js"; import { matchTypeExhaustive } from "../../Type/TypeUtils.js"; +import type { jsonSchemaOptions } from "./language.js"; import { namingFunction } from "./utils.js"; interface Schema { @@ -21,6 +26,16 @@ interface Schema { } export class JSONSchemaRenderer extends ConvenienceRenderer { + private _currentFilename: string | undefined; + + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + private readonly _options: OptionValues, + ) { + super(targetLanguage, renderContext); + } + protected makeNamedTypeNamer(): Namer { return namingFunction; } @@ -177,31 +192,83 @@ export class JSONSchemaRenderer extends ConvenienceRenderer { } protected emitSourceStructure(): void { - // FIXME: Find a good way to do multiple top-levels. Maybe multiple files? - const topLevelType = - this.topLevels.size === 1 - ? this.schemaForType(defined(mapFirst(this.topLevels))) - : {}; - const schema: Schema = { - $schema: "http://json-schema.org/draft-06/schema#", - ...topLevelType, - }; const definitions: { [name: string]: Schema } = {}; this.forEachObject("none", (o: ObjectType, name: Name) => { const title = defined(this.names.get(name)); - definitions[title] = this.definitionForObject(o, title); + if (this._options.multiFileOutput === true) { + this.outputFile(title, { + [title]: this.definitionForObject(o, title), + }); + } else { + definitions[title] = this.definitionForObject(o, title); + } }); this.forEachUnion("none", (u, name) => { if (!this.unionNeedsName(u)) return; const title = defined(this.names.get(name)); - definitions[title] = this.definitionForUnion(u, title); + if (this._options.multiFileOutput === true) { + this.outputFile(title, { + [title]: this.definitionForUnion(u, title), + }); + } else { + definitions[title] = this.definitionForUnion(u, title); + } }); this.forEachEnum("none", (e, name) => { const title = defined(this.names.get(name)); - definitions[title] = this.definitionForEnum(e, title); + if (this._options.multiFileOutput === true) { + this.outputFile(title, { + [title]: this.definitionForEnum(e, title), + }); + } else { + definitions[title] = this.definitionForEnum(e, title); + } }); - schema.definitions = definitions; + if (this._options.multiFileOutput === false) { + this.outputFile("stdout", definitions); + } + } + private outputFile( + title: string, + definitions: { [name: string]: Schema }, + ): void { + // FIXME: Find a good way to do multiple top-levels. Maybe multiple files? + const topLevelType = + this.topLevels.size === 1 + ? this.schemaForType(defined(mapFirst(this.topLevels))) + : {}; + const schema: Schema = { + $schema: "http://json-schema.org/draft-06/schema#", + ...topLevelType, + }; + schema.definitions = definitions; + this.startFile(title); this.emitMultiline(JSON.stringify(schema, undefined, " ")); + this.endFile(); + } + + /// startFile takes a file name, lowercases it, appends ".schema" to it, and sets it as the current filename. + protected startFile(basename: Sourcelike): void { + if (this._options.multiFileOutput === false) { + return; + } + + assert( + this._currentFilename === undefined, + `Previous file wasn't finished: ${this._currentFilename}`, + ); + this._currentFilename = `${this.sourcelikeToString(basename)}.schema`; + this.initializeEmitContextForFilename(this._currentFilename); + } + + /// endFile pushes the current file name onto the collection of finished files and then resets the current file name. These finished files are used in index.ts to write the output. + protected endFile(): void { + if (this._options.multiFileOutput === false) { + return; + } + + this.finishFile(defined(this._currentFilename)); + this._currentFilename = undefined; } } diff --git a/packages/quicktype-core/src/language/JSONSchema/language.ts b/packages/quicktype-core/src/language/JSONSchema/language.ts index f0488ae8fc..7fcb3ed963 100644 --- a/packages/quicktype-core/src/language/JSONSchema/language.ts +++ b/packages/quicktype-core/src/language/JSONSchema/language.ts @@ -1,4 +1,5 @@ import type { RenderContext } from "../../Renderer.js"; +import { BooleanOption, getOptionValues } from "../../RendererOptions/index.js"; import type { IntegerRange } from "../../support/IntegerRange.js"; import { TargetLanguage } from "../../TargetLanguage.js"; import { @@ -9,6 +10,14 @@ import type { LanguageName, RendererOptions } from "../../types.js"; import { JSONSchemaRenderer } from "./JSONSchemaRenderer.js"; +export const jsonSchemaOptions = { + multiFileOutput: new BooleanOption( + "multi-file-output", + "Renders each top-level object in its own JSON schema file", + false, + ), +}; + export const JSONSchemaLanguageConfig = { displayName: "JSON Schema", names: ["schema", "json-schema"], @@ -27,8 +36,8 @@ export class JSONSchemaTargetLanguage extends TargetLanguage< super(JSONSchemaLanguageConfig); } - public getOptions(): Record { - return {}; + public getOptions(): typeof jsonSchemaOptions { + return jsonSchemaOptions; } public get stringTypeMapping(): StringTypeMapping { @@ -45,8 +54,12 @@ export class JSONSchemaTargetLanguage extends TargetLanguage< protected makeRenderer( renderContext: RenderContext, - _untypedOptionValues: RendererOptions, + untypedOptionValues: RendererOptions, ): JSONSchemaRenderer { - return new JSONSchemaRenderer(this, renderContext); + return new JSONSchemaRenderer( + this, + renderContext, + getOptionValues(jsonSchemaOptions, untypedOptionValues), + ); } }