@azure-tools/typespec-azure-resource-manager
Version:
TypeSpec Azure Resource Manager library
445 lines • 19.3 kB
JavaScript
import { $doc, getFriendlyName, ignoreDiagnostics, } from "@typespec/compiler";
import { unsafe_mutateSubgraph as mutateSubgraph, unsafe_MutatorFlow as MutatorFlow, } from "@typespec/compiler/experimental";
import { useStateMap } from "@typespec/compiler/utils";
import { $route, getHttpOperation, isPathParam } from "@typespec/http";
import { $actionSegment, $autoRoute, $createsOrReplacesResource, $deletesResource, $readsResource, $updatesResource, getActionSegment, getParentResource, getSegment, } from "@typespec/rest";
import { pascalCase } from "change-case";
import { reportDiagnostic } from "./lib.js";
import { isArmLibraryNamespace } from "./namespace.js";
import { getArmResourceInfo, getResourceBaseType, getResourcePathElements, isArmVirtualResource, isCustomAzureResource, ResourceBaseType, } from "./resource.js";
import { ArmStateKeys } from "./state.js";
export function getArmResourceOperations(program, resourceType) {
let operations = program.stateMap(ArmStateKeys.armResourceOperations).get(resourceType);
if (!operations) {
operations = { lifecycle: {}, lists: {}, actions: {} };
program.stateMap(ArmStateKeys.armResourceOperations).set(resourceType, operations);
}
return operations;
}
function resolveHttpOperations(program, data) {
const result = {};
for (const [key, item] of Object.entries(data)) {
const httpOperation = ignoreDiagnostics(getHttpOperation(program, item.operation));
result[key] = {
...item,
path: httpOperation.path,
httpOperation: httpOperation,
resourceName: getResourceNameForOperation(program, item, httpOperation.path),
};
}
return result;
}
export function resolveResourceOperations(program, resourceType) {
const operations = getArmResourceOperations(program, resourceType);
// Returned the updated operations object
return {
lifecycle: resolveHttpOperations(program, operations.lifecycle),
actions: resolveHttpOperations(program, operations.actions),
lists: resolveHttpOperations(program, operations.lists),
};
}
function setResourceLifecycleOperation(context, target, resourceType, kind, resourceName) {
// Only register methods from non-templated interface types
if (target.interface === undefined ||
target.interface.node === undefined ||
target.interface.node.templateParameters.length > 0) {
return;
}
// We can't resolve the operation path yet so treat the operation as a partial
// type so that we can fill in the missing details later
const operations = getArmResourceOperations(context.program, resourceType);
const resolvedResourceName = resourceName ?? resourceType.name;
const operation = {
name: target.name,
kind,
operation: target,
operationGroup: target.interface.name,
resourceName: resolvedResourceName,
};
operations.lifecycle[kind] = operation;
setArmOperationIdentifier(context.program, target, resourceType, {
name: target.name,
kind: kind,
operation: target,
operationGroup: target.interface.name,
resourceModelName: resourceType.name,
resourceName: resolvedResourceName,
});
const operationId = {
name: target.name,
kind: kind,
operation: target,
operationGroup: target.interface.name,
resource: resourceType,
resourceName: resolvedResourceName,
};
addArmResourceOperation(context.program, resourceType, operationId);
}
export const [getArmOperationList, setArmOperationList] = useStateMap(ArmStateKeys.resourceOperationList);
export function getArmResourceOperationList(program, resourceType) {
let operations = getArmOperationList(program, resourceType);
if (operations === undefined) {
operations = new Set();
setArmOperationList(program, resourceType, operations);
}
return operations;
}
export function addArmResourceOperation(program, resourceType, operationData) {
const operations = getArmResourceOperationList(program, resourceType);
operations.add(operationData);
setArmOperationList(program, resourceType, operations);
}
export const [getArmResourceOperationData, setArmResourceOperationData] = useStateMap(ArmStateKeys.armResourceOperationData);
export function setArmOperationIdentifier(program, target, resourceType, data) {
// Initialize the operations for the resource type if not already done
if (!getArmResourceOperationData(program, target)) {
setArmResourceOperationData(program, target, { ...data });
}
}
export const $armResourceCheckExistence = (context, target, resourceType, resourceName) => {
setResourceLifecycleOperation(context, target, resourceType, "checkExistence", resourceName);
};
export const $armResourceRead = (context, target, resourceType, resourceName) => {
context.call($readsResource, target, resourceType);
setResourceLifecycleOperation(context, target, resourceType, "read", resourceName);
};
export const $armResourceCreateOrUpdate = (context, target, resourceType, resourceName) => {
context.call($createsOrReplacesResource, target, resourceType);
setResourceLifecycleOperation(context, target, resourceType, "createOrUpdate", resourceName);
};
export const $armResourceUpdate = (context, target, resourceType, resourceName) => {
context.call($updatesResource, target, resourceType);
setResourceLifecycleOperation(context, target, resourceType, "update", resourceName);
};
export const $armResourceDelete = (context, target, resourceType, resourceName) => {
context.call($deletesResource, target, resourceType);
setResourceLifecycleOperation(context, target, resourceType, "delete", resourceName);
};
export const $armResourceList = (context, target, resourceType, resourceName) => {
// Only register methods from non-templated interface types
if (target.interface === undefined ||
target.interface.node === undefined ||
target.interface.node.templateParameters.length > 0) {
return;
}
// We can't resolve the operation path yet so treat the operation as a partial
// type so that we can fill in the missing details later
const operations = getArmResourceOperations(context.program, resourceType);
const resolvedResourceName = resourceName ?? resourceType.name;
const operation = {
name: target.name,
kind: "list",
operation: target,
operationGroup: target.interface.name,
resourceName: resolvedResourceName,
};
operations.lists[target.name] = operation;
const opId = {
name: target.name,
kind: "list",
operation: target,
operationGroup: target.interface.name,
resource: resourceType,
resourceName: resolvedResourceName,
};
addArmResourceOperation(context.program, resourceType, opId);
setArmOperationIdentifier(context.program, target, resourceType, {
kind: "list",
name: target.name,
operation: target,
operationGroup: target.interface.name,
resourceModelName: resourceType.name,
resourceName: resolvedResourceName,
});
};
export function armRenameListByOperationInternal(context, entity, resourceType, parentTypeName, parentFriendlyTypeName, applyOperationRename) {
const { program } = context;
if (parentTypeName === undefined ||
parentTypeName === "" ||
parentFriendlyTypeName === undefined ||
parentFriendlyTypeName === "") {
[parentTypeName, parentFriendlyTypeName] = getArmParentName(context.program, resourceType);
}
const parentType = getParentResource(program, resourceType);
if (parentType &&
!isArmVirtualResource(program, parentType) &&
!isCustomAzureResource(program, parentType)) {
const parentResourceInfo = getArmResourceInfo(program, parentType);
if (!parentResourceInfo &&
resourceType.namespace !== undefined &&
isArmLibraryNamespace(program, resourceType.namespace))
return;
if (!parentResourceInfo) {
reportDiagnostic(program, {
code: "parent-type",
messageId: "notResourceType",
target: resourceType,
format: { type: resourceType.name, parent: parentType.name },
});
return;
}
// Make sure the first character of the name is upper-cased
parentTypeName = parentType.name[0].toUpperCase() + parentType.name.substring(1);
}
// Add a formatted doc string too
context.call($doc, entity, `List ${resourceType.name} resources by ${parentType ? parentTypeName : parentFriendlyTypeName}`, undefined);
if (applyOperationRename === undefined || applyOperationRename === true) {
// Set the operation name
entity.name =
parentTypeName === "Extension" || parentTypeName === undefined || parentTypeName.length < 1
? "list"
: `listBy${parentTypeName}`;
}
}
function getArmParentName(program, resource) {
const parent = getParentResource(program, resource);
if (parent && (isArmVirtualResource(program, parent) || isCustomAzureResource(program, parent))) {
const parentName = getFriendlyName(program, parent) ?? parent.name;
if (parentName === undefined || parentName.length < 2) {
return ["", ""];
}
return [
parentName,
parentName.length > 1 ? parentName.charAt(0).toLowerCase() + parentName.substring(1) : "",
];
}
switch (getResourceBaseType(program, resource)) {
case ResourceBaseType.Extension:
return ["Extension", "parent"];
case ResourceBaseType.Location:
return ["Location", "location"];
case ResourceBaseType.Subscription:
return ["Subscription", "subscription"];
case ResourceBaseType.Tenant:
return ["Tenant", "tenant"];
case ResourceBaseType.ResourceGroup:
default:
return ["ResourceGroup", "resource group"];
}
}
export const $armResourceAction = (context, target, resourceType, resourceName) => {
const { program } = context;
// Only register methods from non-templated interface types
if (target.interface === undefined ||
target.interface.node === undefined ||
target.interface.node.templateParameters.length > 0) {
return;
}
// We can't resolve the operation path yet so treat the operation as a partial
// type so that we can fill in the missing details later
const operations = getArmResourceOperations(program, resourceType);
const resolvedResourceName = resourceName ?? resourceType.name;
const operation = {
name: target.name,
kind: "action",
operation: target,
operationGroup: target.interface.name,
resourceName: resolvedResourceName,
};
operations.actions[target.name] = operation;
const opId = {
name: target.name,
kind: "action",
operation: target,
operationGroup: target.interface.name,
resource: resourceType,
resourceName: resolvedResourceName,
};
addArmResourceOperation(program, resourceType, opId);
setArmOperationIdentifier(context.program, target, resourceType, {
kind: "action",
name: target.name,
operation: target,
operationGroup: target.interface.name,
resourceModelName: resourceType.name,
resourceName: resolvedResourceName,
});
const segment = getSegment(program, target) ?? getActionSegment(program, target);
if (!segment) {
// Also apply the @actionSegment decorator to the operation
context.call($actionSegment, target, uncapitalize(target.name));
}
};
function uncapitalize(name) {
if (name === "") {
return name;
}
return name[0].toLowerCase() + name.substring(1);
}
export const $armResourceCollectionAction = (context, target) => {
context.program.stateMap(ArmStateKeys.armResourceCollectionAction).set(target, true);
};
export function isArmCollectionAction(program, target) {
return program.stateMap(ArmStateKeys.armResourceCollectionAction).get(target) === true;
}
export const $armOperationRoute = (context, target, options) => {
const route = options?.route;
if (!route && !options?.useStaticRoute) {
context.call($autoRoute, target);
return;
}
if (route && route.length > 0) {
context.call($route, target, route);
}
};
export function getRouteOptions(program, target) {
let options = undefined;
if (target.interface) {
options = options || program.stateMap(ArmStateKeys.armResourceRoute).get(target.interface);
if (options)
return options;
}
if (target.sourceOperation?.interface) {
options =
options ||
program.stateMap(ArmStateKeys.armResourceRoute).get(target.sourceOperation.interface);
}
if (target.sourceOperation?.interface?.sourceInterfaces[0]) {
options =
options ||
program
.stateMap(ArmStateKeys.armResourceRoute)
.get(target.sourceOperation.interface.sourceInterfaces[0]);
}
if (options)
return options;
return {
useStaticRoute: false,
};
}
function storeRenamePathParameters(program, target, sourceName, targetName) {
let renameMap = program.stateMap(ArmStateKeys.renamePathParameters).get(target);
if (renameMap === undefined) {
renameMap = new Map();
}
renameMap.set(sourceName, targetName);
program.stateMap(ArmStateKeys.renamePathParameters).set(target, renameMap);
}
function getRenamePathParameter(program, target, sourceName, targetName) {
const renameMap = program.stateMap(ArmStateKeys.renamePathParameters).get(target);
if (renameMap === undefined) {
program.stateMap(ArmStateKeys.renamePathParameters).set(target, new Map());
return false;
}
return renameMap.get(sourceName) === targetName;
}
/**
* Renames a path parameter in an Azure Resource Manager operation.
* @param context The decorator context.
* @param target The operation to modify.
* @param sourceParameterName The name of the parameter to rename.
* @param targetParameterName The new name for the parameter.
* @returns
*/
export const $renamePathParameter = (context, target, sourceParameterName, targetParameterName) => {
const { program } = context;
if (getRenamePathParameter(program, target, sourceParameterName, targetParameterName)) {
return;
}
const toMutate = target.parameters;
const existingTarget = toMutate.properties.get(targetParameterName);
const existingSource = toMutate.properties.get(sourceParameterName);
if (existingSource === undefined && existingTarget !== undefined)
return;
if (existingTarget !== undefined) {
reportDiagnostic(context.program, {
code: "invalid-parameter-rename",
messageId: "overwrite",
format: { oldName: sourceParameterName, newName: targetParameterName },
target: context.decoratorTarget,
});
return;
}
if (existingSource === undefined) {
reportDiagnostic(context.program, {
code: "invalid-parameter-rename",
messageId: "missing",
format: { oldName: sourceParameterName },
target: context.decoratorTarget,
});
return;
}
if (!isPathParam(program, existingSource)) {
reportDiagnostic(context.program, {
code: "invalid-parameter-rename",
messageId: "notpath",
format: { oldName: sourceParameterName },
target: context.decoratorTarget,
});
return;
}
const mutated = mutateSubgraph(program, [createParamMutator(sourceParameterName, targetParameterName)], toMutate);
target.parameters = mutated.type;
storeRenamePathParameters(program, target, sourceParameterName, targetParameterName);
};
function createParamMutator(sourceParameterName, targetParameterName) {
return {
name: "RenameMutator",
Model: {
filter: (m, prog) => {
const param = m.properties.get(sourceParameterName);
if (m.properties.has(targetParameterName) ||
param === undefined ||
!isPathParam(prog, param)) {
return MutatorFlow.DoNotMutate;
}
return MutatorFlow.DoNotRecur;
},
mutate: (_, clone) => {
const param = clone.properties.get(sourceParameterName);
param.name = targetParameterName;
clone.properties.delete(sourceParameterName);
clone.properties.set(targetParameterName, param);
return MutatorFlow.DoNotRecur;
},
},
};
}
export function getDefaultLegacyExtensionResourceName(path, resourceName, operationKind) {
const providerIndex = path.lastIndexOf("/providers");
if (providerIndex > -1 && providerIndex < path.length - 1) {
const targetPath = path.slice(0, providerIndex);
const extensionPath = path.slice(providerIndex);
const extensionInfo = getResourcePathElements(extensionPath, operationKind);
if (!extensionInfo)
return resourceName;
const extensionName = extensionInfo.resourceType.types.flatMap((t) => pascalCase(t)).join("");
if (targetPath.length === 0) {
return extensionName;
}
if (targetPath.length === 1) {
return `${pascalCase(targetPath[0].replaceAll("{", "").replaceAll("}", ""))}${extensionName}`;
}
const targetInfo = getResourcePathElements(targetPath, "read");
if (!targetInfo || targetInfo.resourceType.types.length === 0)
return resourceName;
const types = targetInfo.resourceType.types;
return `${pascalCase(types[types.length - 1])}${extensionName}`;
}
return resourceName;
}
function getDefaultLegacyResourceName(operation, httpOp) {
const pathInfo = getResourcePathElements(httpOp, operation.kind);
if (pathInfo !== undefined) {
let types = pathInfo.resourceType.types;
if (types.length > 1) {
types = types.slice(types.length - 2);
}
return types.flatMap((t) => pascalCase(t)).join("");
}
else {
return operation.resourceModelName;
}
}
export function getResourceNameForOperation(program, operation, operationPath) {
if (operation.resourceName !== undefined && operation.resourceName.length > 0)
return operation.resourceName;
if (operation.resourceKind === "legacy-extension") {
return getDefaultLegacyExtensionResourceName(operationPath, operation.resourceModelName, operation.kind);
}
if (operation.resourceKind === "legacy") {
return getDefaultLegacyResourceName(operation, operationPath);
}
return undefined;
}
//# sourceMappingURL=operations.js.map