@typespec/json-schema
Version:
TypeSpec library for emitting TypeSpec to JSON Schema and converting JSON Schema to TypeSpec
877 lines • 37.3 kB
JavaScript
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