diff --git a/packages/extensions/openapi-to-cadl/src/emiters/emit-arm-resources.ts b/packages/extensions/openapi-to-cadl/src/emiters/emit-arm-resources.ts index d624601157..0d35475be5 100644 --- a/packages/extensions/openapi-to-cadl/src/emiters/emit-arm-resources.ts +++ b/packages/extensions/openapi-to-cadl/src/emiters/emit-arm-resources.ts @@ -1,5 +1,6 @@ import { join } from "path"; import { getSession } from "../autorest-session"; +import { generateArmOperations } from "../generate/generate-arm-operations"; import { generateObject } from "../generate/generate-object"; import { CadlProgram } from "../interfaces"; import { ArmResourcesCache } from "../transforms/transform-resources"; @@ -13,7 +14,16 @@ export function emitArmResources(program: CadlProgram, basePath: string) { const { modules, namespaces } = getResourcesImports(program); const filePath = join(basePath, `${armResource.name}.tsp`); const generatedResource = generateObject(armResource); - const content = [modules.join("\n"), "\n", namespaces.join("\n"), "\n", generatedResource].join("\n"); + const armOperations = generateArmOperations(armResource); + const content = [ + modules.join("\n"), + "\n", + namespaces.join("\n"), + "\n", + generatedResource, + "\n", + armOperations.join("\n"), + ].join("\n"); session.writeFile({ filename: filePath, content: formatCadlFile(content, filePath) }); } } diff --git a/packages/extensions/openapi-to-cadl/src/generate/generate-arm-operations.ts b/packages/extensions/openapi-to-cadl/src/generate/generate-arm-operations.ts new file mode 100644 index 0000000000..c5cd46bab0 --- /dev/null +++ b/packages/extensions/openapi-to-cadl/src/generate/generate-arm-operations.ts @@ -0,0 +1,113 @@ +import { Operation, Schema, SchemaResponse, isObjectSchema } from "@autorest/codemodel"; +import { lowerFirst } from "lodash"; +import { plural } from "pluralize"; +import { getSession } from "../autorest-session"; +import { CadlObject, CadlObjectProperty, CadlOperation, TypespecArmResource } from "../interfaces"; +import { transformOperation } from "../transforms/transform-operations"; +import { ArmResourcesCache, getArmResourceNames } from "../transforms/transform-resources"; +import { generateDecorators } from "../utils/decorators"; +import { generateDocs } from "../utils/docs"; +import { isArraySchema, isResponseSchema } from "../utils/schemas"; + +export function generateArmOperations(resource: TypespecArmResource) { + const definitions: string[] = []; + const codeModel = getSession().model; + + const resourceOperationsKind = getResourceOperationsKind(resource); + definitions.push("@armResourceOperations"); + definitions.push( + `interface ${plural(resource.name)} extends Azure.ResourceManager.${resourceOperationsKind}<${resource.name}, ${ + resource.propertiesModelName + }>{`, + ); + + for (const operation of resource.operations) { + const typespecOperations = transformOperation(operation, codeModel).flatMap((p) => p); + for (const op of typespecOperations) { + definitions.push("@autoroute"); + definitions.push(generateDocs(op)); + definitions.push(`@armResourceLocation(${resource.name})`); + definitions.push(`@${op.verb}`); + definitions.push( + `${lowerFirst(op.name)}(${getOperationParameters(resource, op)}): ArmResponse<${getResponseType( + op, + operation, + )}> | ErrorResponse;`, + ); + } + } + definitions.push("}"); + + return definitions; +} + +function getOperationParameters(resource: TypespecArmResource, operation: CadlOperation) { + const params = [`...ResourceInstanceParameters<${resource.name}>`]; + + if (operation.extensions.includes("Pageable")) { + params.push(`...ListQueryParameters`); + } + + return params.join(", "); +} + +function getResponseType(operation: CadlOperation, rawOperation: Operation) { + const responseTypes = operation.responses.join(" |"); + if (operation.extensions.includes("Pageable")) { + const armResourceNames = getArmResourceNames(); + + if (!isResultResourceList(rawOperation)) { + return `Page<${responseTypes}>`; + } + + return `ResourceListResult<${operation.responses[0]}>`; + } + + return responseTypes; +} + +function isResultResourceList(operation: Operation) { + if (!operation.responses) { + return false; + } + + if (!operation.responses[0]) { + return false; + } + + const response = operation.responses[0]; + + if (!isResponseSchema(response)) { + return false; + } + + if (!isObjectSchema(response.schema)) { + return false; + } + + const values = response.schema.properties?.find((p) => p.serializedName === "value"); + + if (!values) { + return false; + } + + if (!isArraySchema(values.schema)) { + return false; + } + + const resultName = values.schema.elementType.language.default.name; + const resources = getArmResourceNames(); + + return resources.has(resultName); +} + +function getResourceOperationsKind(resource: TypespecArmResource) { + switch (resource.resourceKind) { + case "ProxyResource": + return "ProxyResourceOperations"; + case "TrackedResource": + return "TrackedResourceOperations"; + default: + throw new Error(`Generating operations for ${resource.resourceKind} is not yet supported`); + } +} diff --git a/packages/extensions/openapi-to-cadl/src/interfaces.ts b/packages/extensions/openapi-to-cadl/src/interfaces.ts index b489523358..2d0eac4314 100644 --- a/packages/extensions/openapi-to-cadl/src/interfaces.ts +++ b/packages/extensions/openapi-to-cadl/src/interfaces.ts @@ -1,4 +1,4 @@ -import { ObjectSchema } from "@autorest/codemodel"; +import { ObjectSchema, Operation } from "@autorest/codemodel"; export interface CadlProgram { models: Models; @@ -145,6 +145,7 @@ export interface TypespecArmResource extends CadlObject { resourceKind: ArmResourceKind; propertiesModelName: string; path: string; + operations: Operation[]; schema: ObjectSchema; } diff --git a/packages/extensions/openapi-to-cadl/src/transforms/transform-operations.ts b/packages/extensions/openapi-to-cadl/src/transforms/transform-operations.ts index c717f9e5a7..dc686b79cc 100644 --- a/packages/extensions/openapi-to-cadl/src/transforms/transform-operations.ts +++ b/packages/extensions/openapi-to-cadl/src/transforms/transform-operations.ts @@ -97,7 +97,7 @@ function transformRequest(_request: Request, operation: Operation, codeModel: Co verb: transformVerb(requests?.[0].protocol), route: transformRoute(requests?.[0].protocol), responses: [...new Set(transformedResponses)], - extensions: [], + extensions, resource, }; } diff --git a/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.port.ts b/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.port.ts deleted file mode 100644 index ae81abaf7d..0000000000 --- a/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.port.ts +++ /dev/null @@ -1,497 +0,0 @@ -// import { -// CodeModel, -// Operation, -// ObjectSchema, -// HttpMethod, -// SchemaResponse, -// isObjectSchema, -// SchemaType, -// Schema, -// HttpRequest, -// OperationGroup, -// Request, -// } from "@autorest/codemodel"; -// import { isResponseSchema } from "utils/schemas"; - -// type OperationsByPath = Record; - -// type OperationSet = Operation[] & { -// path: string; -// }; - -// type HttpVerb = "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH"; - -// const _resourceDataSchemaCache = new Map(); -// const _operationToParentRequestPathCache = new Map(); -// const _providerSegment = "/providers/"; -// const _managementGroupScopePrefix = "/providers/Microsoft.Management/managementGroups"; -// const _resourceGroupScopePrefix = "/subscriptions/{subscriptionId}/resourceGroups"; -// const _subscriptionScopePrefix = "/subscriptions"; -// const _tenantScopePrefix = "/tenants"; -// const _operationsToRequestPath: Map = new Map(); -// const _operationGroupToRequestPaths: Map> = new Map>(); -// let _rawRequestPathToOperationSets = new Map(); -// let _resourceDataSchemaNameToOperationSets = new Map>(); -// const _scopePathCache = new Map(); - -// export function decorateOperationSets(operationSets: OperationsByPath) { -// _resourceDataSchemaNameToOperationSets = new Map>(); -// for (const [path, operations] of Object.entries(operationSets)) { -// const operationSet: OperationSet = Object.assign({}, operations, { path }); -// const resourceDataSchema = getResourceDataSchema(operationSet); -// if (resourceDataSchema) { -// const schemaName = resourceDataSchema.language.default.name; - -// // if this operation set corresponds to a SDK resource, we add it to the map -// const sdkResource = _resourceDataSchemaNameToOperationSets.get(schemaName); -// if (!sdkResource) { -// _resourceDataSchemaNameToOperationSets.set(schemaName, new Set()); -// } else { -// sdkResource.add(operationSet); -// } -// } -// } - -// return _resourceDataSchemaNameToOperationSets; -// } - -// function isResource(operationSet: OperationSet){ -// return Boolean(getResourceDataSchema(operationSet)); -// } - -// function getRequestPathToResourcesMap(operationSets: OperationsByPath) { -// const resourceDataSchemaNameToOperationSets = decorateOperationSets(operationSets); - -// for (const [schemaName, operationSets] of resourceDataSchemaNameToOperationSets.entries()) { -// for (const operationSet of operationSets) { -// // get the corresponding resource data -// const originalResourcePath = getNonHintRequestPath(operationSet); -// // const opertations = -// } -// } -// } - -// function getNonHintRequestPath(operationSet: OperationSet) { -// const operation = findBestOperation(operationSet); - -// if (operation) { -// return getRequestPathFromOperation(operation); -// } - -// // TODO -// // we do not have an operation in this operation set to construct the RequestPath -// // therefore this must be a request path for a virtual resource -// // we find an operation with a prefix of this and take that many segment from its path as the request path of this operation set - -// throw new Error(`Virtual resources not implemented yet. Path: ${operationSet.path}`); -// } - -// function findBestOperation(operationSet: OperationSet) { -// if (!operationSet.length) { -// return undefined; -// } - -// const getOperation = operationSet.find((o) => hasHttpVerb(o, "GET")); - -// if (getOperation) { -// return getOperation; -// } - -// const putOperation = operationSet.find((o) => hasHttpVerb(o, "PUT")); - -// if (putOperation) { -// return putOperation; -// } - -// // if no PUT or GET, we just return the first one -// return operationSet[0]; -// } - -// type RequestPath = string; - -// function populateOperationsToRequestPaths({ operationGroups }: CodeModel) { -// for (const { operations } of operationGroups) { -// for (const operation of operations) { -// const requestPath = getRequestPathFromOperation(operation); -// _operationsToRequestPath.set(operation, requestPath); -// } -// } -// } - -// function getRequestPathFromOperation(operation: Operation): RequestPath { -// for (const request of operation.requests ?? []) { -// const httpRequest = request.protocol.http as HttpRequest; - -// if (!httpRequest) { -// continue; -// } - -// // TODO - Segments: -// // var references = new MgmtRestClientBuilder(operationGroup).GetReferencesToOperationParameters(operation, request.Parameters); -// // var segments = new List(); -// // var segmentIndex = 0; -// // CreateSegments(httpRequest.Uri, references, segments, ref segmentIndex); -// // CreateSegments(httpRequest.Path, references, segments, ref segmentIndex); - -// // return new RequestPath(CheckByIdPath(segments), operation.GetHttpPath()); - -// return httpRequest.path; -// } - -// throw new Error(`Operation doesn't contain a Path`); -// } - -// export function getPathToOperationMap(codeModel: CodeModel) { -// const { operationGroups } = codeModel; -// _rawRequestPathToOperationSets = categorizeOperationGroups(codeModel); -// populateOperationsToRequestPaths(codeModel); - -// const operationSets: OperationsByPath = {}; -// const requestPaths = new Set(); -// for (const operationGroup of operationGroups) { -// for (const operation of operationGroup.operations) { -// const httpPath = getHttpPath(operation); -// requestPaths.add(httpPath); - -// if (operationSets[httpPath]) { -// operationSets[httpPath].push(operation); -// } else { -// operationSets[httpPath] = [operation]; -// } -// } -// } - -// // TODO: Tracked resources - -// decorateOperationSets(operationSets); -// return operationSets; -// } - -// function getScopePath(requestPath: RequestPath): RequestPath { -// let scopePath = _scopePathCache.get(requestPath); - -// if(scopePath) { -// return scopePath; -// } - -// scopePath = calulateScopePath(requestPath); -// _scopePathCache.set(requestPath, scopePath); -// return scopePath; -// } - -// function calulateScopePath(requestPath: RequestPath): RequestPath { -// const segments = requestPath.split("/").filter((segment) => segment !== ""); -// const lastIndexOfProviderSegment = segments.lastIndexOf(_providerSegment); - -// // if there is no providers segment, myself should be a scope request path. Just return myself -// if(lastIndexOfProviderSegment < 0) { -// if(lastIndexOfProviderSegment === 0 && requestPath.toLowerCase().startsWith(_managementGroupScopePrefix.toLowerCase())) { -// return `${_managementGroupScopePrefix}/{managementGroupId}` -// } else { -// return requestPath.substring(0, lastIndexOfProviderSegment); -// } -// } - -// if(requestPath.toLowerCase().startsWith(_resourceGroupScopePrefix.toLowerCase())) { -// return `${_resourceGroupScopePrefix}/{resourceGroupName}`; -// } - -// if(requestPath.toLowerCase().startsWith(_subscriptionScopePrefix.toLowerCase())) { -// return `${_subscriptionScopePrefix}/{subscriptionId}`; -// } - -// if(requestPath.toLowerCase() === _tenantScopePrefix.toLowerCase()) { -// return ""; -// } - -// return requestPath; -// } - -// function getResourceDataSchema(operationSet: OperationSet) { -// const {path} = operationSet; -// const cachedSchema = _resourceDataSchemaCache.get(path); - -// if (cachedSchema === null) { -// return undefined; -// } - -// if (cachedSchema) { -// return cachedSchema; -// } - -// // TODO: Tracked and Partial resources - -// // Check if the request path has even number of segments after the providers segment -// if (getSegmentCountKind(path) === "even") { -// return undefined; -// } - -// // before we are finding any operations, we need to ensure this operation set has a GET request. -// if (!operationSet.some((operation) => hasHttpVerb(operation, "GET"))) { -// return undefined; -// } - -// // try put operation to get the resource name -// const putResponseSchema = getResponseSchema(operationSet, "PUT"); - -// if (putResponseSchema && isObjectSchema(putResponseSchema)) { -// _resourceDataSchemaCache.set(path, putResponseSchema); -// return putResponseSchema; -// } - -// // try get operation to get the resource name -// const responseSchema = getResponseSchema(operationSet, "GET"); -// if (responseSchema && isObjectSchema(responseSchema)) { -// _resourceDataSchemaCache.set(path, responseSchema); -// return responseSchema; -// } - -// // We tried everything, this is not a resource -// _resourceDataSchemaCache.set(path, null); -// return undefined; -// } - -// function getSegmentCountKind(path: string): SegmentCountKind { -// const index = path.lastIndexOf(_providerSegment); - -// if (index < 0) { -// return "even"; -// } - -// const following = path.substring(index); -// const segments: string[] = following.split("/").filter((segment) => segment !== ""); - -// return segments.length % 2 == 0 ? "even" : "odd"; -// } - -// type SegmentCountKind = "even" | "odd"; - -// function getHttpPath(operation: Operation): string { -// const httpRequest = getHttpRequest(operation); - -// if (!httpRequest?.path) { -// throw new Error(`operation ${operation.language.default.name} doesn't have an http path`); -// } - -// return httpRequest.path; -// } - -// function calculateResourceChildOperations() { -// const childOperations = new Map>(); - -// for(const [path, operationSet] of _rawRequestPathToOperationSets) { -// if (isResource(operationSet)) { -// continue; -// } - -// for(const operation of operationSet) { -// const parentRequestPath = operation. -// } -// } -// } - -// export function parentRequestPath(operation: Operation): RequestPath { -// let requestPath = _operationToParentRequestPathCache.get(operation); -// if (requestPath) { -// return requestPath; -// } - -// const result = getParentRequestPath(operation); -// _operationToParentRequestPathCache.set(operation, result); -// return result; -// } - -// function getParentRequestPath(operation: Operation): RequestPath { -// // escape the calculation if this is configured in the configuration -// const httpPath = getHttpPath(operation); -// const currentRequestPath = getRequestPathFromOperation(operation); -// const currentOperationSet = _rawRequestPathToOperationSets.get(currentRequestPath); -// // if this operation comes from a resource, return itself -// if (currentOperationSet && isResource(currentOperationSet)) { -// return currentRequestPath; -// } - -// // if this operation corresponds to a collection operation of a resource, return the path of the resource -// let operationSetOfResource: OperationSet; -// if (isResourceCollectionOperation(operation)) { -// return operationSetOfResource.getRequestPath(); -// } - -// // if neither of the above, we find a request path that is the longest parent of this, and belongs to a resource -// return parentRequestPath(currentRequestPath); -// } - -// function isListOperation(operation: Operation) { -// for (const response of operation.responses ?? []) { -// if(!isResponseSchema(response)) { -// continue; -// } - -// return response.schema.type === SchemaType.Array; -// } - -// return false; -// } - -// export function isResourceCollectionOperation(operation: Operation): boolean { -// let operationSetOfResource: OperationSet | null = null; -// // first we need to ensure this operation at least returns a collection of something - -// if (!isListOperation(operation)) { -// return false; -// } - -// // then check if its path is a prefix of which resource's operationSet -// // if there are multiple resources that share the same prefix of request path, we choose the shortest one -// const requestPath = getHttpPath(operation); -// operationSetOfResource = findOperationSetOfResource(requestPath); -// // if we find none, this cannot be a resource collection operation -// if (!operationSetOfResource) { -// return [false, null]; -// } - -// // then check if this method returns a collection of the corresponding resource data -// // check if valueType is the current resource data type -// const resourceData = MgmtContext.Library.getResourceData(operationSetOfResource.requestPath); -// return [valueType === resourceData.type, operationSetOfResource]; -// } - -// function findOperationSetOfResource(requestPath: RequestPath): OperationSet | null { -// const candidates: OperationSet[] = []; - -// for(const operationSet of _rawRequestPathToOperationSets.values()) { -// const resourceRequestPath = operationSet.path; -// // we compare the request with the resource request in two parts: -// // 1. Compare if they have the same scope -// // 2. Compare if they have the "compatible" remaining path -// // check if they have compatible scopes -// // if(!isScopeCompatible(resourceRequestPath, requestPath)) { -// // continue; -// // } - -// } -// } - -// function getScopeResourceTypes(requestPath: RequestPath) { -// const scope = getScopePath(requestPath); -// // TODO: Handle non-parametrized scopes - -// } - -// function isScopeCompatible(requestPath: RequestPath, resourcePath: RequestPath): boolean { -// // get scope types -// const requestScopeTypes = getScopeResourceTypes(requestPath); -// const resourceScopeTypes = getScopeResourceTypes(resourcePath); -// if (resourceScopeTypes.has(ResourceTypeSegment.Any)) { -// return true; -// } -// return isSubset(requestScopeTypes, resourceScopeTypes); -// } - -// function categorizeOperationGroups(codeModel: CodeModel) { -// const rawRequestPathToOperationSets = new Map(); -// for (const operationGroup of codeModel.operationGroups) { -// const requestPathSet = new Set(); -// _operationGroupToRequestPaths.set(operationGroup, requestPathSet); -// for (const operation of operationGroup.operations) { -// const path = getHttpPath(operation); -// requestPathSet.add(path); - -// let operationSet = rawRequestPathToOperationSets.get(path); -// if (operationSet) { -// operationSet.push(operation); -// } else { -// operationSet = Object.assign({}, [operation], { path }); -// rawRequestPathToOperationSets.set(path, operationSet); -// } -// } -// } - -// return rawRequestPathToOperationSets; -// } - -// function getResponseSchema(operationSet: OperationSet, verb: HttpVerb, statusCode = "200") { -// const operation = operationSet.find((operation) => hasHttpVerb(operation, verb)); -// const response = operation?.responses?.find( -// (r) => r.protocol.http?.statusCodes && r.protocol.http.statusCodes.includes(statusCode), -// ); - -// if (!response) { -// return undefined; -// } - -// if (!isResponseSchema(response)) { -// return undefined; -// } - -// // we need to verify this schema has ID, type and name so that this is a resource model -// if (!isResourceSchema(response.schema)) { -// return undefined; -// } - -// return response.schema; -// } - -// function isResourceSchema(schema: Schema) { -// if (!isObjectSchema(schema)) { -// return undefined; -// } - -// const allProperties = schema.properties ?? []; -// let hasIdProperty = false; -// let hasTypeProperty = false; -// let hasNameProperty = false; -// // TODO: -// // bool typePropertyFound = !Configuration.MgmtConfiguration.DoesResourceModelRequireType; -// // bool namePropertyFound = !Configuration.MgmtConfiguration.DoesResourceModelRequireName; - -// for (const property of allProperties) { -// // check if this property is flattened from lower level, we should only consider first level properties in this model -// // therefore if flattenedNames is not empty, this property is flattened, we skip this property -// if (property.flattenedNames?.length) { -// continue; -// } - -// switch (property.serializedName) { -// case "id": -// if (property.schema.type === SchemaType.String || property.schema.type === SchemaType.ArmId) { -// hasIdProperty = true; -// continue; -// } -// break; -// case "type": -// if (property.schema.type === SchemaType.String) { -// hasTypeProperty = true; -// continue; -// } -// break; -// case "name": -// if (property.schema.type === SchemaType.String) { -// hasNameProperty = true; -// continue; -// } -// break; -// } -// } - -// return hasIdProperty && hasNameProperty && hasTypeProperty; -// } - -// function hasHttpVerb(operation: Operation, verb: HttpVerb) { -// for (const request of operation.requests ?? []) { -// const method: string = ((request.protocol.http?.method as string) ?? "").toUpperCase(); -// if (method === verb) { -// return true; -// } -// } - -// return false; -// } - -// function getHttpRequest({ requests = [] }: Operation) { -// for (const request of requests) { -// return request.protocol.http ?? undefined; -// } - -// return undefined; -// } diff --git a/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.ts b/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.ts index ac5015d55f..81e307ad31 100644 --- a/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.ts +++ b/packages/extensions/openapi-to-cadl/src/transforms/transform-resources.ts @@ -1,35 +1,29 @@ -import { CodeModel, HttpRequest, ObjectSchema, Operation, StringSchema } from "@autorest/codemodel"; +import { CodeModel, ObjectSchema, Operation, StringSchema } from "@autorest/codemodel"; import { upperFirst } from "lodash"; -import { singular } from "pluralize"; -import { getSession } from "../autorest-session"; +import pluralize, { singular } from "pluralize"; import { ArmResourceHierarchy, CadlDecorator, TypespecArmResource } from "../interfaces"; import { isResponseSchema } from "../utils/schemas"; +import { transformOperation } from "./transform-operations"; const _resourceKinds = ["ProxyResource", "TrackedResource"]; export const ArmResourcesCache = new Map(); +let _armResourcesnameCache: Set | undefined; export interface ArmResourceSchema extends ObjectSchema { armResource?: TypespecArmResource; } -// export function getResources(codeModel: CodeModel) { -// const resources: TypespecArmResource[] = []; -// for (const schema of codeModel.schemas?.objects ?? []) { -// if (isResourceModel(schema)) { -// const resourceOperation = findResourceOperation(codeModel, schema); -// const path = getOperationPath(resourceOperation); -// const key = findResourceKey(resourceOperation); -// schema.armResource = { -// kind: "object", -// resourceKind: getResourceKind(schema), -// name: schema.language.default.name, -// schema, -// path, -// properties: [], -// parents: [], -// }; -// } -// } -// } +export function getArmResourceNames(): Set { + if (_armResourcesnameCache) { + return _armResourcesnameCache; + } + + _armResourcesnameCache = new Set(); + for (const resource of ArmResourcesCache.values()) { + _armResourcesnameCache.add(resource.name); + } + + return _armResourcesnameCache; +} function getPropertiesModelName(schema: ObjectSchema) { const property = schema.properties?.find((p) => p.serializedName === "properties"); @@ -46,7 +40,8 @@ export function calculateArmResources(codeModel: CodeModel) { } for (const schema of codeModel.schemas?.objects ?? []) { if (isResourceModel(schema)) { - const resourceOperation = findResourceOperation(codeModel, schema); + const resourceOperation = findResourceFirstOperation(codeModel, schema); + const path = getOperationPath(resourceOperation); const key = findResourceKey(resourceOperation); @@ -58,11 +53,17 @@ export function calculateArmResources(codeModel: CodeModel) { resourceDecorators.push({ name: "parentResource", arguments: [hierarchy.parent.name] }); } + const resourceName = schema.language.default.name; + + const operations = findResourceOperations(codeModel, resourceName); + ArmResourcesCache.set(schema, { kind: "object", + operations, + doc: schema.language.default.description, propertiesModelName: getPropertiesModelName(schema), resourceKind: getResourceKind(schema), - name: schema.language.default.name, + name: resourceName, schema, path, decorators: resourceDecorators, @@ -74,9 +75,10 @@ export function calculateArmResources(codeModel: CodeModel) { name: "name", type: "string", decorators: [ - { name: "key", arguments: [key.name] }, ...(key.pattern ? [{ name: "pattern", arguments: [escapeRegex(key.pattern)] }] : []), + { name: "key", arguments: [key.name] }, { name: key.location }, + { name: "segment", arguments: [key.segmentName] }, ], }, ], @@ -121,18 +123,6 @@ function escapeRegex(str: string) { // return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string } -// export function getArmResource(schema: ObjectSchema): TypespecArmResource | undefined { -// const session = getSession(); -// const codeModel = session.model; -// if (!isResourceModel(schema)) { -// return undefined; -// } -// const resourceOperation = findResourceOperation(codeModel, schema); -// const path = getOperationPath(resourceOperation); -// const key = findResourceKey(resourceOperation); -// return { kind: getResourceKind(schema), name: schema.language.default.name, schema, path }; -// } - function findResourceKey(operation: Operation) { const path = getOperationPath(operation); const segments = path.split("/").filter((s) => s !== ""); @@ -151,7 +141,7 @@ function findResourceKey(operation: Operation) { }; } -function findResourceOperation(codeModel: CodeModel, schema: ObjectSchema): Operation { +function findResourceFirstOperation(codeModel: CodeModel, schema: ObjectSchema): Operation { for (const operationGroup of codeModel.operationGroups) { for (const operation of operationGroup.operations) { for (const response of operation.responses ?? []) { @@ -166,6 +156,14 @@ function findResourceOperation(codeModel: CodeModel, schema: ObjectSchema): Oper throw new Error(`Unable to determine path for resource ${schema.language.default.name}`); } +const _defaultVerbs = ["get", "put", "patch", "delete"]; + +function findResourceOperations(codeModel: CodeModel, resourceName: string): Operation[] { + return codeModel.operationGroups + .filter((o) => o.language.default.name === pluralize(resourceName)) + .flatMap((og) => og.operations.filter((op) => !_defaultVerbs.includes(op.requests?.[0].protocol?.http?.method))); +} + function getOperationPath(operation: Operation): string { for (const request of operation.requests ?? []) { if (request.protocol.http?.path) { @@ -176,14 +174,6 @@ function getOperationPath(operation: Operation): string { throw new Error(`Unable to determine path for operation ${operation.language.default.name}`); } -function isResourceResponse(request: HttpRequest, schema: ObjectSchema) { - for (const response of request.responses ?? []) { - if (isResourceResponse(response, schema)) { - return true; - } - } -} - function getResourceKind(schema: ObjectSchema) { for (const parent of schema.parents?.immediate ?? []) { switch (parent.language.default.name) { diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Catalog.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Catalog.tsp index f72452d381..e6a0b61738 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Catalog.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Catalog.tsp @@ -8,10 +8,59 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An Azure Sphere catalog") model Catalog is TrackedResource { @doc("Name of catalog") - @key("catalogName") @pattern("^[A-Za-z0-9_-]{1,50}$") + @key("catalogName") @path + @segment("catalogs") name: string; } + +@armResourceOperations +interface Catalogs + extends Azure.ResourceManager.TrackedResourceOperations< + Catalog, + CatalogProperties + > { + @autoroute + @doc("Counts devices in catalog.") + @armResourceLocation(Catalog) + @post + countDevices( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; + @autoroute + @doc("Lists deployments for catalog.") + @armResourceLocation(Catalog) + @post + listDeployments( + ...ResourceInstanceParameters, + ...ListQueryParameters + ): ArmResponse> | ErrorResponse; + @autoroute + @doc("List the device groups for the catalog.") + @armResourceLocation(Catalog) + @post + listDeviceGroups( + ...ResourceInstanceParameters, + ...ListQueryParameters + ): ArmResponse> | ErrorResponse; + @autoroute + @doc("Lists device insights for catalog.") + @armResourceLocation(Catalog) + @post + listDeviceInsights( + ...ResourceInstanceParameters, + ...ListQueryParameters + ): ArmResponse> | ErrorResponse; + @autoroute + @doc("Lists devices for catalog.") + @armResourceLocation(Catalog) + @post + listDevices( + ...ResourceInstanceParameters, + ...ListQueryParameters + ): ArmResponse> | ErrorResponse; +} diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Certificate.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Certificate.tsp index cd1b7dcd4c..c34f0a04f9 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Certificate.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Certificate.tsp @@ -8,6 +8,7 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An certificate resource belonging to a catalog resource.") @parentResource("Catalog") model Certificate is ProxyResource { @doc(""" @@ -16,5 +17,28 @@ certificate. """) @key("serialNumber") @path + @segment("certificates") name: string; } + +@armResourceOperations +interface Certificates + extends Azure.ResourceManager.ProxyResourceOperations< + Certificate, + CertificateProperties + > { + @autoroute + @doc("Retrieves cert chain.") + @armResourceLocation(Certificate) + @post + retrieveCertChain( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; + @autoroute + @doc("Gets the proof of possession nonce.") + @armResourceLocation(Certificate) + @post + retrieveProofOfPossessionNonce( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; +} diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Deployment.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Deployment.tsp index bcd92fe38c..d050396091 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Deployment.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Deployment.tsp @@ -8,6 +8,7 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An deployment resource belonging to a device group resource.") @parentResource("DeviceGroup") model Deployment is ProxyResource { @doc(""" @@ -16,5 +17,13 @@ deployment for the associated device group. """) @key("deploymentName") @path + @segment("deployments") name: string; } + +@armResourceOperations +interface Deployments + extends Azure.ResourceManager.ProxyResourceOperations< + Deployment, + DeploymentProperties + > {} diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Device.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Device.tsp index e06bee140f..6b3e7f1ab2 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Device.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Device.tsp @@ -8,11 +8,32 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An device resource belonging to a device group resource.") @parentResource("DeviceGroup") model Device is ProxyResource { @doc("Device name") - @key("deviceName") @pattern("^[a-zA-Z0-9-]{128}$") + @key("deviceName") @path + @segment("devices") name: string; } + +@armResourceOperations +interface Devices + extends Azure.ResourceManager.ProxyResourceOperations< + Device, + DeviceProperties + > { + @autoroute + @doc(""" +Generates the capability image for the device. Use '.unassigned' or '.default' +for the device group and product names to generate the image for a device that +does not belong to a specific device group and product. +""") + @armResourceLocation(Device) + @post + generateCapabilityImage( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; +} diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/DeviceGroup.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/DeviceGroup.tsp index 7e498bcabe..0f7c568ac2 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/DeviceGroup.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/DeviceGroup.tsp @@ -8,11 +8,41 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An device group resource belonging to a product resource.") @parentResource("Product") model DeviceGroup is ProxyResource { @doc("Name of device group.") - @key("deviceGroupName") @pattern("^[A-Za-z0-9]{1,2}$|^[A-Za-z0-9][A-Za-z0-9\\s]{1,48}[A-Za-z0-9]$|^\\.default$|^\\.unassigned$") + @key("deviceGroupName") @path + @segment("deviceGroups") name: string; } + +@armResourceOperations +interface DeviceGroups + extends Azure.ResourceManager.ProxyResourceOperations< + DeviceGroup, + DeviceGroupProperties + > { + @autoroute + @doc(""" +Bulk claims the devices. Use '.unassigned' or '.default' for the device group +and product names when bulk claiming devices to a catalog only. +""") + @armResourceLocation(DeviceGroup) + @post + claimDevices( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; + @autoroute + @doc(""" +Counts devices in device group. '.default' and '.unassigned' are system defined +values and cannot be used for product or device group name. +""") + @armResourceLocation(DeviceGroup) + @post + countDevices( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; +} diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Image.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Image.tsp index dd1fc8db42..32a9155bd1 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Image.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Image.tsp @@ -8,10 +8,19 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An image resource belonging to a catalog resource.") @parentResource("Catalog") model Image is ProxyResource { @doc("Image name. Use .default for image creation.") @key("imageName") @path + @segment("images") name: string; } + +@armResourceOperations +interface Images + extends Azure.ResourceManager.ProxyResourceOperations< + Image, + ImageProperties + > {} diff --git a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Product.tsp b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Product.tsp index b937630af7..4e60cac06c 100644 --- a/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Product.tsp +++ b/packages/extensions/openapi-to-cadl/test/arm-sphere/tsp-output/Product.tsp @@ -8,11 +8,42 @@ using Azure.ResourceManager; using Azure.ResourceManager.Foundations; using TypeSpec.Http; +@doc("An product resource belonging to a catalog resource.") @parentResource("Catalog") model Product is ProxyResource { @doc("Name of product.") - @key("productName") @pattern("^[\\w][\\w\\s]{1,48}[\\w]$|^\\.default$|^\\.unassigned$") + @key("productName") @path + @segment("products") name: string; } + +@armResourceOperations +interface Products + extends Azure.ResourceManager.ProxyResourceOperations< + Product, + ProductProperties + > { + @autoroute + @doc(""" +Counts devices in product. '.default' and '.unassigned' are system defined +values and cannot be used for product name. +""") + @armResourceLocation(Product) + @post + countDevices( + ...ResourceInstanceParameters + ): ArmResponse | ErrorResponse; + @autoroute + @doc(""" +Generates default device groups for the product. '.default' and '.unassigned' +are system defined values and cannot be used for product name. +""") + @armResourceLocation(Product) + @post + generateDefaultDeviceGroups( + ...ResourceInstanceParameters, + ...ListQueryParameters + ): ArmResponse> | ErrorResponse; +} diff --git a/packages/extensions/openapi-to-cadl/test/utils/generate-cadl.ts b/packages/extensions/openapi-to-cadl/test/utils/generate-cadl.ts index 09ece40c10..c441894443 100644 --- a/packages/extensions/openapi-to-cadl/test/utils/generate-cadl.ts +++ b/packages/extensions/openapi-to-cadl/test/utils/generate-cadl.ts @@ -63,7 +63,7 @@ async function main() { try { await generateCadl(folder, true); } catch (e) { - throw new Error(`Failed to generate ${folder}`); + throw new Error(`Failed to generate ${e}`); } } }