@typespec/http-server-js
Version:
TypeSpec HTTP server code generator for JavaScript
312 lines (265 loc) • 8.68 kB
text/typescript
import {
isErrorType,
isVoidType,
LiteralType,
Model,
ModelProperty,
Scalar,
Type,
Union,
} from "@typespec/compiler";
import { $ } from "@typespec/compiler/typekit";
import { JsContext, Module } from "../../ctx.js";
import { isUnspeakable, parseCase } from "../../util/case.js";
import { KEYWORDS } from "../../util/keywords.js";
import { getFullyQualifiedTypeName } from "../../util/name.js";
import { module as dateTimeHelper } from "../../../generated-defs/helpers/datetime.js";
/**
* Generates a mock value for a TypeSpec Model.
* Handles arrays and records as special cases.
*
* @param type - The TypeSpec Model to mock
* @returns A JavaScript string representation of the mock data
* @throws Error if a property cannot be mocked
*/
function mockModel(ctx: JsContext, module: Module, type: Model): string {
const tk = $(ctx.program);
if (tk.array.is(type)) {
return mockArray(ctx, module, type);
}
if (tk.record.is(type)) {
return mockRecord(ctx, module, type);
}
const mock: string[][] = [];
const properties = tk.model.getProperties(type, { includeExtended: true });
// If no properties exist, return an empty object
if (properties.size === 0) {
return "{}";
}
for (const [name, prop] of properties) {
if (prop.optional || isUnspeakable(prop.name)) {
continue;
}
const propMock = mockType(ctx, module, prop.type);
if (!propMock) {
throw new Error(`Could not mock property ${name} of type ${prop.type.kind}`);
}
const propName = parseCase(name).camelCase;
mock.push([KEYWORDS.has(propName) ? `_${propName}` : propName, propMock]);
}
// If all properties were optional, return an empty object
if (mock.length === 0) {
return "{}";
}
return `{
${mock.map(([name, value]) => `${name}: ${value}`).join(",\n")}
}`;
}
/**
* Generates a mock array containing a single mocked element.
*
* @param type - The TypeSpec array Model to mock
* @returns A JavaScript string representation of the mock array
*/
function mockArray(ctx: JsContext, module: Module, type: Model): string {
const tk = $(ctx.program);
const elementType = tk.array.getElementType(type);
const mockedType = mockType(ctx, module, elementType);
// If we can't mock the element type, return an empty array
if (mockedType === undefined) {
return "[]";
}
return `[${mockedType}]`;
}
/**
* Generates a mock for a TypeSpec record type with a sample key.
*
* @param type - The TypeSpec record Model to mock
* @returns A JavaScript string representation of the mock record
*/
function mockRecord(ctx: JsContext, module: Module, type: Model): string {
const tk = $(ctx.program);
const elementType = tk.record.getElementType(type);
const mockedType = mockType(ctx, module, elementType);
if (mockedType === undefined) {
return "{}";
}
return `{
mockKey: ${mockedType},
}`;
}
/**
* Mocks a TypeSpec Model property by mocking its type.
*
* @param prop - The TypeSpec model property to mock
* @returns A JavaScript string representation of the mocked property or undefined if it cannot be mocked
*/
function mockModelProperty(
ctx: JsContext,
module: Module,
prop: ModelProperty,
): string | undefined {
return mockType(ctx, module, prop.type);
}
/**
* Generates a mock for a TypeSpec literal value.
*
* @param type - The TypeSpec literal type to mock
* @returns A JavaScript string representation of the literal value
*/
function mockLiteral(type: LiteralType): string {
return JSON.stringify(type.value);
}
/**
* Entry point for generating mock data for any TypeSpec type.
* Delegates to specific mock functions based on the type kind.
*
* @param type - The TypeSpec type to mock
* @returns A JavaScript string representation of the mock data, or undefined if the type cannot be mocked
*/
export function mockType(ctx: JsContext, module: Module, type: Type): string | undefined {
const tk = $(ctx.program);
if (tk.model.is(type)) {
return mockModel(ctx, module, type);
}
if (tk.literal.is(type)) {
return mockLiteral(type);
}
if (tk.modelProperty.is(type)) {
return mockModelProperty(ctx, module, type);
}
if (tk.scalar.is(type)) {
return mockScalar(ctx, module, type);
}
if (tk.union.is(type)) {
return mockUnion(ctx, module, type);
}
if (isVoidType(type)) {
return "void";
}
return undefined;
}
/**
* Generates a mock for a TypeSpec union by mocking the first non-error variant.
*
* @param union - The TypeSpec union to mock
* @returns A JavaScript string representation of a mock for one variant, or undefined if no suitable variant is found
*/
function mockUnion(ctx: JsContext, module: Module, union: Union): string | undefined {
for (const variant of union.variants.values()) {
if (isErrorType(variant.type)) {
continue;
}
return mockType(ctx, module, variant.type);
}
return undefined;
}
/**
* Generates appropriate mock values for TypeSpec scalar types.
* Handles various scalar types including primitives and specialized types like dates.
*
* @param scalar - The TypeSpec scalar to mock
* @returns A JavaScript string representation of a suitable mock value for the scalar type
*/
function mockScalar(ctx: JsContext, module: Module, scalar: Scalar): string | undefined {
const tk = $(ctx.program);
const dateTimeMode = ctx.options.datetime ?? "temporal-polyfill";
if (tk.scalar.isBoolean(scalar) || tk.scalar.extendsBoolean(scalar)) {
return JSON.stringify(true);
}
if (tk.scalar.isNumeric(scalar) || tk.scalar.extendsNumeric(scalar)) {
if (
tk.scalar.extendsSafeint(scalar) ||
tk.scalar.extendsInt32(scalar) ||
tk.scalar.extendsUint32(scalar) ||
tk.scalar.extendsFloat64(scalar)
) {
return "42";
} else if (tk.scalar.extendsInteger(scalar)) {
return "42n";
} else {
module.imports.push({ from: "decimal.js", binder: ["Decimal"] });
return "new Decimal(42)";
}
}
if (tk.scalar.isUtcDateTime(scalar)) {
if (dateTimeMode === "date-duration") {
return "new Date()";
} else {
if (dateTimeMode === "temporal-polyfill") {
module.imports.push({
from: "temporal-polyfill",
binder: ["Temporal"],
});
}
// Current instant
return "Temporal.Now.instant()";
}
}
if (
tk.scalar.isOffsetDateTime(scalar) ||
tk.scalar.isPlainDate(scalar) ||
tk.scalar.isPlainTime(scalar)
) {
if (dateTimeMode === "date-duration") {
return "new Date()";
} else {
if (dateTimeMode === "temporal-polyfill") {
module.imports.push({
from: "temporal-polyfill",
binder: ["Temporal"],
});
}
// Current instant
switch ((scalar as Scalar).name) {
case "offsetDateTime":
return "Temporal.Now.zonedDateTimeISO()";
case "plainDate":
return "Temporal.Now.zonedDateTimeISO().toPlainDate()";
case "plainTime":
return "Temporal.Now.zonedDateTimeISO().toPlainTime()";
default:
throw new Error("Unexpected scalar type: " + (scalar as Scalar).name);
}
}
}
if (getFullyQualifiedTypeName(scalar) === "TypeSpec.unixTimestamp32") {
return "Math.floor(new Date().getTime() / 1000)";
}
if (tk.scalar.isBytes(scalar) || tk.scalar.extendsBytes(scalar)) {
return "new Uint8Array()";
}
if (tk.scalar.isDuration(scalar) || tk.scalar.extendsDuration(scalar)) {
if (dateTimeMode === "date-duration") {
module.imports.push({
from: dateTimeHelper,
binder: ["Duration"],
});
return 'Duration.parseISO8601("P1Y2M3DT4H5M6S")';
} else {
if (dateTimeMode === "temporal-polyfill") {
module.imports.push({
from: "temporal-polyfill",
binder: ["Temporal"],
});
}
return `Temporal.Duration.from({ years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6 })`;
}
}
if (tk.scalar.isOffsetDateTime(scalar) || tk.scalar.extendsOffsetDateTime(scalar)) {
return JSON.stringify("2022-01-01T00:00:00Z");
}
if (tk.scalar.isPlainDate(scalar) || tk.scalar.extendsPlainDate(scalar)) {
return JSON.stringify("2022-01-01");
}
if (tk.scalar.isPlainTime(scalar) || tk.scalar.extendsPlainTime(scalar)) {
return JSON.stringify("00:00:00");
}
if (tk.scalar.isUrl(scalar) || tk.scalar.extendsUrl(scalar)) {
return JSON.stringify("https://example.com");
}
if (tk.scalar.isString(scalar) || tk.scalar.extendsString(scalar)) {
return JSON.stringify("mock-string");
}
return undefined;
}