Skip to content

Commit

Permalink
Json Schema: Add support for simple defaults (#2657)
Browse files Browse the repository at this point in the history
fix [#2536](#2536)
  • Loading branch information
timotheeguerin authored Nov 15, 2023
1 parent 38aa320 commit 5fc1495
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/json-schema",
"comment": "Add support for simple literal default on model properties",
"type": "none"
}
],
"packageName": "@typespec/json-schema"
}
65 changes: 59 additions & 6 deletions packages/json-schema/src/json-schema-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BooleanLiteral,
compilerAssert,
emitFile,
Enum,
EnumMember,
Expand All @@ -19,6 +20,8 @@ import {
getRelativePathFromDirectory,
getSummary,
IntrinsicType,
isArrayModelType,
isNullType,
Model,
ModelProperty,
NumericLiteral,
Expand Down Expand Up @@ -64,7 +67,7 @@ import {
isJsonSchemaDeclaration,
JsonSchemaDeclaration,
} from "./index.js";
import { JSONSchemaEmitterOptions } from "./lib.js";
import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js";
export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSchemaEmitterOptions> {
#seenIds = new Set();
#typeForSourceFile = new Map<SourceFile<any>, JsonSchemaDeclaration>();
Expand Down Expand Up @@ -160,16 +163,66 @@ export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSche
}

modelPropertyLiteral(property: ModelProperty): EmitterOutput<object> {
const result = this.emitter.emitTypeReference(property.type);
const propertyType = this.emitter.emitTypeReference(property.type);

if (result.kind !== "code") {
if (propertyType.kind !== "code") {
throw new Error("Unexpected non-code result from emit reference");
}

const withConstraints = new ObjectBuilder(result.value);
this.#applyConstraints(property, withConstraints);
const result = new ObjectBuilder(propertyType.value);

return withConstraints;
if (property.default) {
result.default = this.#getDefaultValue(property.type, property.default);
}

this.#applyConstraints(property, result);

return result;
}

#getDefaultValue(type: Type, defaultType: Type): any {
const program = this.emitter.getProgram();

switch (defaultType.kind) {
case "String":
return defaultType.value;
case "Number":
return defaultType.value;
case "Boolean":
return defaultType.value;
case "Tuple":
compilerAssert(
type.kind === "Tuple" || (type.kind === "Model" && isArrayModelType(program, type)),
"setting tuple default to non-tuple value"
);

if (type.kind === "Tuple") {
return defaultType.values.map((defaultTupleValue, index) =>
this.#getDefaultValue(type.values[index], defaultTupleValue)
);
} else {
return defaultType.values.map((defaultTuplevalue) =>
this.#getDefaultValue(type.indexer!.value, defaultTuplevalue)
);
}

case "Intrinsic":
return isNullType(defaultType)
? null
: reportDiagnostic(program, {
code: "invalid-default",
format: { type: defaultType.kind },
target: defaultType,
});
case "EnumMember":
return defaultType.value ?? defaultType.name;
default:
reportDiagnostic(program, {
code: "invalid-default",
format: { type: defaultType.kind },
target: defaultType,
});
}
}

booleanLiteral(boolean: BooleanLiteral): EmitterOutput<object> {
Expand Down
11 changes: 9 additions & 2 deletions packages/json-schema/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createTypeSpecLibrary, JSONSchemaType } from "@typespec/compiler";
import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";

export type FileType = "yaml" | "json";
export type Int64Strategy = "string" | "number";
Expand Down Expand Up @@ -82,7 +82,14 @@ export const EmitterOptionsSchema: JSONSchemaType<JSONSchemaEmitterOptions> = {

export const libDef = {
name: "@typespec/json-schema",
diagnostics: {},
diagnostics: {
"invalid-default": {
severity: "error",
messages: {
default: paramMessage`Invalid type '${"type"}' for a default value`,
},
},
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType<JSONSchemaEmitterOptions>,
},
Expand Down
71 changes: 70 additions & 1 deletion packages/json-schema/test/models.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import assert from "assert";
import assert, { deepStrictEqual } from "assert";
import { emitSchema } from "./utils.js";

describe("emitting models", () => {
Expand Down Expand Up @@ -254,4 +254,73 @@ describe("emitting models", () => {
$ref: "RecordInt32.json",
});
});

describe("default values", () => {
it("specify default value on enum property", async () => {
const res = await emitSchema(
`
model Foo {
optionalEnum?: MyEnum = MyEnum.a;
};
enum MyEnum {
a: "a-value",
b,
}
`
);

deepStrictEqual(res["Foo.json"].properties.optionalEnum, {
$ref: "MyEnum.json",
default: "a-value",
});
});

it("specify default value on string property", async () => {
const res = await emitSchema(
`
model Foo {
optional?: string = "abc";
}
`
);

deepStrictEqual(res["Foo.json"].properties.optional, {
type: "string",
default: "abc",
});
});

it("specify default value on numeric property", async () => {
const res = await emitSchema(
`
model Foo {
optional?: int32 = 123;
}
`
);

deepStrictEqual(res["Foo.json"].properties.optional, {
type: "integer",
minimum: -2147483648,
maximum: 2147483647,
default: 123,
});
});

it("specify default value on boolean property", async () => {
const res = await emitSchema(
`
model Foo {
optional?: boolean = true;
}
`
);

deepStrictEqual(res["Foo.json"].properties.optional, {
type: "boolean",
default: true,
});
});
});
});

0 comments on commit 5fc1495

Please sign in to comment.