@azure-tools/typespec-azure-resource-manager
Version:
TypeSpec Azure Resource Manager library
109 lines • 4.75 kB
JavaScript
import { createRule, getEffectiveModelType, getProperty, isErrorType, isType, paramMessage, } from "@typespec/compiler";
import { getOperationVerb, isBody, isBodyRoot, isHeader, isPathParam, isQueryParam, } from "@typespec/http";
import { getArmResource } from "../resource.js";
import { getSourceModel, isInternalTypeSpec } from "./utils.js";
export const patchOperationsRule = createRule({
name: "arm-resource-patch",
severity: "warning",
description: "Validate ARM PATCH operations.",
messages: {
default: "The request body of a PATCH must be a model with a subset of resource properties",
missingTags: "Resource PATCH must contain the 'tags' property.",
modelSuperset: paramMessage `Resource PATCH models must be a subset of the resource type. The following properties: [${"name"}] do not exist in resource Model '${"resourceModel"}'.`,
},
create(context) {
return {
operation: (operation) => {
if (!isInternalTypeSpec(context.program, operation)) {
const verb = getOperationVerb(context.program, operation);
if (verb === "patch") {
const resourceType = getResourceModel(context.program, operation);
if (resourceType) {
checkPatchModel(context, operation, resourceType);
}
}
}
},
};
},
});
function checkPatchModel(context, operation, resourceType) {
const patchModel = getPatchModel(context.program, operation);
if (patchModel === undefined) {
context.reportDiagnostic({
target: operation,
});
}
else if (resourceType.properties.has("tags") &&
!patchModel.some((p) => p.name === "tags" && p.type.kind === "Model")) {
context.reportDiagnostic({
messageId: "missingTags",
target: operation,
});
}
else {
const resourceProperties = resourceType.properties.get("properties");
const badProperties = [];
for (const property of patchModel) {
const sourceModel = getSourceModel(property);
if (sourceModel === undefined || !getArmResource(context.program, sourceModel)) {
if (!getProperty(resourceType, property.name) &&
(resourceProperties === undefined ||
resourceProperties.type.kind !== "Model" ||
!getProperty(resourceProperties.type, property.name))) {
badProperties.push(property);
}
}
}
if (badProperties.length > 0)
context.reportDiagnostic({
messageId: "modelSuperset",
format: {
name: badProperties.flatMap((t) => t.name).join(", "),
resourceModel: resourceType.name,
},
target: operation,
});
}
}
function getResourceModel(program, operation) {
const returnType = operation.returnType;
if (returnType.kind === "Union") {
for (const variant of returnType.variants.values()) {
if (!isErrorType(variant.type) && variant.type.kind === "Model") {
const modelCandidate = getEffectiveModelType(program, variant.type);
if (getArmResource(program, modelCandidate)) {
return modelCandidate;
}
if (modelCandidate.templateMapper !== undefined) {
for (const arg of modelCandidate.templateMapper.args) {
if (isType(arg) && arg.kind === "Model" && getArmResource(program, arg)) {
return arg;
}
}
}
}
}
}
return undefined;
}
function getPatchModel(program, operation) {
const bodyProperties = [];
const patchModel = getEffectiveModelType(program, operation.parameters);
for (const [_, property] of patchModel.properties) {
if (isHeader(program, property) ||
isQueryParam(program, property) ||
isPathParam(program, property))
continue;
if ((isBody(program, property) || isBodyRoot(program, property)) &&
property.type.kind === "Scalar")
return undefined;
bodyProperties.push(property);
}
if (bodyProperties.length === 0)
return undefined;
if (bodyProperties.length === 1 && bodyProperties[0].type.kind === "Model")
return [...bodyProperties[0].type.properties.values()];
return bodyProperties;
}
//# sourceMappingURL=arm-resource-patch.js.map