UNPKG

@typespec/compiler

Version:

TypeSpec Compiler Preview

269 lines 9.69 kB
import { createDiagnosticCollector } from "../core/diagnostics.js"; import { getTypeName } from "../core/helpers/type-name-utils.js"; import { createDiagnostic, reportDiagnostic } from "../core/messages.js"; import { navigateProgram } from "../core/semantic-walker.js"; import { isArrayModelType } from "../core/type-utils.js"; import { createStateSymbol } from "../lib/utils.js"; import { DuplicateTracker, useStateSet } from "../utils/index.js"; import { isNumericType, isStringType } from "./decorators.js"; export const [ /** * Check if the given operation is used to page through a list. * @param program Program * @param target Operation */ isList, markList, /** {@inheritdoc ListDecorator} */ listDecorator,] = createMarkerDecorator("list"); export const [ /** * Check if the given property is the `@offset` property for a paging operation. * @param program Program * @param target Model Property */ isOffsetProperty, markOffset, /** {@inheritdoc OffsetDecorator} */ offsetDecorator,] = createMarkerDecorator("offset", createNumericValidation("offset")); export const [ /** * Check if the given property is the `@pageIndex` property for a paging operation. * @param program Program * @param target Model Property */ isPageIndexProperty, markPageIndexProperty, /** {@inheritdoc PageIndexDecorator} */ pageIndexDecorator,] = createMarkerDecorator("pageIndex", createNumericValidation("pageIndex")); export const [ /** * Check if the given property is the `@pageIndex` property for a paging operation. * @param program Program * @param target Model Property */ isPageSizeProperty, markPageSizeProperty, /** {@inheritdoc PageSizeDecorator} */ pageSizeDecorator,] = createMarkerDecorator("pageSize", createNumericValidation("pageSize")); export const [ /** * Check if the given property is the `@pageIndex` property for a paging operation. * @param program Program * @param target Model Property */ isPageItemsProperty, markPageItemsProperty, /** {@inheritdoc PageItemsDecorator} */ pageItemsDecorator,] = createMarkerDecorator("pageItems", (context, target) => { if (target.type.kind !== "Model" || !isArrayModelType(context.program, target.type)) { reportDiagnostic(context.program, { code: "decorator-wrong-target", messageId: "withExpected", format: { decorator: "continuationToken", expected: "Array", to: getTypeName(target.type) }, target: context.decoratorTarget, }); return false; } return true; }); export const [ /** * Check if the given property is the `@pageIndex` property for a paging operation. * @param program Program * @param target Model Property */ isContinuationTokenProperty, markContinuationTokenProperty, /** {@inheritdoc ContinuationTokenDecorator} */ continuationTokenDecorator,] = createMarkerDecorator("continuationToken", (context, target) => { if (!isStringType(context.program, target.type)) { reportDiagnostic(context.program, { code: "decorator-wrong-target", messageId: "withExpected", format: { decorator: "continuationToken", expected: "string", to: getTypeName(target.type) }, target: context.decoratorTarget, }); return false; } return true; }); export const [ /** * Check if the given property is the `@nextLink` property for a paging operation. * @param program Program * @param target Model Property */ isNextLink, markNextLink, /** {@inheritdoc NextLinkDecorator} */ nextLinkDecorator,] = createMarkerDecorator("nextLink"); export const [ /** * Check if the given property is the `@prevLink` property for a paging operation. * @param program Program * @param target Model Property */ isPrevLink, markPrevLink, /** {@inheritdoc PrevLinkDecorator} */ prevLinkDecorator,] = createMarkerDecorator("prevLink"); export const [ /** * Check if the given property is the `@firstLink` property for a paging operation. * @param program Program * @param target Model Property */ isFirstLink, markFirstLink, /** {@inheritdoc FirstLinkDecorator} */ firstLinkDecorator,] = createMarkerDecorator("firstLink"); export const [ /** * Check if the given property is the `@lastLink` property for a paging operation. * @param program Program * @param target Model Property */ isLastLink, markLastLink, /** {@inheritdoc LastLinkDecorator} */ lastLinkDecorator,] = createMarkerDecorator("lastLink"); export function validatePagingOperations(program) { navigateProgram(program, { operation: (op) => { if (isList(program, op)) { validatePagingOperation(program, op); } }, }); } const inputProps = new Set(["offset", "pageIndex", "pageSize", "continuationToken"]); const outputProps = new Set([ "pageItems", "nextLink", "prevLink", "firstLink", "lastLink", "continuationToken", ]); function findPagingProperties(program, op, base, source) { const diags = createDiagnosticCollector(); const acceptableProps = source === "input" ? inputProps : outputProps; const duplicateTracker = new DuplicateTracker(); const data = {}; navigateProperties(base, (property, path) => { const kind = diags.pipe(getPagingProperty(program, property)); if (kind === undefined) { return; } duplicateTracker.track(kind, property); if (acceptableProps.has(kind)) { data[kind] = { property, path }; } else { diags.add(createDiagnostic({ code: "invalid-paging-prop", messageId: source === "input" ? "input" : "output", format: { kind }, target: property, })); } }); for (const [key, duplicates] of duplicateTracker.entries()) { for (const prop of duplicates) { diags.add(createDiagnostic({ code: "duplicate-paging-prop", format: { kind: key, operationName: op.name }, target: prop, })); } } return diags.wrap(data); } export function getPagingOperation(program, op) { const diags = createDiagnosticCollector(); const result = { input: diags.pipe(findPagingProperties(program, op, op.parameters, "input")), output: diags.pipe(findPagingProperties(program, op, op.returnType, "output")), }; if (result.output.pageItems === undefined) { diags.add(createDiagnostic({ code: "missing-paging-items", format: { operationName: op.name }, target: op, })); return diags.wrap(undefined); } return diags.wrap(result); } function navigateProperties(type, callback, path = [], visited = new Set()) { if (visited.has(type)) return; visited.add(type); switch (type.kind) { case "Model": for (const prop of type.properties.values()) { callback(prop, [...path, prop]); navigateProperties(prop.type, callback, [...path, prop], visited); } if (type.baseModel) { navigateProperties(type.baseModel, callback, path, visited); } break; case "Union": for (const member of type.variants.values()) { navigateProperties(member, callback, path, visited); } break; case "UnionVariant": navigateProperties(type.type, callback, path, visited); break; } } function getPagingProperty(program, prop) { const diagnostics = []; const props = { offset: isOffsetProperty(program, prop), pageIndex: isPageIndexProperty(program, prop), pageItems: isPageItemsProperty(program, prop), pageSize: isPageSizeProperty(program, prop), continuationToken: isContinuationTokenProperty(program, prop), nextLink: isNextLink(program, prop), prevLink: isPrevLink(program, prop), lastLink: isLastLink(program, prop), firstLink: isFirstLink(program, prop), }; const defined = Object.entries(props).filter((x) => !!x[1]); if (defined.length > 1) { diagnostics.push(createDiagnostic({ code: "incompatible-paging-props", format: { kinds: defined.map((x) => x[0]).join(", ") }, target: prop, })); } if (defined.length === 0) { return [undefined, diagnostics]; } return [defined[0][0], diagnostics]; } function validatePagingOperation(program, op) { const [_, diagnostics] = getPagingOperation(program, op); program.reportDiagnostics(diagnostics); } function createMarkerDecorator(key, validate) { const [isLink, markLink] = useStateSet(createStateSymbol(key)); const decorator = (...args) => { if (validate && !validate(...args)) { return; } const [context, target] = args; markLink(context.program, target); }; return [isLink, markLink, decorator]; } function createNumericValidation(decoratorName) { return (context, target) => { if (!isNumericType(context.program, target.type)) { reportDiagnostic(context.program, { code: "decorator-wrong-target", messageId: "withExpected", format: { decorator: decoratorName, expected: "numeric", to: getTypeName(target.type), }, target: context.decoratorTarget, }); return false; } return true; }; } //# sourceMappingURL=paging.js.map