@typespec/compiler
Version:
TypeSpec Compiler Preview
269 lines • 9.69 kB
JavaScript
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