diff --git a/src/Bicep.Types.UnitTests/TypeSerializerTests.cs b/src/Bicep.Types.UnitTests/TypeSerializerTests.cs index 64e9364eb9..682c73db78 100644 --- a/src/Bicep.Types.UnitTests/TypeSerializerTests.cs +++ b/src/Bicep.Types.UnitTests/TypeSerializerTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using Azure.Bicep.Types.Concrete; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -69,12 +70,11 @@ public void Different_types_can_be_serialized_and_deserialized() var intType = factory.Create(() => new BuiltInType(BuiltInTypeKind.Int)); var objectType = factory.Create(() => new ObjectType("steven", new Dictionary(), null)); var arrayType = factory.Create(() => new ArrayType(factory.GetReference(objectType))); - var resourceType = factory.Create(() => new ResourceType("gerrard", ScopeType.ResourceGroup, factory.GetReference(objectType))); + var resourceType = factory.Create(() => new ResourceType("gerrard", ScopeType.ResourceGroup|ScopeType.Tenant, ScopeType.Tenant, factory.GetReference(objectType), ResourceFlags.None)); var unionType = factory.Create(() => new UnionType(new [] { factory.GetReference(intType), factory.GetReference(objectType) })); var stringLiteralType = factory.Create(() => new StringLiteralType("abcdef")); var discriminatedObjectType = factory.Create(() => new DiscriminatedObjectType("disctest", "disctest", new Dictionary(), new Dictionary())); var resourceFunctionType = factory.Create(() => new ResourceFunctionType("listTest", "zona", "2020-01-01", factory.GetReference(objectType), factory.GetReference(objectType))); - var serialized = TypeSerializer.Serialize(factory.GetTypes()); var deserialized = TypeSerializer.Deserialize(serialized); @@ -91,11 +91,38 @@ public void Different_types_can_be_serialized_and_deserialized() ((ObjectType)deserialized[1]).Name.Should().Be(objectType.Name); ((ArrayType)deserialized[2]).ItemType!.Type.Should().Be(deserialized[1]); ((ResourceType)deserialized[3]).Name.Should().Be(resourceType.Name); + ((ResourceType)deserialized[3]).Flags.Should().Be(resourceType.Flags); + ((ResourceType)deserialized[3]).ReadOnlyScopes.HasValue.Should().Be(true); + ((ResourceType)deserialized[3]).ReadOnlyScopes.Should().Be(resourceType.ReadOnlyScopes); ((UnionType)deserialized[4]).Elements![0].Type.Should().Be(deserialized[0]); ((UnionType)deserialized[4]).Elements![1].Type.Should().Be(deserialized[1]); ((StringLiteralType)deserialized[5]).Value.Should().Be(stringLiteralType.Value); ((DiscriminatedObjectType)deserialized[6]).Name.Should().Be(discriminatedObjectType.Name); ((ResourceFunctionType)deserialized[7]).Name.Should().Be(resourceFunctionType.Name); } + + [TestMethod] + public void Resources_without_flags_or_readonly_scopes_can_be_deserialized() + { + var factory = new TypeFactory(Enumerable.Empty()); + var objectType = factory.Create(() => new ObjectType("steven", new Dictionary(), null)); + var resourceType = factory.Create(() => new ResourceType("gerrard", ScopeType.ResourceGroup|ScopeType.Tenant, ScopeType.Tenant, factory.GetReference(objectType), ResourceFlags.ReadOnly)); + var serialized = TypeSerializer.Serialize(factory.GetTypes()); + + var deserializedNode = JsonNode.Parse(serialized)!; + deserializedNode.AsArray()[1]?.AsObject()["4"]?.AsObject().Remove("Flags").Should().BeTrue(); + deserializedNode.AsArray()[1]?.AsObject()["4"]?.AsObject().Remove("ReadOnlyScopes").Should().BeTrue(); + serialized = deserializedNode.ToJsonString(); + + var deserialized = TypeSerializer.Deserialize(serialized); + + deserialized[0].Should().BeOfType(); + deserialized[1].Should().BeOfType(); + + ((ObjectType)deserialized[0]).Name.Should().Be(objectType.Name); + ((ResourceType)deserialized[1]).Name.Should().Be(resourceType.Name); + ((ResourceType)deserialized[1]).Flags.Should().Be(ResourceFlags.None); + ((ResourceType)deserialized[1]).ReadOnlyScopes.HasValue.Should().Be(false); + } } -} \ No newline at end of file +} diff --git a/src/Bicep.Types/Concrete/ResourceType.cs b/src/Bicep.Types/Concrete/ResourceType.cs index ca10746936..51ecff3bc0 100644 --- a/src/Bicep.Types/Concrete/ResourceType.cs +++ b/src/Bicep.Types/Concrete/ResourceType.cs @@ -1,21 +1,35 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; namespace Azure.Bicep.Types.Concrete { + [Flags] + public enum ResourceFlags + { + None = 0, + ReadOnly = 1 << 0, + } + public class ResourceType : TypeBase { - public ResourceType(string name, ScopeType scopeType, ITypeReference body) + public ResourceType(string name, ScopeType scopeType, ScopeType? readOnlyScopes, ITypeReference body, ResourceFlags flags) { Name = name; ScopeType = scopeType; + ReadOnlyScopes = readOnlyScopes; Body = body; + Flags = flags; } public string Name { get; set; } public ScopeType ScopeType { get; set; } + public ScopeType? ReadOnlyScopes { get; set; } + public ITypeReference Body { get; set; } + + public ResourceFlags Flags { get; set; } } -} \ No newline at end of file +} diff --git a/src/autorest.bicep/src/resources.ts b/src/autorest.bicep/src/resources.ts index fea03c42fc..3fb478a845 100644 --- a/src/autorest.bicep/src/resources.ts +++ b/src/autorest.bicep/src/resources.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ChoiceSchema, CodeModel, HttpMethod, HttpParameter, HttpRequest, HttpResponse, ImplementationLocation, ObjectSchema, Operation, Parameter, ParameterLocation, Request, Response, Schema, SchemaResponse, SealedChoiceSchema, Metadata } from "@autorest/codemodel"; +import { ChoiceSchema, CodeModel, ComplexSchema, HttpMethod, HttpParameter, HttpRequest, HttpResponse, ImplementationLocation, isObjectSchema, ObjectSchema, Operation, Parameter, ParameterLocation, Request, Response, Schema, SchemaResponse, SealedChoiceSchema, Metadata } from "@autorest/codemodel"; import { Channel, AutorestExtensionHost } from "@autorest/extension-base"; -import { keys, Dictionary, values, groupBy, uniqBy } from 'lodash'; +import { keys, Dictionary, values, groupBy, uniqBy, chain, flatten } from 'lodash'; import { success, failure, Result } from './utils'; export enum ScopeType { @@ -21,6 +21,7 @@ export interface ResourceDescriptor { typeSegments: string[]; apiVersion: string; constantName?: string; + readonlyScopes?: ScopeType; } export interface ProviderDefinition { @@ -30,12 +31,18 @@ export interface ProviderDefinition { resourceActions: ResourceListActionDefinition[]; } +export interface ResourceOperationDefintion { + request: HttpRequest; + response?: HttpResponse; + parameters: Parameter[]; + requestSchema?: ObjectSchema; + responseSchema?: ObjectSchema; +} + export interface ResourceDefinition { descriptor: ResourceDescriptor; - putRequest: HttpRequest; - putParameters: Parameter[]; - putSchema?: ObjectSchema; - getSchema?: ObjectSchema; + putOperation?: ResourceOperationDefintion; + getOperation?: ResourceOperationDefintion; } export interface ResourceListActionDefinition { @@ -108,11 +115,23 @@ function getNormalizedMethodPath(path: string) { return path; } -export function getSerializedName(metadata: Metadata) { +export function getSerializedName(metadata: Metadata) { return metadata.language.default.serializedName ?? metadata.language.default.name; } -export function parseNameSchema(request: HttpRequest, parameters: Parameter[], parseType: (schema: Schema) => T, createConstantName: (name: string) => T): Result { +interface ParameterizedName { + type: 'parameterized'; + schema: Schema; +} + +interface ConstantName { + type: 'constant'; + value: string; +} + +type NameSchema = ParameterizedName|ConstantName; + +export function getNameSchema(request: HttpRequest, parameters: Parameter[]): Result { const path = getNormalizedMethodPath(request.path); const finalProvidersMatch = path.match(parentScopePrefix)?.slice(-1)[0]; @@ -134,14 +153,28 @@ export function parseNameSchema(request: HttpRequest, parameters: Parameter[] return failure(`Unable to locate parameter with name '${resNameParam}'`); } - return success(parseType(param.schema)); + return success({type: 'parameterized', schema: param.schema}); } if (!/^[a-zA-Z0-9]*$/.test(resNameParam)) { return failure(`Unable to process non-alphanumeric name '${resNameParam}'`); } - return success(createConstantName(resNameParam)); + return success({type: 'constant', value: resNameParam}); +} + +export function parseNameSchema(request: HttpRequest, parameters: Parameter[], parseType: (schema: Schema) => T, createConstantName: (name: string) => T): Result { + const nsResult = getNameSchema(request, parameters); + if (!nsResult.success) { + return nsResult; + } + + const {value} = nsResult; + if (value.type === 'parameterized') { + return success(parseType(value.schema)); + } + + return success(createConstantName(value.value)); } export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExtensionHost): ProviderDefinition[] { @@ -161,6 +194,29 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten return apiVersions.flatMap(v => getProviderDefinitionsForApiVersion(v)); } + function getExtensions(schema: ComplexSchema) { + // extensions are defined as Record in autorest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extensions: Record = {}; + if (isObjectSchema(schema)) { + for (const parent of schema.parents?.all || []) { + for (const [key, value] of Object.entries(getExtensions(parent))) { + extensions[key] = value; + } + } + } + + for (const [key, value] of Object.entries(schema.extensions || {})) { + extensions[key] = value; + } + + return extensions; + } + + function isResourceSchema(schema?: ComplexSchema) { + return schema && getExtensions(schema)['x-ms-azure-resource']; + } + function getProviderDefinitionsForApiVersion(apiVersion: string) { const providerDefinitions: Dictionary = {}; const operations = codeModel.operationGroups.flatMap(x => x.operations); @@ -191,7 +247,7 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten if (putRequest) { putOperationsByPath[putRequest.path.toLowerCase()] = operation; } - const postListRequest = requests.filter(r => { + const postListRequest = requests.filter(r => { if (r.method !== HttpMethod.Post) { return false; } @@ -211,19 +267,31 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten }); const resourcesByProvider: Dictionary = {}; - for (const lcPath in putOperationsByPath) { + for (const lcPath of new Set([...Object.keys(putOperationsByPath), ...Object.keys(getOperationsByPath)])) { const putOperation = putOperationsByPath[lcPath]; const getOperation = getOperationsByPath[lcPath]; - const putData = getPutSchema(putOperation); - const getData = getGetSchema(getOperation) ?? putData; - if (!putData || !getData) { + const getData = getGetSchema(getOperation); + + let parseResult: Result; + if (putData) { + parseResult = parseResourceMethod( + putData.request.path, + putData.parameters, + apiVersion, + !!getData, + true + ); + } else if (getData && isResourceSchema(getData.responseSchema)) { + parseResult = parseResourceMethod(getData.request.path, getData.parameters, apiVersion, true, false); + } else { + // A non-resource get with no corresponding put is most likely a collection or utility endpoint. + // No types should be generated continue; } - const parseResult = parseResourceMethod(putData.request.path, putData.parameters, apiVersion); if (!parseResult.success) { - logWarning(`Skipping path '${putData.request.path}': ${parseResult.error}`); + logWarning(`Skipping path '${putData?.request.path ?? getData?.request.path ?? lcPath}': ${parseResult.error}`); continue; } @@ -232,10 +300,20 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten const resource: ResourceDefinition = { descriptor, - putRequest: putData.request, - putParameters: putData.parameters, - putSchema: (putData.schema instanceof ObjectSchema) ? putData.schema : undefined, - getSchema: (getData.schema instanceof ObjectSchema) ? getData.schema : undefined, + putOperation: putData + ? { + ...putData, + requestSchema: (putData?.requestSchema instanceof ObjectSchema) ? putData.requestSchema : undefined, + responseSchema: (putData?.responseSchema instanceof ObjectSchema) ? putData.responseSchema : undefined, + } + : undefined, + getOperation: getData + ? { + ...getData, + requestSchema: (getData?.requestSchema instanceof ObjectSchema) ? getData.requestSchema : undefined, + responseSchema: (getData?.responseSchema instanceof ObjectSchema) ? getData.responseSchema : undefined, + } + : undefined }; const lcNamespace = descriptor.namespace.toLowerCase(); @@ -289,7 +367,7 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten return values(providerDefinitions); } - + function getRequestSchema(operation: Operation | undefined, requests: Request[]) { if (!operation || requests.length === 0) { return; @@ -346,14 +424,41 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten } function getGetSchema(operation?: Operation) { - return getResponseSchema(operation); + const requestSchema = getRequestSchema( + operation, + operation?.requests?.filter(r => r.protocol.http?.method === HttpMethod.Get) ?? [] + ); + const responseSchema = getResponseSchema(operation); + + if (!requestSchema || !responseSchema) { + return; + } + + return { + request: requestSchema.request, + response: responseSchema.response, + parameters: requestSchema.parameters, + requestSchema: requestSchema.schema, + responseSchema: responseSchema.schema, + }; } function getPutSchema(operation?: Operation) { const requests = operation?.requests ?? []; const validRequests = requests.filter(r => (r.protocol.http as HttpRequest)?.method === HttpMethod.Put); + const requestSchema = getRequestSchema(operation, validRequests); + const responseSchema = getResponseSchema(operation); + if (!requestSchema) { + return; + } - return getRequestSchema(operation, validRequests); + return { + request: requestSchema.request, + response: responseSchema?.response, + parameters: requestSchema.parameters, + requestSchema: requestSchema.schema, + responseSchema: responseSchema?.schema, + }; } function getPostSchema(operation?: Operation) { @@ -369,6 +474,7 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten return { request: request.request, + response: response.response, parameters: request.parameters, requestSchema: request.schema, responseSchema: response.schema, @@ -391,7 +497,14 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten return success({ scopeType, routingScope }); } - function parseResourceDescriptors(parameters: Parameter[], apiVersion: string, scopeType: ScopeType, routingScope: string): Result { + function parseResourceDescriptors( + parameters: Parameter[], + apiVersion: string, + scopeType: ScopeType, + routingScope: string, + readable: boolean, + writable: boolean + ): Result { const namespace = routingScope.substr(0, routingScope.indexOf('/')); if (isPathVariable(namespace)) { return failure(`Unable to process parameterized provider namespace "${namespace}"`); @@ -411,12 +524,19 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten typeSegments: type, apiVersion, constantName, + readonlyScopes: readable && !writable ? scopeType : undefined, })); return success(descriptors); } - function parseResourceMethod(path: string, parameters: Parameter[], apiVersion: string) { + function parseResourceMethod( + path: string, + parameters: Parameter[], + apiVersion: string, + readable: boolean, + writable: boolean + ) { const resourceScopeResult = parseResourceScopes(path); if (!resourceScopeResult.success) { @@ -425,7 +545,7 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten const { scopeType, routingScope } = resourceScopeResult.value; - return parseResourceDescriptors(parameters, apiVersion, scopeType, routingScope); + return parseResourceDescriptors(parameters, apiVersion, scopeType, routingScope, readable, writable); } function parseResourceActionMethod(path: string, parameters: Parameter[], apiVersion: string) { @@ -440,12 +560,12 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten const routingScope = actionRoutingScope.substr(0, actionRoutingScope.lastIndexOf('/')); const actionName = actionRoutingScope.substr(actionRoutingScope.lastIndexOf('/') + 1); - const resourceDescriptorsResult = parseResourceDescriptors(parameters, apiVersion, scopeType, routingScope); + const resourceDescriptorsResult = parseResourceDescriptors(parameters, apiVersion, scopeType, routingScope, false, true); if (!resourceDescriptorsResult.success) { return failure(resourceDescriptorsResult.error); } - return success({ + return success({ descriptors: resourceDescriptorsResult.value, actionName: actionName, }); @@ -478,7 +598,7 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten const choiceSchema = parameter.schema; if (!(choiceSchema instanceof ChoiceSchema || choiceSchema instanceof SealedChoiceSchema)) { return failure(`Parameter reference ${typeSegment} is not defined as an enum`); - } + } if (choiceSchema.choices.length === 0) { return failure(`Parameter reference ${typeSegment} is defined as an enum, but doesn't have any specified values`); @@ -528,6 +648,25 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten return scopeA | scopeB; } + function mergeReadonlyScopes( + currentScopes: ScopeType, + currentReadonlyScopes: ScopeType|undefined, + newScopes: ScopeType, + newReadonlyScopes: ScopeType|undefined + ) { + function writableScopes(scopes: ScopeType, readonlyScopes: ScopeType|undefined) { + return readonlyScopes !== undefined ? scopes ^ readonlyScopes : scopes; + } + const mergedScopes = mergeScopes(currentScopes, newScopes); + if (mergedScopes === ScopeType.Unknown) { + const writingPermittedSomewhere = currentScopes !== currentReadonlyScopes || newScopes !== newReadonlyScopes; + return writingPermittedSomewhere ? undefined : ScopeType.Unknown; + } + + const mergedWritableScopes = writableScopes(currentScopes, currentReadonlyScopes) | writableScopes(newScopes, newReadonlyScopes); + return mergedScopes === mergedWritableScopes ? undefined : mergedScopes ^ mergedWritableScopes; + } + function collapseDefinitionScopes(resources: ResourceDefinition[]) { const definitionsByName: Dictionary = {}; for (const resource of resources) { @@ -541,6 +680,7 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten descriptor: { ...curDescriptor, scopeType: mergeScopes(curDescriptor.scopeType, newDescriptor.scopeType), + readonlyScopes: mergeReadonlyScopes(curDescriptor.scopeType, curDescriptor.readonlyScopes, newDescriptor.scopeType, newDescriptor.readonlyScopes), }, }; } else { @@ -551,9 +691,67 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten return Object.values(definitionsByName); } + function collapsePartiallyConstantNameResources(resources: ResourceDefinition[]) + { + const definitionsByNormalizedPath = resources.reduce((acc, resource) => { + const path = resource.putOperation?.request.path ?? resource.getOperation?.request.path ?? '/'; + const normalizedPath = path.substring(0, path.lastIndexOf('/') + 1); + if (acc[normalizedPath]) { + acc[normalizedPath].push(resource); + } else { + acc[normalizedPath] = [resource]; + } + + return acc; + }, {} as Dictionary); + + function hasComparableSchemata( + definitions: ResourceDefinition[], + schemaExtractor: (r: ResourceDefinition) => ObjectSchema|undefined + ) { + return chain(definitions) + .map(schemaExtractor) + .filter() + .map(s => s!.language.default.name) + .uniq() + .value().length < 2; + } + + for (const path of Object.keys(definitionsByNormalizedPath)) { + const atPath = definitionsByNormalizedPath[path]; + const parameterized = chain(atPath).map(r => r.descriptor).filter(d => d.constantName === undefined).value(); + + if ( + parameterized.length === 1 && + hasComparableSchemata(atPath, d => d.putOperation?.requestSchema) && + hasComparableSchemata(atPath, d => d.getOperation?.responseSchema) + ) { + let scopeType = atPath[0].descriptor.scopeType; + let readonlyScopes = atPath[0].descriptor.readonlyScopes; + for (let i = 1; i < atPath.length; i++) { + const {scopeType: newScopes, readonlyScopes: newReadonlyScopes} = atPath[i].descriptor; + scopeType = mergeScopes(scopeType, newScopes); + readonlyScopes = mergeReadonlyScopes(scopeType, readonlyScopes, newScopes, newReadonlyScopes); + } + + definitionsByNormalizedPath[path] = [{ + descriptor: { + ...parameterized[0], + scopeType, + readonlyScopes, + }, + putOperation: chain(atPath).map(r => r.putOperation).find().value(), + getOperation: chain(atPath).map(r => r.getOperation).find().value(), + }]; + } + } + + return flatten(Object.values(definitionsByNormalizedPath)); + } + function collapseDefinitions(resources: ResourceDefinition[]) { - const resourcesByType = groupByType(resources); - const collapsedResources = Object.values(resourcesByType).flatMap(collapseDefinitionScopes); + const deduplicated = Object.values(groupByType(resources)).flatMap(collapsePartiallyConstantNameResources); + const collapsedResources = Object.values(groupByType(deduplicated)).flatMap(collapseDefinitionScopes); return groupByType(collapsedResources); } @@ -565,4 +763,4 @@ export function getProviderDefinitions(codeModel: CodeModel, host: AutorestExten } return getProviderDefinitions(); -} \ No newline at end of file +} diff --git a/src/autorest.bicep/src/type-generator.ts b/src/autorest.bicep/src/type-generator.ts index 7e95bd5e00..870034aeca 100755 --- a/src/autorest.bicep/src/type-generator.ts +++ b/src/autorest.bicep/src/type-generator.ts @@ -3,9 +3,10 @@ import { AnySchema, ArraySchema, ChoiceSchema, ConstantSchema, DictionarySchema, ObjectSchema, PrimitiveSchema, Property, Schema, SchemaType, SealedChoiceSchema, StringSchema } from "@autorest/codemodel"; import { Channel, AutorestExtensionHost } from "@autorest/extension-base"; -import { ArrayType, BuiltInTypeKind, DiscriminatedObjectType, ObjectProperty, ObjectPropertyFlags, ObjectType, ResourceFunctionType, ResourceType, StringLiteralType, TypeFactory, TypeReference, UnionType } from "./types"; +import { ArrayType, BuiltInTypeKind, DiscriminatedObjectType, ObjectProperty, ObjectPropertyFlags, ObjectType, ResourceFlags, ResourceFunctionType, ResourceType, StringLiteralType, TypeFactory, TypeReference, UnionType } from "./types"; import { uniq, keys, keyBy, Dictionary, flatMap } from 'lodash'; -import { getFullyQualifiedType, getSerializedName, parseNameSchema, ProviderDefinition, ResourceDefinition, ResourceDescriptor } from "./resources"; +import { getFullyQualifiedType, getNameSchema, getSerializedName, ProviderDefinition, ResourceDefinition, ResourceDescriptor, ResourceOperationDefintion } from "./resources"; +import { failure, success } from "./utils"; export function generateTypes(host: AutorestExtensionHost, definition: ProviderDefinition) { const factory = new TypeFactory(); @@ -19,31 +20,89 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD host.message({ Channel: Channel.Information, Text: message, }); } - function processResourceBody(fullyQualifiedType: string, definition: ResourceDefinition) { - const { descriptor, putRequest, putParameters, putSchema, getSchema, } = definition; - const nameSchemaResult = parseNameSchema( - putRequest, - putParameters, - schema => parseType(schema, schema), - (name) => factory.addType(new StringLiteralType(name))); - - if (!nameSchemaResult.success) { - logWarning(`Skipping resource type ${fullyQualifiedType} under path '${putRequest.path}': ${nameSchemaResult.error}`); - return + function getResourcePath(definition: ResourceDefinition) { + return (definition.putOperation ?? definition.getOperation)?.request.path; + } + + function getNameType(fullyQualifiedType: string, definition: ResourceDefinition) { + function getSchema(op: ResourceOperationDefintion) { + const r = getNameSchema(op.request, op.parameters); + + if (!r.success) { + logWarning(`Skipping resource type ${fullyQualifiedType} under path '${op.request.path}': ${r.error}`); + return + } + + return r.value; } - if (!nameSchemaResult.value) { - logWarning(`Skipping resource type ${fullyQualifiedType} under path '${putRequest.path}': failed to obtain a name value`); + // In some cases, the one of the PUT or GET operations for a resource is defined with a constant name while the + // other defines a parameterized name, or a resource may use an enum to strictly enforce what names may be used to + // PUT a resource while reserving itself some flexibility by providing a looser definition of what will be returned + // by a GET. Because the resource's name property will be used both when defining the resource and when using the + // `existing` keyword, the two definitions of a resource's name need to be reconciled with a different approach than + // is used for other resource properties. + const {putOperation, getOperation} = definition; + const nameLiterals = new Set(); + const nameTypes = new Set(); + for (const ns of [putOperation ? getSchema(putOperation) : undefined, getOperation ? getSchema(getOperation) : undefined]) { + if (!ns) { + continue; + } + + if (ns.type === 'parameterized') { + const {schema} = ns; + if (schema instanceof ConstantSchema && toBuiltInTypeKind(schema.valueType) === BuiltInTypeKind.String) { + nameLiterals.add(schema.value.value); + } else if (schema instanceof ChoiceSchema || schema instanceof SealedChoiceSchema) { + const enumValues = getValuesForEnum(schema); + if (enumValues.success) { + const {values, closed} = enumValues.value; + values.forEach(v => nameLiterals.add(v)); + if (!closed) { + nameTypes.add(BuiltInTypeKind.String); + } + } + } else { + nameTypes.add(toBuiltInTypeKind(schema)); + } + } else { + nameLiterals.add(ns.value); + } + } + + const enumTypes = [...nameLiterals].map(l => factory.addType(new StringLiteralType(l))) + .concat([...nameTypes].map(t => factory.lookupBuiltInType(t))); + + if (enumTypes.length === 1) { + return success(enumTypes[0]); + } else if (enumTypes.length > 0) { + return success(factory.addType(new UnionType(enumTypes))); + } + + return failure('failed to obtain a name value'); + } + + function processResourceBody(fullyQualifiedType: string, definition: ResourceDefinition) { + const { descriptor, putOperation, getOperation } = definition; + const {requestSchema: putSchema} = putOperation || {}; + const getSchema = getOperation ? getOperation.responseSchema : putSchema; + + const nameType = getNameType(fullyQualifiedType, definition); + + if (!nameType.success) { + logWarning(`Skipping resource type ${fullyQualifiedType} under path '${getResourcePath(definition)}': ${nameType.error}`); return } - const resourceProperties = getStandardizedResourceProperties(descriptor, nameSchemaResult.value); + const resourceProperties = getStandardizedResourceProperties(descriptor, nameType.value); let resourceDefinition: TypeReference; - if (putSchema) { - resourceDefinition = createObject(getFullyQualifiedType(descriptor), putSchema, resourceProperties); + const schema = definition.putOperation ? putSchema : getSchema; + if (schema) { + resourceDefinition = createObject(getFullyQualifiedType(descriptor), schema, resourceProperties); } else { - logInfo(`Resource type ${fullyQualifiedType} under path '${putRequest.path}' has no body defined.`); + logInfo(`Resource type ${fullyQualifiedType} under path '${getResourcePath(definition)}' has no body defined.`); resourceDefinition = factory.addType(new ObjectType(getFullyQualifiedType(descriptor), resourceProperties)); } @@ -60,7 +119,7 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD } } - if (putSchema?.discriminator) { + if (schema?.discriminator) { const discriminatedObjectType = factory.lookupType(resourceDefinition) as DiscriminatedObjectType; handlePolymorphicType(discriminatedObjectType, putSchema, getSchema); @@ -73,7 +132,7 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD if (definitions.length > 1) { for (const definition of definitions) { if (!definition.descriptor.constantName) { - logWarning(`Skipping resource type ${fullyQualifiedType} under path '${definitions[0].putRequest.path}': Found multiple definitions for the same type`); + logWarning(`Skipping resource type ${fullyQualifiedType} under path '${getResourcePath(definitions[0])}': Found multiple definitions for the same type`); return null; } } @@ -129,11 +188,17 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD } const { descriptor, bodyType } = output; + let flags = ResourceFlags.None; + if (descriptor.readonlyScopes === descriptor.scopeType) { + flags |= ResourceFlags.ReadOnly; + } factory.addType(new ResourceType( `${getFullyQualifiedType(descriptor)}@${descriptor.apiVersion}`, descriptor.scopeType, - bodyType)); + descriptor.readonlyScopes !== descriptor.scopeType ? descriptor.readonlyScopes : undefined, + bodyType, + flags)); } for (const action of resourceActions) { @@ -370,20 +435,18 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD return flags; } - function parsePrimaryType(putSchema: PrimitiveSchema | undefined, getSchema: PrimitiveSchema | undefined) { - const combinedSchema = combineAndThrowIfNull(putSchema, getSchema); - - switch (combinedSchema.type) { + function toBuiltInTypeKind(schema: PrimitiveSchema) { + switch (schema.type) { case SchemaType.Boolean: - return factory.lookupBuiltInType(BuiltInTypeKind.Bool); + return BuiltInTypeKind.Bool; case SchemaType.Integer: case SchemaType.Number: case SchemaType.UnixTime: - return factory.lookupBuiltInType(BuiltInTypeKind.Int); + return BuiltInTypeKind.Int; case SchemaType.Object: - return factory.lookupBuiltInType(BuiltInTypeKind.Any); + return BuiltInTypeKind.Any; case SchemaType.ByteArray: - return factory.lookupBuiltInType(BuiltInTypeKind.Array); + return BuiltInTypeKind.Array; case SchemaType.Uri: case SchemaType.Date: case SchemaType.DateTime: @@ -392,13 +455,18 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD case SchemaType.Uuid: case SchemaType.Duration: case SchemaType.Credential: - return factory.lookupBuiltInType(BuiltInTypeKind.String); + return BuiltInTypeKind.String; default: - logWarning(`Unrecognized known property type: "${combinedSchema.type}"`); - return factory.lookupBuiltInType(BuiltInTypeKind.Any); + logWarning(`Unrecognized known property type: "${schema.type}"`); + return BuiltInTypeKind.Any; } } + function parsePrimaryType(putSchema: PrimitiveSchema | undefined, getSchema: PrimitiveSchema | undefined) { + const combinedSchema = combineAndThrowIfNull(putSchema, getSchema); + return factory.lookupBuiltInType(toBuiltInTypeKind(combinedSchema)); + } + function handlePolymorphicType(discriminatedObjectType: DiscriminatedObjectType, putSchema?: ObjectSchema, getSchema?: ObjectSchema) { for (const { putSubType, getSubType } of getDiscriminatedSubTypes(putSchema, getSchema)) { const combinedSubType = combineAndThrowIfNull(putSubType, getSubType); @@ -499,21 +567,32 @@ export function generateTypes(host: AutorestExtensionHost, definition: ProviderD return definition; } + function getValuesForEnum(schema: ChoiceSchema|SealedChoiceSchema) { + if (!(schema.choiceType instanceof StringSchema)) { + // we can only handle string enums right now + return failure('Only string enums can be converted to union types'); + } + + return success({ + values: schema.choices.map(c => c.value.toString()), + closed: schema instanceof SealedChoiceSchema + }); + } + function parseEnumType(putSchema: ChoiceSchema | SealedChoiceSchema | undefined, getSchema: ChoiceSchema | SealedChoiceSchema | undefined) { const combinedSchema = combineAndThrowIfNull(putSchema, getSchema); - if (!(combinedSchema.choiceType instanceof StringSchema)) { - // we can only handle string enums right now + const enumValues = getValuesForEnum(combinedSchema); + + if (!enumValues.success) { return parseType(putSchema?.choiceType, getSchema?.choiceType); } - const enumTypes = []; - for (const enumValue of combinedSchema.choices) { - const stringLiteralType = factory.addType(new StringLiteralType(enumValue.value.toString())); - enumTypes.push(stringLiteralType); - } + const {values, closed} = enumValues.value; + + const enumTypes = values.map(s => factory.addType(new StringLiteralType(s))); - if (combinedSchema.type === SchemaType.Choice) { + if (!closed) { enumTypes.push(factory.lookupBuiltInType(BuiltInTypeKind.String)); } diff --git a/src/autorest.bicep/src/types.ts b/src/autorest.bicep/src/types.ts index dd643aae1b..4840477d71 100644 --- a/src/autorest.bicep/src/types.ts +++ b/src/autorest.bicep/src/types.ts @@ -45,11 +45,17 @@ const ScopeTypeLabel = new Map([ [ScopeType.Extension, 'Extension'], ]); -export function getScopeTypeLabels(input: ScopeType) { +export function getScopeTypeLabels(input: ScopeType, ...scopeLabels: [ScopeType|undefined, string][]) { const types = []; for (const [key, value] of ScopeTypeLabel) { if ((key & input) === key) { - types.push(value); + const labels = []; + for (const [labeledScopes, label] of scopeLabels) { + if (labeledScopes !== undefined && (key & labeledScopes) === key) { + labels.push(label); + } + } + types.push(`${value}${labels.length > 0 ? ` (${labels.join(', ')})` : ''}`); } } @@ -107,6 +113,26 @@ export function getTypeBaseKindLabel(input: TypeBaseKind) { return TypeBaseKindLabel.get(input) ?? ''; } +export enum ResourceFlags { + None = 0, + ReadOnly = 1 << 0, +} + +const ResourceFlagsLabels = new Map([ + [ResourceFlags.ReadOnly, 'ReadOnly'], +]); + +export function getResourceFlagsLabels(input: ResourceFlags) { + const flags = []; + for (const [bitmask, label] of ResourceFlagsLabels) { + if ((bitmask & input) === bitmask) { + flags.push(label); + } + } + + return flags; +} + export abstract class TypeBase { constructor(type: TypeBaseKind) { this.Type = type; @@ -146,15 +172,19 @@ export class StringLiteralType extends TypeBase { } export class ResourceType extends TypeBase { - constructor(name: string, scopeType: ScopeType, body: TypeReference) { + constructor(name: string, scopeType: ScopeType, readOnlyScopes: ScopeType|undefined, body: TypeReference, flags: ResourceFlags) { super(TypeBaseKind.ResourceType); this.Name = name; this.ScopeType = scopeType; + this.ReadOnlyScopes = readOnlyScopes; this.Body = body; + this.Flags = flags; } readonly Name: string; readonly ScopeType: ScopeType; + readonly ReadOnlyScopes?: ScopeType; readonly Body: TypeReference; + readonly Flags: ResourceFlags; } export class ResourceFunctionType extends TypeBase { @@ -250,4 +280,4 @@ export class TypeFactory { public lookupBuiltInType(kind: BuiltInTypeKind): TypeReference { return this.builtInTypes[kind]; } -} \ No newline at end of file +} diff --git a/src/autorest.bicep/src/writers/markdown.ts b/src/autorest.bicep/src/writers/markdown.ts index 39ec18f156..7b05f235d2 100644 --- a/src/autorest.bicep/src/writers/markdown.ts +++ b/src/autorest.bicep/src/writers/markdown.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { Dictionary, keys, orderBy } from 'lodash'; -import { ArrayType, BuiltInType, DiscriminatedObjectType, getBuiltInTypeKindLabel, getObjectPropertyFlagsLabels, getScopeTypeLabels, ObjectProperty, ObjectType, ResourceFunctionType, ResourceType, StringLiteralType, TypeBase, TypeBaseKind, TypeReference, UnionType } from '../types'; +import { ArrayType, BuiltInType, DiscriminatedObjectType, getBuiltInTypeKindLabel, getObjectPropertyFlagsLabels, getResourceFlagsLabels, getScopeTypeLabels, ObjectProperty, ObjectType, ResourceFunctionType, ResourceType, StringLiteralType, TypeBase, TypeBaseKind, TypeReference, UnionType } from '../types'; export function writeMarkdown(provider: string, apiVersion: string, types: TypeBase[]) { let output = ''; @@ -117,8 +117,9 @@ export function writeMarkdown(provider: string, apiVersion: string, types: TypeB switch (type.Type) { case TypeBaseKind.ResourceType: { const resourceType = type as ResourceType; - writeHeading(nesting, `Resource ${resourceType.Name}`); - writeBullet("Valid Scope(s)", `${getScopeTypeLabels(resourceType.ScopeType).join(', ') || 'Unknown'}`); + const flagsString = resourceType.Flags ? ` (${getResourceFlagsLabels(resourceType.Flags).join(', ')})` : ''; + writeHeading(nesting, `Resource ${resourceType.Name}${flagsString}`); + writeBullet("Valid Scope(s)", `${getScopeTypeLabels(resourceType.ScopeType, [resourceType.ReadOnlyScopes, 'ReadOnly']).join(', ') || 'Unknown'}`); writeComplexType(types, types[resourceType.Body.Index], nesting, false); return; diff --git a/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.json b/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.json index a404bb763b..2474d8139b 100644 --- a/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.json +++ b/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.json @@ -1 +1 @@ -[{"1":{"Kind":1}},{"1":{"Kind":2}},{"1":{"Kind":3}},{"1":{"Kind":4}},{"1":{"Kind":5}},{"1":{"Kind":6}},{"1":{"Kind":7}},{"1":{"Kind":8}},{"6":{"Value":"Test.Rp1/testType1"}},{"6":{"Value":"2021-10-31"}},{"2":{"Name":"Test.Rp1/testType1","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":8,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":9,"Flags":10,"Description":"The resource api version"},"properties":{"Type":11,"Flags":0,"Description":"The resource properties."},"tags":{"Type":26,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":27,"Flags":2,"Description":"Azure Resource Manager metadata containing createdBy and modifiedBy information."}}}},{"2":{"Name":"TestType1CreateOrUpdatePropertiesOrTestType1Properties","Properties":{"basicString":{"Type":4,"Flags":0,"Description":"Description for a basic string property."},"stringEnum":{"Type":14,"Flags":0,"Description":"Description for a basic enum property."},"skuTier":{"Type":19,"Flags":0,"Description":"This field is required to be implemented by the Resource Provider if the service has more than one tier, but is not required on a PUT."},"encryptionProperties":{"Type":20,"Flags":0,"Description":"TestType1 encryption properties"},"locationData":{"Type":25,"Flags":2,"Description":"Metadata pertaining to the geographic location of the resource."}}}},{"6":{"Value":"Foo"}},{"6":{"Value":"Bar"}},{"5":{"Elements":[12,13,4]}},{"6":{"Value":"Free"}},{"6":{"Value":"Basic"}},{"6":{"Value":"Standard"}},{"6":{"Value":"Premium"}},{"5":{"Elements":[15,16,17,18]}},{"2":{"Name":"EncryptionProperties","Properties":{"status":{"Type":23,"Flags":0,"Description":"Indicates whether or not the encryption is enabled for container registry."},"keyVaultProperties":{"Type":24,"Flags":0,"Description":"Key vault properties."}}}},{"6":{"Value":"enabled"}},{"6":{"Value":"disabled"}},{"5":{"Elements":[21,22,4]}},{"2":{"Name":"KeyVaultProperties","Properties":{"keyIdentifier":{"Type":4,"Flags":0,"Description":"Key vault uri to access the encryption key."},"identity":{"Type":4,"Flags":0,"Description":"The client ID of the identity which will be used to access key vault."}}}},{"2":{"Name":"LocationData","Properties":{"name":{"Type":4,"Flags":1,"Description":"A canonical name for the geographic or physical location."},"city":{"Type":4,"Flags":0,"Description":"The city or locality where the resource is located."},"district":{"Type":4,"Flags":0,"Description":"The district, state, or province where the resource is located."},"countryOrRegion":{"Type":4,"Flags":0,"Description":"The country or region where the resource is located"}}}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"SystemData","Properties":{"createdBy":{"Type":4,"Flags":0,"Description":"The identity that created the resource."},"createdByType":{"Type":32,"Flags":0,"Description":"The type of identity that created the resource."},"createdAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource creation (UTC)."},"lastModifiedBy":{"Type":4,"Flags":0,"Description":"The identity that last modified the resource."},"lastModifiedByType":{"Type":37,"Flags":0,"Description":"The type of identity that last modified the resource."},"lastModifiedAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource last modification (UTC)"}}}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[28,29,30,31,4]}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[33,34,35,36,4]}},{"4":{"Name":"Test.Rp1/testType1@2021-10-31","ScopeType":8,"Body":10}},{"2":{"Name":"FoosRequest","Properties":{"someString":{"Type":4,"Flags":1,"Description":"The foo request string"},"locationData":{"Type":25,"Flags":0,"Description":"Metadata pertaining to the geographic location of the resource."}}}},{"2":{"Name":"FoosResponse","Properties":{"someString":{"Type":4,"Flags":0,"Description":"The foo response string"}}}},{"8":{"Name":"listFoos","ResourceType":"Test.Rp1/testType1","ApiVersion":"2021-10-31","Output":40,"Input":39}},{"3":{"ItemType":40}},{"8":{"Name":"listArrayOfFoos","ResourceType":"Test.Rp1/testType1","ApiVersion":"2021-10-31","Output":42}}] \ No newline at end of file +[{"1":{"Kind":1}},{"1":{"Kind":2}},{"1":{"Kind":3}},{"1":{"Kind":4}},{"1":{"Kind":5}},{"1":{"Kind":6}},{"1":{"Kind":7}},{"1":{"Kind":8}},{"6":{"Value":"Test.Rp1/testType1"}},{"6":{"Value":"2021-10-31"}},{"2":{"Name":"Test.Rp1/testType1","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":8,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":9,"Flags":10,"Description":"The resource api version"},"properties":{"Type":11,"Flags":0,"Description":"The resource properties."},"tags":{"Type":26,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":27,"Flags":2,"Description":"Azure Resource Manager metadata containing createdBy and modifiedBy information."}}}},{"2":{"Name":"TestType1CreateOrUpdatePropertiesOrTestType1Properties","Properties":{"basicString":{"Type":4,"Flags":0,"Description":"Description for a basic string property."},"stringEnum":{"Type":14,"Flags":0,"Description":"Description for a basic enum property."},"skuTier":{"Type":19,"Flags":0,"Description":"This field is required to be implemented by the Resource Provider if the service has more than one tier, but is not required on a PUT."},"encryptionProperties":{"Type":20,"Flags":0,"Description":"TestType1 encryption properties"},"locationData":{"Type":25,"Flags":2,"Description":"Metadata pertaining to the geographic location of the resource."}}}},{"6":{"Value":"Foo"}},{"6":{"Value":"Bar"}},{"5":{"Elements":[12,13,4]}},{"6":{"Value":"Free"}},{"6":{"Value":"Basic"}},{"6":{"Value":"Standard"}},{"6":{"Value":"Premium"}},{"5":{"Elements":[15,16,17,18]}},{"2":{"Name":"EncryptionProperties","Properties":{"status":{"Type":23,"Flags":0,"Description":"Indicates whether or not the encryption is enabled for container registry."},"keyVaultProperties":{"Type":24,"Flags":0,"Description":"Key vault properties."}}}},{"6":{"Value":"enabled"}},{"6":{"Value":"disabled"}},{"5":{"Elements":[21,22,4]}},{"2":{"Name":"KeyVaultProperties","Properties":{"keyIdentifier":{"Type":4,"Flags":0,"Description":"Key vault uri to access the encryption key."},"identity":{"Type":4,"Flags":0,"Description":"The client ID of the identity which will be used to access key vault."}}}},{"2":{"Name":"LocationData","Properties":{"name":{"Type":4,"Flags":1,"Description":"A canonical name for the geographic or physical location."},"city":{"Type":4,"Flags":0,"Description":"The city or locality where the resource is located."},"district":{"Type":4,"Flags":0,"Description":"The district, state, or province where the resource is located."},"countryOrRegion":{"Type":4,"Flags":0,"Description":"The country or region where the resource is located"}}}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"SystemData","Properties":{"createdBy":{"Type":4,"Flags":0,"Description":"The identity that created the resource."},"createdByType":{"Type":32,"Flags":0,"Description":"The type of identity that created the resource."},"createdAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource creation (UTC)."},"lastModifiedBy":{"Type":4,"Flags":0,"Description":"The identity that last modified the resource."},"lastModifiedByType":{"Type":37,"Flags":0,"Description":"The type of identity that last modified the resource."},"lastModifiedAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource last modification (UTC)"}}}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[28,29,30,31,4]}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[33,34,35,36,4]}},{"4":{"Name":"Test.Rp1/testType1@2021-10-31","ScopeType":8,"Body":10,"Flags":0}},{"6":{"Value":"constantName"}},{"6":{"Value":"yetAnotherName"}},{"5":{"Elements":[39,40,4]}},{"6":{"Value":"Test.Rp1/splitPutAndGetType"}},{"6":{"Value":"2021-10-31"}},{"2":{"Name":"Test.Rp1/splitPutAndGetType","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":41,"Flags":9,"Description":"The resource name"},"type":{"Type":42,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":43,"Flags":10,"Description":"The resource api version"},"properties":{"Type":11,"Flags":0,"Description":"The resource properties."},"tags":{"Type":45,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":27,"Flags":2,"Description":"Azure Resource Manager metadata containing createdBy and modifiedBy information."}}}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Test.Rp1/splitPutAndGetType@2021-10-31","ScopeType":4,"Body":44,"Flags":0}},{"6":{"Value":"Test.Rp1/partlyReadonlyType"}},{"6":{"Value":"2021-10-31"}},{"2":{"Name":"Test.Rp1/partlyReadonlyType","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":47,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":48,"Flags":10,"Description":"The resource api version"},"properties":{"Type":50,"Flags":0},"tags":{"Type":59,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":27,"Flags":2,"Description":"Azure Resource Manager metadata containing createdBy and modifiedBy information."}}}},{"2":{"Name":"TestType1Properties","Properties":{"locationData":{"Type":25,"Flags":0,"Description":"Metadata pertaining to the geographic location of the resource."},"basicString":{"Type":4,"Flags":0,"Description":"Description for a basic string property."},"stringEnum":{"Type":53,"Flags":0,"Description":"Description for a basic enum property."},"skuTier":{"Type":58,"Flags":0,"Description":"This field is required to be implemented by the Resource Provider if the service has more than one tier, but is not required on a PUT."},"encryptionProperties":{"Type":20,"Flags":0,"Description":"TestType1 encryption properties"}}}},{"6":{"Value":"Foo"}},{"6":{"Value":"Bar"}},{"5":{"Elements":[51,52,4]}},{"6":{"Value":"Free"}},{"6":{"Value":"Basic"}},{"6":{"Value":"Standard"}},{"6":{"Value":"Premium"}},{"5":{"Elements":[54,55,56,57]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Test.Rp1/partlyReadonlyType@2021-10-31","ScopeType":9,"ReadOnlyScopes":1,"Body":49,"Flags":0}},{"6":{"Value":"Test.Rp1/readOnlyTestType"}},{"6":{"Value":"2021-10-31"}},{"2":{"Name":"Test.Rp1/readOnlyTestType","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":61,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":62,"Flags":10,"Description":"The resource api version"},"properties":{"Type":64,"Flags":2},"tags":{"Type":66,"Flags":2,"Description":"Resource tags."},"location":{"Type":4,"Flags":2,"Description":"The geo-location where the resource lives"},"systemData":{"Type":27,"Flags":2,"Description":"Azure Resource Manager metadata containing createdBy and modifiedBy information."}}}},{"2":{"Name":"ReadOnlyTestTypeProperties","Properties":{"plan":{"Type":65,"Flags":0,"Description":"Plan for the resource."}}}},{"2":{"Name":"Plan","Properties":{"name":{"Type":4,"Flags":1,"Description":"A user defined name of the 3rd Party Artifact that is being procured."},"publisher":{"Type":4,"Flags":1,"Description":"The publisher of the 3rd Party Artifact that is being bought. E.g. NewRelic"},"product":{"Type":4,"Flags":1,"Description":"The 3rd Party artifact that is being procured. E.g. NewRelic. Product maps to the OfferID specified for the artifact at the time of Data Market onboarding."},"promotionCode":{"Type":4,"Flags":0,"Description":"A publisher provided promotion code as provisioned in Data Market for the said product/artifact."},"version":{"Type":4,"Flags":0,"Description":"The version of the desired product/artifact."}}}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Test.Rp1/readOnlyTestType@2021-10-31","ScopeType":8,"Body":63,"Flags":1}},{"2":{"Name":"FoosRequest","Properties":{"someString":{"Type":4,"Flags":1,"Description":"The foo request string"},"locationData":{"Type":25,"Flags":0,"Description":"Metadata pertaining to the geographic location of the resource."}}}},{"2":{"Name":"FoosResponse","Properties":{"someString":{"Type":4,"Flags":0,"Description":"The foo response string"}}}},{"8":{"Name":"listFoos","ResourceType":"Test.Rp1/testType1","ApiVersion":"2021-10-31","Output":69,"Input":68}},{"3":{"ItemType":69}},{"8":{"Name":"listArrayOfFoos","ResourceType":"Test.Rp1/testType1","ApiVersion":"2021-10-31","Output":71}}] \ No newline at end of file diff --git a/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.md b/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.md index 80c5de4b08..b1916a11eb 100644 --- a/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.md +++ b/src/autorest.bicep/test/integration/generated/basic/test.rp1/2021-10-31/types.md @@ -1,5 +1,41 @@ # Test.Rp1 @ 2021-10-31 +## Resource Test.Rp1/partlyReadonlyType@2021-10-31 +* **Valid Scope(s)**: Tenant (ReadOnly), ResourceGroup +### Properties +* **apiVersion**: '2021-10-31' (ReadOnly, DeployTimeConstant): The resource api version +* **id**: string (ReadOnly, DeployTimeConstant): The resource id +* **location**: string (Required): The geo-location where the resource lives +* **name**: string (Required, DeployTimeConstant): The resource name +* **properties**: [TestType1Properties](#testtype1properties) +* **systemData**: [SystemData](#systemdata) (ReadOnly): Azure Resource Manager metadata containing createdBy and modifiedBy information. +* **tags**: [TrackedResourceTags](#trackedresourcetags): Resource tags. +* **type**: 'Test.Rp1/partlyReadonlyType' (ReadOnly, DeployTimeConstant): The resource type + +## Resource Test.Rp1/readOnlyTestType@2021-10-31 (ReadOnly) +* **Valid Scope(s)**: ResourceGroup +### Properties +* **apiVersion**: '2021-10-31' (ReadOnly, DeployTimeConstant): The resource api version +* **id**: string (ReadOnly, DeployTimeConstant): The resource id +* **location**: string (ReadOnly): The geo-location where the resource lives +* **name**: string (Required, DeployTimeConstant): The resource name +* **properties**: [ReadOnlyTestTypeProperties](#readonlytesttypeproperties) (ReadOnly) +* **systemData**: [SystemData](#systemdata) (ReadOnly): Azure Resource Manager metadata containing createdBy and modifiedBy information. +* **tags**: [TrackedResourceTags](#trackedresourcetags) (ReadOnly): Resource tags. +* **type**: 'Test.Rp1/readOnlyTestType' (ReadOnly, DeployTimeConstant): The resource type + +## Resource Test.Rp1/splitPutAndGetType@2021-10-31 +* **Valid Scope(s)**: Subscription +### Properties +* **apiVersion**: '2021-10-31' (ReadOnly, DeployTimeConstant): The resource api version +* **id**: string (ReadOnly, DeployTimeConstant): The resource id +* **location**: string (Required): The geo-location where the resource lives +* **name**: 'constantName' | 'yetAnotherName' | string (Required, DeployTimeConstant): The resource name +* **properties**: [TestType1CreateOrUpdatePropertiesOrTestType1Properties](#testtype1createorupdatepropertiesortesttype1properties): The resource properties. +* **systemData**: [SystemData](#systemdata) (ReadOnly): Azure Resource Manager metadata containing createdBy and modifiedBy information. +* **tags**: [TrackedResourceTags](#trackedresourcetags): Resource tags. +* **type**: 'Test.Rp1/splitPutAndGetType' (ReadOnly, DeployTimeConstant): The resource type + ## Resource Test.Rp1/testType1@2021-10-31 * **Valid Scope(s)**: ResourceGroup ### Properties @@ -53,6 +89,18 @@ * **district**: string: The district, state, or province where the resource is located. * **name**: string (Required): A canonical name for the geographic or physical location. +## Plan +### Properties +* **name**: string (Required): A user defined name of the 3rd Party Artifact that is being procured. +* **product**: string (Required): The 3rd Party artifact that is being procured. E.g. NewRelic. Product maps to the OfferID specified for the artifact at the time of Data Market onboarding. +* **promotionCode**: string: A publisher provided promotion code as provisioned in Data Market for the said product/artifact. +* **publisher**: string (Required): The publisher of the 3rd Party Artifact that is being bought. E.g. NewRelic +* **version**: string: The version of the desired product/artifact. + +## ReadOnlyTestTypeProperties +### Properties +* **plan**: [Plan](#plan): Plan for the resource. + ## SystemData ### Properties * **createdAt**: string: The timestamp of resource creation (UTC). @@ -70,6 +118,29 @@ * **skuTier**: 'Basic' | 'Free' | 'Premium' | 'Standard': This field is required to be implemented by the Resource Provider if the service has more than one tier, but is not required on a PUT. * **stringEnum**: 'Bar' | 'Foo' | string: Description for a basic enum property. +## TestType1Properties +### Properties +* **basicString**: string: Description for a basic string property. +* **encryptionProperties**: [EncryptionProperties](#encryptionproperties): TestType1 encryption properties +* **locationData**: [LocationData](#locationdata): Metadata pertaining to the geographic location of the resource. +* **skuTier**: 'Basic' | 'Free' | 'Premium' | 'Standard': This field is required to be implemented by the Resource Provider if the service has more than one tier, but is not required on a PUT. +* **stringEnum**: 'Bar' | 'Foo' | string: Description for a basic enum property. + +## TrackedResourceTags +### Properties +### Additional Properties +* **Additional Properties Type**: string + +## TrackedResourceTags +### Properties +### Additional Properties +* **Additional Properties Type**: string + +## TrackedResourceTags +### Properties +### Additional Properties +* **Additional Properties Type**: string + ## TrackedResourceTags ### Properties ### Additional Properties diff --git a/src/autorest.bicep/test/integration/specs/basic/resource-manager/Test.Rp1/stable/2021-10-31/spec.json b/src/autorest.bicep/test/integration/specs/basic/resource-manager/Test.Rp1/stable/2021-10-31/spec.json index 101f713c5d..62bd478fab 100644 --- a/src/autorest.bicep/test/integration/specs/basic/resource-manager/Test.Rp1/stable/2021-10-31/spec.json +++ b/src/autorest.bicep/test/integration/specs/basic/resource-manager/Test.Rp1/stable/2021-10-31/spec.json @@ -107,6 +107,23 @@ } } }, + "ReadOnlyTestType": { + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/TrackedResource" + } + ], + "properties": { + "properties": { + "type": "object", + "properties": { + "plan": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/Plan" + } + } + } + } + }, "FoosResponse": { "properties": { "someString": { @@ -330,6 +347,245 @@ } ] } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Test.Rp1/readOnlyTestType/{typeName}": { + "get": { + "summary": "Get a readOnlyTestType resource", + "description": "Get a readOnlyTestType resource", + "operationId": "ReadOnlyTestType_Get", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ReadOnlyTestType" + } + }, + "default": { + "description": "Detailed error information.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "typeName", + "in": "path", + "required": true, + "type": "string", + "description": "The readOnlyTestType resource name." + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ] + } + }, + "/subscriptions/{subscriptionId}/providers/Test.Rp1/splitPutAndGetType/{typeName}": { + "get": { + "summary": "Get a splitPutAndGetType resource", + "description": "Get a splitPutAndGetType resource", + "operationId": "SplitPutAndGetType_Get", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/TestType1" + } + }, + "default": { + "description": "Detailed error information.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/SubscriptionIdParameter" + }, + { + "name": "typeName", + "in": "path", + "required": true, + "type": "string", + "description": "The splitPutAndGetType resource name.", + "enum": [ + "constantName", + "yetAnotherName" + ], + "x-ms-enum": { + "name": "SplitPutAndGetType_Name", + "modelAsString": true + } + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ] + } + }, + "/subscriptions/{subscriptionId}/providers/Test.Rp1/splitPutAndGetType/constantName": { + "put": { + "summary": "Create or update a splitPutAndGetType resource", + "description": "Create or update a splitPutAndGetType resource", + "operationId": "SplitPutAndGetType_CreateOrUpdate", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/TestType1" + } + }, + "default": { + "description": "Detailed error information.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/SubscriptionIdParameter" + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TestType1Input" + }, + "description": "The request parameters" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ] + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Test.Rp1/partlyReadonlyType/{typeName}": { + "put": { + "summary": "Create or update a partlyReadonlyType resource", + "description": "Create or update a partlyReadonlyType resource", + "operationId": "PartlyReadonlyType_CreateOrUpdate", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/TestType1" + } + }, + "default": { + "description": "Detailed error information.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "typeName", + "in": "path", + "required": true, + "type": "string", + "description": "The partlyReadonlyType resource name." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TestType1" + }, + "description": "The request parameters" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ] + }, + "get": { + "summary": "Get a partlyReadonlyType resource", + "description": "Get a partlyReadonlyType resource", + "operationId": "PartlyReadonlyType_Get", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/TestType1" + } + }, + "default": { + "description": "Detailed error information.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "typeName", + "in": "path", + "required": true, + "type": "string", + "description": "The partlyReadonlyType resource name." + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ] + } + }, + "/providers/Test.Rp1/partlyReadonlyType/{typeName}": { + "get": { + "summary": "Get a partlyReadonlyType resource at the tenant level", + "description": "Get a partlyReadonlyType resource at the tenant level", + "operationId": "PartlyReadonlyType_GetAtTenant", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/TestType1" + } + }, + "default": { + "description": "Detailed error information.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse" + } + } + }, + "parameters": [ + { + "name": "typeName", + "in": "path", + "required": true, + "type": "string", + "description": "The partlyReadonlyType resource name." + }, + { + "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter" + } + ] + } } } } diff --git a/version.json b/version.json index 18a9a6d189..65f7ba8ceb 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.1", + "version": "0.2", "cloudBuild": { "setVersionVariables": false }