UNPKG

@typespec/json-schema

Version:

TypeSpec library for emitting TypeSpec to JSON Schema and converting JSON Schema to TypeSpec

877 lines 37.3 kB
import { ArrayBuilder, Declaration, ObjectBuilder, Placeholder, TypeEmitter, setProperty, } from "@typespec/asset-emitter"; import { compilerAssert, emitFile, explainStringTemplateNotSerializable, getDeprecated, getDirectoryPath, getDiscriminator, getDoc, getExamples, getFormat, getMaxItems, getMaxLength, getMaxValue, getMaxValueExclusive, getMinItems, getMinLength, getMinValue, getMinValueExclusive, getPattern, getRelativePathFromDirectory, getSummary, isStringType, isType, joinPaths, serializeValueAsJson, } from "@typespec/compiler"; import { DuplicateTracker } from "@typespec/compiler/utils"; import { stringify } from "yaml"; import { findBaseUri, getContains, getContentEncoding, getContentMediaType, getContentSchema, getExtensions, getId, getMaxContains, getMaxProperties, getMinContains, getMinProperties, getMultipleOf, getPrefixItems, getUniqueItems, isJsonSchemaDeclaration, isOneOf, } from "./index.js"; import { reportDiagnostic } from "./lib.js"; import { includeDerivedModel } from "./utils.js"; /** @internal */ export class JsonSchemaEmitter extends TypeEmitter { #idDuplicateTracker = new DuplicateTracker(); #typeForSourceFile = new Map(); #applyModelIndexer(schema, model) { if (model.indexer) { setProperty(schema, "unevaluatedProperties", this.emitter.emitTypeReference(model.indexer.value)); return; } if (!this.emitter.getOptions()["seal-object-schemas"]) return; const derivedModels = model.derivedModels.filter(includeDerivedModel); if (!derivedModels.length) { setProperty(schema, "unevaluatedProperties", { not: {} }); } } modelDeclaration(model, name) { // Check if this should emit as a discriminated union const discriminator = getDiscriminator(this.emitter.getProgram(), model); const strategy = this.emitter.getOptions()["polymorphic-models-strategy"]; if ((strategy === "oneOf" || strategy === "anyOf") && discriminator && model.derivedModels.length > 0) { return this.#createDiscriminatedUnionDeclaration(model, name, discriminator, strategy); } // Check if base model is using discriminated union strategy // If so, don't use allOf (to avoid circular references), inline properties instead const shouldInlineBase = model.baseModel && this.#isBaseUsingDiscriminatedUnion(model.baseModel); const schema = this.#initializeSchema(model, name, { type: "object", properties: shouldInlineBase ? this.#getAllModelProperties(model) : this.emitter.emitModelProperties(model), required: shouldInlineBase ? this.#getAllRequiredModelProperties(model) : this.#requiredModelProperties(model), }); if (model.baseModel && !shouldInlineBase) { const allOf = new ArrayBuilder(); allOf.push(this.emitter.emitTypeReference(model.baseModel)); setProperty(schema, "allOf", allOf); } this.#applyModelIndexer(schema, model); this.#applyConstraints(model, schema); return this.#createDeclaration(model, name, schema); } #isBaseUsingDiscriminatedUnion(model) { const discriminator = getDiscriminator(this.emitter.getProgram(), model); const strategy = this.emitter.getOptions()["polymorphic-models-strategy"]; return ((strategy === "oneOf" || strategy === "anyOf") && !!discriminator && model.derivedModels.length > 0); } modelLiteral(model) { const schema = new ObjectBuilder({ type: "object", properties: this.emitter.emitModelProperties(model), required: this.#requiredModelProperties(model), }); this.#applyModelIndexer(schema, model); return schema; } modelInstantiation(model, name) { if (!name) { return this.modelLiteral(model); } return this.modelDeclaration(model, name); } arrayDeclaration(array, name, elementType) { const schema = this.#initializeSchema(array, name, { type: "array", items: this.emitter.emitTypeReference(elementType), }); this.#applyConstraints(array, schema); return this.#createDeclaration(array, name, schema); } arrayLiteral(array, elementType) { return new ObjectBuilder({ type: "array", items: this.emitter.emitTypeReference(elementType), }); } #requiredModelProperties(model) { const requiredProps = []; for (const prop of model.properties.values()) { if (!prop.optional) { requiredProps.push(prop.name); } } // Add discriminator property to required if model has @discriminator decorator and property is not already defined const discriminator = getDiscriminator(this.emitter.getProgram(), model); if (discriminator && !model.properties.has(discriminator.propertyName)) { requiredProps.push(discriminator.propertyName); } return requiredProps.length > 0 ? requiredProps : undefined; } // Get all required properties including inherited ones (for when base uses discriminated union) #getAllRequiredModelProperties(model) { const requiredProps = []; const visited = new Set(); const collectRequired = (m) => { if (visited.has(m)) return; visited.add(m); // Collect from base first if (m.baseModel) { collectRequired(m.baseModel); } // Then collect from this model for (const prop of m.properties.values()) { if (!prop.optional && !requiredProps.includes(prop.name)) { requiredProps.push(prop.name); } } // Add discriminator property if defined on this model const discriminator = getDiscriminator(this.emitter.getProgram(), m); if (discriminator && !m.properties.has(discriminator.propertyName) && !requiredProps.includes(discriminator.propertyName)) { requiredProps.push(discriminator.propertyName); } }; collectRequired(model); return requiredProps.length > 0 ? requiredProps : undefined; } // Get all properties including inherited ones (for when base uses discriminated union) #getAllModelProperties(model) { const props = new ObjectBuilder(); const visited = new Set(); const collectProperties = (m) => { if (visited.has(m)) return; visited.add(m); // Collect from base first if (m.baseModel) { collectProperties(m.baseModel); } // Then collect from this model (overriding base if needed) for (const [name, prop] of m.properties) { const result = this.emitter.emitModelProperty(prop); setProperty(props, name, result); } // Add discriminator property if defined on this model and not already added const discriminator = getDiscriminator(this.emitter.getProgram(), m); if (discriminator && !(discriminator.propertyName in props)) { setProperty(props, discriminator.propertyName, { type: "string", description: `Discriminator property for ${m.name}.`, }); } }; collectProperties(model); return props; } modelProperties(model) { const props = new ObjectBuilder(); for (const [name, prop] of model.properties) { const result = this.emitter.emitModelProperty(prop); setProperty(props, name, result); } // Add discriminator property if model has @discriminator decorator and property is not already defined const discriminator = getDiscriminator(this.emitter.getProgram(), model); if (discriminator && !(discriminator.propertyName in props)) { setProperty(props, discriminator.propertyName, { type: "string", description: `Discriminator property for ${model.name}.`, }); } return props; } modelPropertyLiteral(property) { const propertyType = this.emitter.emitTypeReference(property.type); compilerAssert(propertyType.kind === "code", "Unexpected non-code result from emit reference"); const result = new ObjectBuilder(propertyType.value); if (property.defaultValue) { result.default = this.#getDefaultValue(property, property.defaultValue); } if (result.anyOf && isOneOf(this.emitter.getProgram(), property)) { result.oneOf = result.anyOf; delete result.anyOf; } this.#applyConstraints(property, result); return result; } #getDefaultValue(modelProperty, defaultType) { return serializeValueAsJson(this.emitter.getProgram(), defaultType, modelProperty); } booleanLiteral(boolean) { return { type: "boolean", const: boolean.value }; } stringLiteral(string) { return { type: "string", const: string.value }; } stringTemplate(string) { if (string.stringValue !== undefined) { return { type: "string", const: string.stringValue }; } const diagnostics = explainStringTemplateNotSerializable(string); this.emitter .getProgram() .reportDiagnostics(diagnostics.map((x) => ({ ...x, severity: "warning" }))); return { type: "string" }; } numericLiteral(number) { return { type: "number", const: number.value }; } enumDeclaration(en, name) { const enumTypes = new Set(); const enumValues = new Set(); for (const member of en.members.values()) { // ???: why do we let emitters decide what the default type of an enum is enumTypes.add(typeof member.value === "number" ? "number" : "string"); enumValues.add(member.value ?? member.name); } const enumTypesArray = [...enumTypes]; const withConstraints = this.#initializeSchema(en, name, { type: enumTypesArray.length === 1 ? enumTypesArray[0] : enumTypesArray, enum: [...enumValues], }); this.#applyConstraints(en, withConstraints); return this.#createDeclaration(en, name, withConstraints); } enumMemberReference(member) { // would like to dispatch to the same `literal` codepaths but enum members aren't literal types switch (typeof member.value) { case "undefined": return { type: "string", const: member.name }; case "string": return { type: "string", const: member.value }; case "number": return { type: "number", const: member.value }; } } tupleLiteral(tuple) { return new ObjectBuilder({ type: "array", prefixItems: this.emitter.emitTupleLiteralValues(tuple), }); } tupleLiteralValues(tuple) { const values = new ArrayBuilder(); for (const value of tuple.values.values()) { values.push(this.emitter.emitType(value)); } return values; } unionInstantiation(union, name) { if (!name) { return this.unionLiteral(union); } return this.unionDeclaration(union, name); } unionDeclaration(union, name) { const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf"; const withConstraints = this.#initializeSchema(union, name, { [key]: this.emitter.emitUnionVariants(union), }); this.#applyConstraints(union, withConstraints); return this.#createDeclaration(union, name, withConstraints); } unionLiteral(union) { const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf"; return new ObjectBuilder({ [key]: this.emitter.emitUnionVariants(union), }); } unionVariants(union) { const variants = new ArrayBuilder(); for (const variant of union.variants.values()) { variants.push(this.emitter.emitType(variant)); } return variants; } unionVariant(variant) { const variantType = this.emitter.emitTypeReference(variant.type); compilerAssert(variantType.kind === "code", "Unexpected non-code result from emit reference"); const result = new ObjectBuilder(variantType.value); this.#applyConstraints(variant, result); return result; } modelPropertyReference(property) { // this is interesting - model property references will generally need to inherit // the relevant decorators from the property they are referencing. I wonder if this // could be made easier, as it's a bit subtle. const refSchema = this.emitter.emitTypeReference(property.type); compilerAssert(refSchema.kind === "code", "Unexpected non-code result from emit reference"); const schema = new ObjectBuilder(refSchema.value); this.#applyConstraints(property, schema); return schema; } reference(targetDeclaration, pathUp, pathDown, commonScope) { if (targetDeclaration.value instanceof Placeholder) { // I don't think this is possible, confirm. throw new Error("Can't form reference to declaration that hasn't been created yet"); } // these will be undefined when creating a self-reference const currentSfScope = pathUp[pathUp.length - 1]; const targetSfScope = pathDown[0]; if (targetSfScope && currentSfScope && !targetSfScope.sourceFile.meta.shouldEmit) { currentSfScope.sourceFile.meta.bundledRefs.push(targetDeclaration); } if (targetDeclaration.value.$id) { return { $ref: targetDeclaration.value.$id }; } if (!commonScope) { if (targetSfScope && !targetSfScope.sourceFile.meta.shouldEmit) { // referencing a schema which should not be emitted. In which case, it will be inlined // into the defs of the root schema which references this schema. return { $ref: "#/$defs/" + targetDeclaration.name }; } else { // referencing a schema that is in a different source file, but doesn't have an id. // nb: this may be dead code. // when either targetSfScope or currentSfScope are undefined, we have a common scope // (i.e. we are doing a self-reference) const resolved = getRelativePathFromDirectory(getDirectoryPath(currentSfScope.sourceFile.path), targetSfScope.sourceFile.path, false); return { $ref: resolved }; } } if (!currentSfScope && !targetSfScope) { // referencing ourself, and we don't have an id (otherwise we'd have // returned that above), so just return a ref. // This should be accurate because the only case when this can happen is if // the target declaration is not a root schema, and so it will be present in // the defs of some root schema, and there is only one level of defs. return { $ref: "#/$defs/" + targetDeclaration.name }; } throw new Error("JSON Pointer refs to arbitrary schemas is not supported"); } scalarInstantiation(scalar, name) { if (!name) { return this.#getSchemaForScalar(scalar); } return this.scalarDeclaration(scalar, name); } scalarInstantiationContext(scalar, name) { if (name === undefined) { return {}; } else { return this.#newFileScope(scalar); } } scalarDeclaration(scalar, name) { const isStd = this.#isStdType(scalar); const schema = this.#getSchemaForScalar(scalar); // Don't create a declaration for std types if (isStd) { return schema; } const builderSchema = this.#initializeSchema(scalar, name, schema); return this.#createDeclaration(scalar, name, builderSchema); } #getSchemaForScalar(scalar) { let result = {}; const isStd = this.#isStdType(scalar); if (isStd) { result = this.#getSchemaForStdScalars(scalar); } else if (scalar.baseScalar) { result = this.#getSchemaForScalar(scalar.baseScalar); } else { reportDiagnostic(this.emitter.getProgram(), { code: "unknown-scalar", format: { name: scalar.name }, target: scalar, }); return {}; } const objectBuilder = new ObjectBuilder(result); this.#applyConstraints(scalar, objectBuilder); if (isStd) { // Standard types are going to be inlined in the spec and we don't want the description of the scalar to show up delete objectBuilder.description; } return objectBuilder; } #getSchemaForStdScalars(baseBuiltIn) { switch (baseBuiltIn.name) { case "uint8": return { type: "integer", minimum: 0, maximum: 255 }; case "uint16": return { type: "integer", minimum: 0, maximum: 65535 }; case "uint32": return { type: "integer", minimum: 0, maximum: 4294967295 }; case "int8": return { type: "integer", minimum: -128, maximum: 127 }; case "int16": return { type: "integer", minimum: -32768, maximum: 32767 }; case "int32": case "unixTimestamp32": return { type: "integer", minimum: -2147483648, maximum: 2147483647 }; case "int64": const int64Strategy = this.emitter.getOptions()["int64-strategy"] ?? "string"; if (int64Strategy === "string") { return { type: "string" }; } else { // can't use minimum and maximum because we can't actually encode these values as literals // without losing precision. return { type: "integer" }; } case "uint64": const uint64Strategy = this.emitter.getOptions()["int64-strategy"] ?? "string"; if (uint64Strategy === "string") { return { type: "string" }; } else { // can't use minimum and maximum because we can't actually encode these values as literals // without losing precision. return { type: "integer" }; } case "decimal": case "decimal128": return { type: "string" }; case "integer": return { type: "integer" }; case "safeint": return { type: "integer" }; case "float": return { type: "number" }; case "float32": return { type: "number" }; case "float64": return { type: "number" }; case "numeric": return { type: "number" }; case "string": return { type: "string" }; case "boolean": return { type: "boolean" }; case "plainDate": return { type: "string", format: "date" }; case "plainTime": return { type: "string", format: "time" }; case "offsetDateTime": case "utcDateTime": return { type: "string", format: "date-time" }; case "duration": return { type: "string", format: "duration" }; case "url": return { type: "string", format: "uri" }; case "bytes": return { type: "string", contentEncoding: "base64" }; default: reportDiagnostic(this.emitter.getProgram(), { code: "unknown-scalar", format: { name: baseBuiltIn.name }, target: baseBuiltIn, }); return {}; } } #applySchemaExamples(type, target) { const program = this.emitter.getProgram(); const examples = getExamples(program, type); if (examples.length > 0) { setProperty(target, "examples", examples.map((x) => serializeValueAsJson(program, x.value, type))); } } #applyConstraints(type, schema) { const applyConstraint = (fn, key) => { const value = fn(this.emitter.getProgram(), type); if (value !== undefined) { schema[key] = value; } }; const applyTypeConstraint = (fn, key) => { const constraintType = fn(this.emitter.getProgram(), type); if (constraintType) { const ref = this.emitter.emitTypeReference(constraintType); compilerAssert(ref.kind === "code", "Unexpected non-code result from emit reference"); setProperty(schema, key, ref.value); } }; if (type.kind !== "UnionVariant") { this.#applySchemaExamples(type, schema); } applyConstraint(getMinLength, "minLength"); applyConstraint(getMaxLength, "maxLength"); applyConstraint(getMinValue, "minimum"); applyConstraint(getMinValueExclusive, "exclusiveMinimum"); applyConstraint(getMaxValue, "maximum"); applyConstraint(getMaxValueExclusive, "exclusiveMaximum"); applyConstraint(getPattern, "pattern"); applyConstraint(getMinItems, "minItems"); applyConstraint(getMaxItems, "maxItems"); // the stdlib applies a format of "url" but json schema wants "uri", // so ignore this format if it's the built-in type. if (!this.#isStdType(type) || type.name !== "url") { applyConstraint(getFormat, "format"); } applyConstraint(getMultipleOf, "multipleOf"); applyTypeConstraint(getContains, "contains"); applyConstraint(getMinContains, "minContains"); applyConstraint(getMaxContains, "maxContains"); applyConstraint(getUniqueItems, "uniqueItems"); applyConstraint(getMinProperties, "minProperties"); applyConstraint(getMaxProperties, "maxProperties"); applyConstraint(getContentEncoding, "contentEncoding"); applyConstraint(getContentMediaType, "contentMediaType"); applyTypeConstraint(getContentSchema, "contentSchema"); applyConstraint(getDoc, "description"); applyConstraint(getSummary, "title"); applyConstraint((p, t) => (getDeprecated(p, t) !== undefined ? true : undefined), "deprecated"); const prefixItems = getPrefixItems(this.emitter.getProgram(), type); if (prefixItems) { const prefixItemsSchema = new ArrayBuilder(); for (const item of prefixItems.values) { prefixItemsSchema.push(this.emitter.emitTypeReference(item)); } setProperty(schema, "prefixItems", prefixItemsSchema); } const extensions = getExtensions(this.emitter.getProgram(), type); for (const { key, value } of extensions) { if (this.#isTypeLike(value)) { setProperty(schema, key, this.emitter.emitTypeReference(value)); } else { setProperty(schema, key, value); } } } #isTypeLike(value) { return typeof value === "object" && value !== null && isType(value); } #createDeclaration(type, name, schema) { const decl = this.emitter.result.declaration(name, schema); const sf = decl.scope.sourceFile; sf.meta.shouldEmit = this.#shouldEmitRootSchema(type); return decl; } #initializeSchema(type, name, props) { const rootSchemaProps = this.#shouldEmitRootSchema(type) ? this.#getRootSchemaProps(type, name) : {}; return new ObjectBuilder({ ...rootSchemaProps, ...props, }); } #getRootSchemaProps(type, name) { return { $schema: "https://json-schema.org/draft/2020-12/schema", $id: this.#getDeclId(type, name), }; } #shouldEmitRootSchema(type) { return (this.emitter.getOptions().emitAllRefs || this.emitter.getOptions().emitAllModels || isJsonSchemaDeclaration(this.emitter.getProgram(), type)); } #isStdType(type) { return this.emitter.getProgram().checker.isStdType(type); } #createDiscriminatedUnionDeclaration(model, name, discriminator, strategy) { const variants = new ArrayBuilder(); const knownDiscriminatorValues = []; // Collect all derived models and their discriminator values for (const derived of model.derivedModels) { if (!includeDerivedModel(derived)) continue; // Add reference to each derived model const derivedRef = this.emitter.emitTypeReference(derived); variants.push(derivedRef); // Collect discriminator values for catch-all generation const values = this.#getDiscriminatorValues(derived, discriminator.propertyName); knownDiscriminatorValues.push(...values); } // Check if this is an open discriminator (discriminator property type includes string) // If so, add a catch-all variant that matches any unknown discriminator value if (this.#isOpenDiscriminator(model, discriminator.propertyName)) { const catchAllVariant = this.#createCatchAllVariant(model, discriminator.propertyName, knownDiscriminatorValues); variants.push(catchAllVariant); } // For discriminated unions, emit the oneOf/anyOf with base model properties // Since derived models inline properties (not using allOf), we can include // base properties here without creating circular references const schema = this.#initializeSchema(model, name, { type: "object", properties: this.emitter.emitModelProperties(model), required: this.#requiredModelProperties(model), [strategy]: variants, }); this.#applyConstraints(model, schema); return this.#createDeclaration(model, name, schema); } /** * Check if the discriminator property type is "open" (includes the string scalar type). * This means the discriminated union can accept unknown discriminator values. */ #isOpenDiscriminator(model, discriminatorPropertyName) { const prop = model.properties.get(discriminatorPropertyName); if (!prop) return false; return this.#typeIncludesString(prop.type); } /** * Check if a type includes the string scalar (not just string literals). */ #typeIncludesString(type) { const program = this.emitter.getProgram(); switch (type.kind) { case "Scalar": return isStringType(program, type); case "Union": // Check if any variant is the string scalar for (const variant of type.variants.values()) { if (this.#typeIncludesString(variant.type)) { return true; } } return false; default: return false; } } /** * Get string discriminator values from a model's discriminator property. */ #getDiscriminatorValues(model, discriminatorPropertyName) { const prop = model.properties.get(discriminatorPropertyName); if (!prop) return []; return this.#getStringLiteralValues(prop.type); } /** * Extract string literal values from a type. */ #getStringLiteralValues(type) { switch (type.kind) { case "String": return [type.value]; case "Union": return [...type.variants.values()].flatMap((v) => this.#getStringLiteralValues(v.type)); case "UnionVariant": return this.#getStringLiteralValues(type.type); case "EnumMember": return typeof type.value !== "number" ? [type.value ?? type.name] : []; default: return []; } } /** * Create a catch-all variant for open discriminated unions. * This variant matches any discriminator value not in the known values. */ #createCatchAllVariant(model, discriminatorPropertyName, knownValues) { const properties = new ObjectBuilder(); // Emit all base model properties for (const [propName, prop] of model.properties) { if (propName === discriminatorPropertyName) { // Override discriminator property to match any string NOT in known values setProperty(properties, propName, { type: "string", not: { enum: knownValues }, }); } else { const result = this.emitter.emitModelProperty(prop); setProperty(properties, propName, result); } } // If discriminator property is not explicitly defined, add it if (!model.properties.has(discriminatorPropertyName)) { setProperty(properties, discriminatorPropertyName, { type: "string", not: { enum: knownValues }, }); } const required = this.#requiredModelProperties(model); return { type: "object", properties: properties, ...(required && { required }), }; } intrinsic(intrinsic, name) { switch (intrinsic.name) { case "null": return { type: "null" }; case "unknown": return {}; case "never": case "void": return { not: {} }; case "ErrorType": return {}; default: const _assertNever = intrinsic.name; compilerAssert(false, "Unreachable"); } } #reportDuplicateIds() { for (const [id, targets] of this.#idDuplicateTracker.entries()) { for (const target of targets) { reportDiagnostic(this.emitter.getProgram(), { code: "duplicate-id", format: { id }, target: target, }); } } } async writeOutput(sourceFiles) { if (this.emitter.getProgram().compilerOptions.dryRun) { return; } this.#reportDuplicateIds(); const toEmit = []; const bundleId = this.emitter.getOptions().bundleId; if (bundleId) { const content = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: bundleId, $defs: {}, }; for (const sf of sourceFiles) { if (sf.meta.shouldEmit) { content.$defs[sf.globalScope.declarations[0].name] = this.#finalizeSourceFileContent(sf); } } await emitFile(this.emitter.getProgram(), { path: joinPaths(this.emitter.getOptions().emitterOutputDir, bundleId), content: this.#serializeSourceFileContent(content), }); } else { for (const sf of sourceFiles) { const emittedSf = await this.emitter.emitSourceFile(sf); // emitSourceFile will assert if somehow we have more than one declaration here if (sf.meta.shouldEmit) { toEmit.push(emittedSf); } } for (const emittedSf of toEmit) { await emitFile(this.emitter.getProgram(), { path: emittedSf.path, content: emittedSf.contents, }); } } } sourceFile(sourceFile) { const content = this.#finalizeSourceFileContent(sourceFile); return { contents: this.#serializeSourceFileContent(content), path: sourceFile.path, }; } #finalizeSourceFileContent(sourceFile) { const decls = sourceFile.globalScope.declarations; compilerAssert(decls.length === 1, "Multiple decls in single schema per file mode"); const content = { ...decls[0].value }; const bundledDecls = new Set(); if (sourceFile.meta.bundledRefs.length > 0) { // bundle any refs, including refs of refs content.$defs = {}; const refsToBundle = [...sourceFile.meta.bundledRefs]; while (refsToBundle.length > 0) { const decl = refsToBundle.shift(); if (bundledDecls.has(decl)) { // already $def'd, no need to def it again. continue; } bundledDecls.add(decl); content.$defs[decl.name] = decl.value; // all scopes are source file scopes in this emitter const refSf = decl.scope.sourceFile; refsToBundle.push(...refSf.meta.bundledRefs); } } return content; } #serializeSourceFileContent(content) { if (this.emitter.getOptions()["file-type"] === "json") { return JSON.stringify(content, null, 4); } else { return stringify(content, { aliasDuplicateObjects: false, lineWidth: 0, }); } } #getCurrentSourceFile() { let scope = this.emitter.getContext().scope; compilerAssert(scope, "Scope should exists"); while (scope && scope.kind !== "sourceFile") { scope = scope.parentScope; } compilerAssert(scope, "Top level scope should be a source file"); return scope.sourceFile; } #getDeclId(type, name) { const baseUri = findBaseUri(this.emitter.getProgram(), type); const explicitId = getId(this.emitter.getProgram(), type); if (explicitId) { return this.#trackId(idWithBaseURI(explicitId, baseUri), type); } // generate the ID based on the file path const base = this.emitter.getOptions().emitterOutputDir; const file = this.#getCurrentSourceFile().path; const relative = getRelativePathFromDirectory(base, file, false); if (baseUri) { return this.#trackId(new URL(relative, baseUri).href, type); } else { return this.#trackId(relative, type); } function idWithBaseURI(id, baseUri) { if (baseUri) { return new URL(id, baseUri).href; } else { return id; } } } #trackId(id, target) { this.#idDuplicateTracker.track(id, target); return id; } // #region context emitters modelDeclarationContext(model, name) { if (this.#isStdType(model) && model.name === "object") { return {}; } return this.#newFileScope(model); } modelInstantiationContext(model, name) { if (name === undefined) { return { scope: this.emitter.createScope({}, "", this.emitter.getContext().scope) }; } else { return this.#newFileScope(model); } } arrayDeclarationContext(array) { return this.#newFileScope(array); } enumDeclarationContext(en) { return this.#newFileScope(en); } unionDeclarationContext(union) { return this.#newFileScope(union); } scalarDeclarationContext(scalar) { if (this.#isStdType(scalar)) { return {}; } else { return this.#newFileScope(scalar); } } #newFileScope(type) { const sourceFile = this.emitter.createSourceFile(`${this.declarationName(type)}.${this.#fileExtension()}`); sourceFile.meta.shouldEmit = true; sourceFile.meta.bundledRefs = []; this.#typeForSourceFile.set(sourceFile, type); return { scope: sourceFile.globalScope, }; } #fileExtension() { return this.emitter.getOptions()["file-type"] === "json" ? "json" : "yaml"; } } //# sourceMappingURL=json-schema-emitter.js.map