UNPKG

@typespec/http-server-js

Version:

TypeSpec HTTP server code generator for JavaScript

589 lines 23.2 kB
// Copyright (c) Microsoft Corporation // Licensed under the MIT license. import { getDiscriminator, getMaxValue, getMinValue, isArrayModelType, isNeverType, isUnknownType, } from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { getJsScalar } from "../common/scalar.js"; import { reportDiagnostic } from "../lib.js"; import { isUnspeakable, parseCase } from "./case.js"; import { UnimplementedError, UnreachableError } from "./error.js"; import { getAllProperties } from "./extends.js"; import { categorize, indent } from "./iter.js"; /** * Determines if `t` is a precise type. * @param t - the type to test * @returns true if `t` is precise, false otherwise. */ export function isPreciseType(t) { return (t.kind === "Scalar" || t.kind === "Model" || t.kind === "Boolean" || t.kind === "Number" || t.kind === "String" || (t.kind === "Intrinsic" && (t.name === "void" || t.name === "null"))); } const SUBJECT = { kind: "subject" }; function isLiteralValueType(type) { return (type.kind === "Boolean" || type.kind === "Number" || type.kind === "String" || type.kind === "EnumMember"); } const PROPERTY_ID = (prop) => parseCase(prop.name).camelCase; /** * Differentiates the variants of a union type. This function returns a CodeTree that will test an input "subject" and * determine which of the cases it matches. * * Compared to `differentiateTypes`, this function is specialized for union types, and will consider union * discriminators first, then delegate to `differentiateTypes` for the remaining cases. * * @param ctx * @param type */ export function differentiateUnion(ctx, module, union, renderPropertyName = PROPERTY_ID) { const discriminator = getDiscriminator(ctx.program, union)?.propertyName; // Exclude `never` from the union variants. const variants = [...union.variants.values()].filter((v) => !isNeverType(v.type)); if (variants.some((v) => isUnknownType(v.type))) { // Collapse the whole union to `unknown`. return { kind: "result", type: $(ctx.program).intrinsic.any }; } if (!discriminator) { const cases = new Set(); for (const variant of variants) { if (!isPreciseType(variant.type)) { reportDiagnostic(ctx.program, { code: "undifferentiable-union-variant", target: variant, }); } else { cases.add(variant.type); } } return differentiateTypes(ctx, module, cases, renderPropertyName); } else { const property = variants[0].type.properties.get(discriminator); return { kind: "switch", condition: { kind: "model-property", property, }, cases: variants.map((v) => { const discriminatorPropertyType = v.type.properties.get(discriminator).type; return { value: { kind: "literal", value: getJsValue(ctx, discriminatorPropertyType) }, body: { kind: "result", type: v.type }, }; }), default: { kind: "verbatim", body: [ 'throw new Error("Unreachable: discriminator did not match any known value or was not present.");', ], }, }; } } /** * Differentiates a set of input types. This function returns a CodeTree that will test an input "subject" and determine * which of the cases it matches, executing the corresponding code block. * * @param ctx - The emitter context. * @param cases - A map of cases to differentiate to their respective code blocks. * @returns a CodeTree to use with `writeCodeTree` */ export function differentiateTypes(ctx, module, cases, renderPropertyName = PROPERTY_ID) { if (cases.size === 0) { return { kind: "verbatim", body: [ 'throw new Error("Unreachable: encountered a value in differentiation where no variants exist.");', ], }; } const categories = categorize(cases.keys(), (type) => type.kind); const literals = [ ...(categories.Boolean ?? []), ...(categories.Number ?? []), ...(categories.String ?? []), ]; const models = categories.Model ?? []; const scalars = categories.Scalar ?? []; const intrinsics = categories.Intrinsic ?? []; if (literals.length + scalars.length + intrinsics.length === 0) { return differentiateModelTypes(ctx, module, select(models, cases), { renderPropertyName }); } else { const branches = []; for (const intrinsic of intrinsics) { const intrinsicValue = intrinsic.name === "void" ? "undefined" : "null"; branches.push({ condition: { kind: "binary-op", operator: "===", left: SUBJECT, right: { kind: "verbatim", text: intrinsicValue, }, }, body: { kind: "result", type: intrinsic, }, }); } for (const literal of literals) { branches.push({ condition: { kind: "binary-op", operator: "===", left: SUBJECT, right: { kind: "literal", value: getJsValue(ctx, literal) }, }, body: { kind: "result", type: literal, }, }); } const scalarRepresentations = new Map(); for (const scalar of scalars) { const jsScalar = getJsScalar(ctx, module, scalar, scalar).type; if (scalarRepresentations.has(jsScalar)) { reportDiagnostic(ctx.program, { code: "undifferentiable-scalar", target: scalar, format: { competitor: scalarRepresentations.get(jsScalar).name, }, }); continue; } let test; switch (jsScalar) { case "Uint8Array": test = { kind: "binary-op", operator: "instanceof", left: SUBJECT, right: { kind: "verbatim", text: "Uint8Array" }, }; break; case "number": test = { kind: "binary-op", operator: "===", left: { kind: "typeof", operand: SUBJECT }, right: { kind: "literal", value: "number" }, }; break; case "bigint": test = { kind: "binary-op", operator: "===", left: { kind: "typeof", operand: SUBJECT }, right: { kind: "literal", value: "bigint" }, }; break; case "string": test = { kind: "binary-op", operator: "===", left: { kind: "typeof", operand: SUBJECT }, right: { kind: "literal", value: "string" }, }; break; case "boolean": test = { kind: "binary-op", operator: "===", left: { kind: "typeof", operand: SUBJECT }, right: { kind: "literal", value: "boolean" }, }; break; case "Date": test = { kind: "binary-op", operator: "instanceof", left: SUBJECT, right: { kind: "verbatim", text: "Date" }, }; break; default: throw new UnimplementedError(`scalar differentiation for unknown JS Scalar '${jsScalar}'.`); } branches.push({ condition: test, body: { kind: "result", type: scalar, }, }); } return { kind: "if-chain", branches, else: models.length > 0 ? differentiateModelTypes(ctx, module, select(models, cases), { renderPropertyName }) : undefined, }; } /** * Select a subset of keys from a map. * * @param keys - The keys to select. * @param map - The map to select from. * @returns a map containing only those keys of the original map that were also in the `keys` iterable. */ function select(keys, set) { const result = new Set(); for (const key of keys) { if (set.has(key)) result.add(key); } return result; } } /** * Gets a JavaScript literal value for a given LiteralType. */ function getJsValue(ctx, literal) { switch (literal.kind) { case "Boolean": return literal.value; case "Number": { const asNumber = literal.numericValue.asNumber(); if (asNumber) return asNumber; const asBigInt = literal.numericValue.asBigInt(); if (asBigInt) return asBigInt; reportDiagnostic(ctx.program, { code: "unrepresentable-numeric-constant", target: literal, }); return 0; } case "String": return literal.value; case "EnumMember": return literal.value ?? literal.name; default: throw new UnreachableError("getJsValue for " + literal.kind, { literal }); } } function getIntegerRange(ctx, module, property) { if (property.type.kind === "Scalar" && getJsScalar(ctx, module, property.type, property).type === "number") { const minValue = getMinValue(ctx.program, property); const maxValue = getMaxValue(ctx.program, property); if (minValue !== undefined && maxValue !== undefined) { return [minValue, maxValue]; } } return false; } function overlaps(range, other) { return range[0] <= other[1] && range[1] >= other[0]; } const DEFAULT_DIFFERENTIATE_OPTIONS = { renderPropertyName: PROPERTY_ID, filter: () => true, else: undefined, }; export function differentiateModelTypes(ctx, module, models, _options = {}) { const options = { ...DEFAULT_DIFFERENTIATE_OPTIONS, ..._options }; const uniqueProps = new Map(); // Map of property names to maps of literal values that identify a model. const propertyLiterals = new Map(); // Map of models to properties with values that can uniquely identify it const uniqueLiterals = new Map(); const propertyRanges = new Map(); const uniqueRanges = new Map(); let arrayVariant = undefined; for (const model of models) { if (isArrayModelType(ctx.program, model) && model.properties.size === 0 && !arrayVariant) { arrayVariant = model; continue; } const props = new Set(); for (const prop of getAllProperties(model).filter(options.filter)) { // Don't consider optional properties for differentiation. if (prop.optional) continue; // Ignore properties that have no parseable name. if (isUnspeakable(prop.name)) continue; const renderedPropName = options.renderPropertyName(prop); // CASE - literal value if (isLiteralValueType(prop.type)) { let literals = propertyLiterals.get(renderedPropName); if (!literals) { literals = new Map(); propertyLiterals.set(renderedPropName, literals); } const value = getJsValue(ctx, prop.type); const other = literals.get(value); if (other) { // Literal already used. Leave the literal in the propertyLiterals map to prevent future collisions, // but remove the model from the uniqueLiterals map. uniqueLiterals.get(other)?.delete(renderedPropName); } else { // Literal is available. Add the model to the uniqueLiterals map and set this value. literals.set(value, model); let modelsUniqueLiterals = uniqueLiterals.get(model); if (!modelsUniqueLiterals) { modelsUniqueLiterals = new Set(); uniqueLiterals.set(model, modelsUniqueLiterals); } modelsUniqueLiterals.add(renderedPropName); } } // CASE - unique range const range = getIntegerRange(ctx, module, prop); if (range) { let ranges = propertyRanges.get(renderedPropName); if (!ranges) { ranges = new Map(); propertyRanges.set(renderedPropName, ranges); } const overlappingRanges = [...ranges.entries()].filter(([r]) => overlaps(r, range)); if (overlappingRanges.length > 0) { // Overlapping range found. Remove the model from the uniqueRanges map. for (const [, other] of overlappingRanges) { uniqueRanges.get(other)?.delete(renderedPropName); } } else { // No overlapping range found. Add the model to the uniqueRanges map and set this range. ranges.set(range, model); let modelsUniqueRanges = uniqueRanges.get(model); if (!modelsUniqueRanges) { modelsUniqueRanges = new Set(); uniqueRanges.set(model, modelsUniqueRanges); } modelsUniqueRanges.add(renderedPropName); } } // CASE - unique property let valid = true; for (const [, other] of uniqueProps) { if (other.has(prop.name) || (isLiteralValueType(prop.type) && propertyLiterals .get(renderedPropName) ?.has(getJsValue(ctx, prop.type)))) { valid = false; other.delete(prop.name); } } if (valid) { props.add(prop.name); } } uniqueProps.set(model, props); } const branches = []; let defaultCase = options.else; if (arrayVariant) { branches.push({ condition: { kind: "is-array", expr: SUBJECT, }, body: { kind: "result", type: arrayVariant }, }); } for (const [model, unique] of uniqueProps) { const literals = uniqueLiterals.get(model); const ranges = uniqueRanges.get(model); if (unique.size === 0 && (!literals || literals.size === 0) && (!ranges || ranges.size === 0)) { if (defaultCase) { reportDiagnostic(ctx.program, { code: "undifferentiable-model", target: model, }); return defaultCase; } else { // Allow a single default case. This covers more APIs that have a single model that is not differentiated by a // unique property, in which case we can make it the `else` case. defaultCase = { kind: "result", type: model }; continue; } } if (literals && literals.size > 0) { // A literal property value exists that can differentiate this model. const firstUniqueLiteral = literals.values().next().value; const property = [...model.properties.values()].find((p) => options.renderPropertyName(p) === firstUniqueLiteral); branches.push({ condition: { kind: "binary-op", left: { kind: "binary-op", left: { kind: "literal", value: options.renderPropertyName(property) }, operator: "in", right: SUBJECT, }, operator: "&&", right: { kind: "binary-op", left: { kind: "model-property", property }, operator: "===", right: { kind: "literal", value: getJsValue(ctx, property.type), }, }, }, body: { kind: "result", type: model }, }); } else if (ranges && ranges.size > 0) { // A range property value exists that can differentiate this model. const firstUniqueRange = ranges.values().next().value; const property = [...model.properties.values()].find((p) => options.renderPropertyName(p) === firstUniqueRange); const range = [...propertyRanges.get(firstUniqueRange).entries()].find(([range, candidate]) => candidate === model)[0]; branches.push({ condition: { kind: "binary-op", left: { kind: "binary-op", left: { kind: "literal", value: options.renderPropertyName(property) }, operator: "in", right: SUBJECT, }, operator: "&&", right: { kind: "in-range", expr: { kind: "model-property", property }, range, }, }, body: { kind: "result", type: model }, }); } else { const firstUniqueProp = unique.values().next().value; branches.push({ condition: { kind: "binary-op", left: { kind: "literal", value: firstUniqueProp }, operator: "in", right: SUBJECT, }, body: { kind: "result", type: model }, }); } } return { kind: "if-chain", branches, else: defaultCase, }; } /** * Writes a code tree to text, given a set of options. * * @param ctx - The emitter context. * @param tree - The code tree to write. * @param options - The options to use when writing the code tree. */ export function* writeCodeTree(ctx, tree, options) { switch (tree.kind) { case "result": yield* options.renderResult(tree.type); break; case "if-chain": { let first = true; for (const branch of tree.branches) { const condition = writeExpression(ctx, branch.condition, options); if (first) { first = false; yield `if (${condition}) {`; } else { yield `} else if (${condition}) {`; } yield* indent(writeCodeTree(ctx, branch.body, options)); } if (tree.else) { yield "} else {"; yield* indent(writeCodeTree(ctx, tree.else, options)); } yield "}"; break; } case "switch": { yield `switch (${writeExpression(ctx, tree.condition, options)}) {`; for (const _case of tree.cases) { yield ` case ${writeExpression(ctx, _case.value, options)}: {`; yield* indent(indent(writeCodeTree(ctx, _case.body, options))); yield " }"; } if (tree.default) { yield " default: {"; yield* indent(indent(writeCodeTree(ctx, tree.default, options))); yield " }"; } yield "}"; break; } case "verbatim": if (typeof tree.body === "function") { yield* tree.body(); } else { yield* tree.body; } break; default: throw new UnreachableError("writeCodeTree for " + tree.kind, { tree, }); } } function writeExpression(ctx, expression, options) { switch (expression.kind) { case "binary-op": return `(${writeExpression(ctx, expression.left, options)}) ${expression.operator} (${writeExpression(ctx, expression.right, options)})`; case "unary-op": return `${expression.operator}(${writeExpression(ctx, expression.operand, options)})`; case "is-array": return `globalThis.Array.isArray(${writeExpression(ctx, expression.expr, options)})`; case "typeof": return `typeof (${writeExpression(ctx, expression.operand, options)})`; case "literal": switch (typeof expression.value) { case "string": return JSON.stringify(expression.value); case "number": case "bigint": return String(expression.value); case "boolean": return expression.value ? "true" : "false"; default: throw new UnreachableError(`writeExpression for literal value type '${typeof expression.value}'`); } case "in-range": { const { expr, range: [min, max], } = expression; const exprText = writeExpression(ctx, expr, options); return `(${exprText} >= ${min} && ${exprText} <= ${max})`; } case "verbatim": return expression.text; case "subject": return options.subject; case "model-property": return options.referenceModelProperty(expression.property); default: throw new UnreachableError("writeExpression for " + expression.kind, { expression, }); } } //# sourceMappingURL=differentiate.js.map