diff --git a/powershell/autorest-configuration.md b/powershell/autorest-configuration.md index 79134309a6..5d5c8d8ec6 100644 --- a/powershell/autorest-configuration.md +++ b/powershell/autorest-configuration.md @@ -121,9 +121,12 @@ pipeline: # --- extension powershell based on modelerfour tweakcodemodel-v2: input: modelerfour/identity + + tweakcodemodelazure-v2: + input: tweakcodemodel-v2 create-commands-v2: - input: tweakcodemodel-v2 + input: tweakcodemodelazure-v2 create-virtual-properties-v2: input: create-commands-v2 diff --git a/powershell/cmdlets/class.ts b/powershell/cmdlets/class.ts index 42a8a6bf3c..cf021f9190 100644 --- a/powershell/cmdlets/class.ts +++ b/powershell/cmdlets/class.ts @@ -1837,7 +1837,7 @@ export class NewCmdletClass extends Class { name: p.language.csharp?.name, param: values($this.properties). where(each => each.metadata.parameterDefinition). - first(each => each.metadata.parameterDefinition.details.csharp.uid === p.language.csharp?.uid), + first(each => each.metadata.parameterDefinition.schema === p.schema), isPathParam: $this.isViaIdentity && p.protocol.http?.in === ParameterLocation.Path }; @@ -2576,7 +2576,7 @@ export class NewCmdletClass extends Class { regularCmdletParameter.add(new Attribute(AllowEmptyCollectionAttribute)); } - NewAddInfoAttribute(regularCmdletParameter, propertyType, vParam.required, false, vParam.description, vParam.origin.name); + NewAddInfoAttribute(regularCmdletParameter, propertyType, vParam.required, false, vParam.description, origin.details.default.name); NewAddCompleterInfo(regularCmdletParameter, vParam); addDefaultInfo(regularCmdletParameter, vParam); @@ -2586,16 +2586,15 @@ export class NewCmdletClass extends Class { } const httpParam = origin.details.csharp.httpParameter; - const uid = httpParam ? httpParam.details.csharp.uid : 'no-parameter'; + //const uid = httpParam ? httpParam.details.csharp.uid : 'no-parameter'; - // skip-for-time-being - // const cat = values(operation.callGraph[0].parameters). - // where(each => !(each.details.csharp.constantValue)). - // first(each => each.details.csharp.uid === uid); + const cat = values(operation.callGraph[0].parameters). + where(each => !(each.language.csharp?.constantValue)). + first(each => each.schema === httpParam.schema); - // if (cat) { - // regularCmdletParameter.add(new Attribute(CategoryAttribute, { parameters: [`${ParameterCategory}.${pascalCase(cat.in)}`] })); - // } + if (cat) { + regularCmdletParameter.add(new Attribute(CategoryAttribute, { parameters: [`${ParameterCategory}.${pascalCase((cat.protocol.http?.in))}`] })); + } if (origin.details.csharp.completer) { diff --git a/powershell/cmdlets/namespace.ts b/powershell/cmdlets/namespace.ts index 96e05ae6ff..b215bb3eb7 100644 --- a/powershell/cmdlets/namespace.ts +++ b/powershell/cmdlets/namespace.ts @@ -55,7 +55,6 @@ export class NewCmdletNamespace extends Namespace { if (this.state.project.azure && operation.details.csharp.verb === 'Set' && operation.details.csharp.name.indexOf('ViaIdentity') > 0) { continue; } - // skip-for-time-being this.addClass(await new NewCmdletClass(this, operation, this.state.path('commands', 'operations', index)).init()); } return this; diff --git a/powershell/llcsharp/model/interface.ts b/powershell/llcsharp/model/interface.ts index e09576d596..a4d8d61fd5 100644 --- a/powershell/llcsharp/model/interface.ts +++ b/powershell/llcsharp/model/interface.ts @@ -8,13 +8,14 @@ import { KnownMediaType, JsonType, getPolymorphicBases } from '@azure-tools/code import { Expression, ExpressionOrLiteral, Interface, Namespace, OneOrMoreStatements, Variable, Access, InterfaceProperty, Attribute, StringExpression, LiteralExpression, Property, TypeDeclaration } from '@azure-tools/codegen-csharp'; import { ClientRuntime } from '../clientruntime'; import { Schema } from '../code-model'; -import { Schema as NewSchema, Language } from '@azure-tools/codemodel'; +import { Schema as NewSchema, Language, ObjectSchema } from '@azure-tools/codemodel'; import { State, NewState } from '../generator'; import { EnhancedTypeDeclaration, NewEnhancedTypeDeclaration } from '../schema/extended-type-declaration'; import { ModelClass, NewModelClass } from './model-class'; import { TypeContainer } from '@azure-tools/codegen-csharp'; import { DeepPartial } from '@azure-tools/codegen'; import { values } from '@azure-tools/linq'; +import { VirtualProperty as NewVirtualProperty, VirtualProperties as NewVirtualProperties } from '../../utils/schema' export function addInfoAttribute(targetProperty: Property, pType: TypeDeclaration, isRequired: boolean, isReadOnly: boolean, description: string, serializedName: string) { @@ -70,6 +71,58 @@ export function addInfoAttribute(targetProperty: Property, pType: TypeDeclaratio })); } +export function newAddInfoAttribute(targetProperty: Property, pType: TypeDeclaration, isRequired: boolean, isReadOnly: boolean, description: string, serializedName: string) { + + let pt = pType; + while (pt.elementType) { + switch (pt.elementType.schema.type) { + case JsonType.Object: + if (pt.elementType.schema.language.csharp.interfaceImplementation) { + pt = { + declaration: pt.elementType.schema.language.csharp.interfaceImplementation.declaration, + schema: pt.elementType.schema, + }; + } else { + // arg! it's not done yet. Hope it's not polymorphic itself. + pt = { + declaration: `${pt.elementType.schema.language.csharp.namespace}.${pt.elementType.schema.language.csharp.interfaceName}`, + schema: pt.elementType.schema, + }; + } + break; + + case JsonType.Array: + pt = pt.elementType; + break; + + default: + pt = pt.elementType; + break; + } + } + const ptypes = new Array(); + + if (pt.schema && pt.schema.language.csharp.byReference) { + ptypes.push(`typeof(${pt.schema.language.csharp.namespace}.${pt.schema.language.csharp.interfaceName}_Reference)`); + // do we need polymorphic types for by-resource ? Don't think so. + } else { + ptypes.push(`typeof(${pt.declaration})`); + if (pt.schema && pt.schema.language.csharp.classImplementation && pt.schema.language.csharp.classImplementation.discriminators) { + ptypes.push(...[...pt.schema.language.csharp.classImplementation.discriminators.values()].map(each => `typeof(${each.modelInterface.fullName})`)); + } + } + + targetProperty.add(new Attribute(ClientRuntime.InfoAttribute, { + parameters: [ + new LiteralExpression(`\nRequired = ${isRequired}`), + new LiteralExpression(`\nReadOnly = ${isReadOnly}`), + new LiteralExpression(`\nDescription = ${new StringExpression(description ?? '').value}`), + new LiteralExpression(`\nSerializedName = ${new StringExpression(serializedName).value}`), + new LiteralExpression(`\nPossibleTypes = new [] { ${ptypes.join(',').replace(/\?/g, '').replace(/undefined\./g, '')} }`), + ] + })); +} + export class ModelInterface extends Interface implements EnhancedTypeDeclaration { get schema(): Schema { @@ -302,58 +355,58 @@ export class NewModelInterface extends Interface implements NewEnhancedTypeDecla //implData.interfaceImplementation = this; this.description = `${this.schema.language.csharp.description}`; - const virtualProperties = this.schema.language.csharp.virtualProperties || { + const virtualProperties: NewVirtualProperties = this.schema.language.csharp.virtualProperties || { owned: [], inherited: [], inlined: [] }; // skip-for-time-being - // if (this.schema.language.csharp.virtualProperties) { + if (this.schema.language.csharp.virtualProperties) { - // for (const virtualProperty of values(virtualProperties.owned)) { - // if (virtualProperty.private && !this.isInternal) { - // continue; - // } + for (const virtualProperty of values(virtualProperties.owned)) { + if (virtualProperty.private && !this.isInternal) { + continue; + } - // const modelProperty = virtualProperty.property; + const modelProperty = virtualProperty.property; - // const internalSet = !!(!this.isInternal && (modelProperty.details.csharp.readOnly || modelProperty.details.csharp.constantValue)); + const internalSet = !!(!this.isInternal && (modelProperty.readOnly || (modelProperty.language.csharp).constantValue)); - // const isRequired = !!modelProperty.details.csharp.required; - // const pType = this.state.project.modelsNamespace.resolveTypeDeclaration(modelProperty.schema, isRequired, this.state.path('schema')); - // const p = this.add(new InterfaceProperty(virtualProperty.name, pType, { - // description: modelProperty.details.csharp.description, - // setAccess: internalSet ? Access.Internal : Access.Public - // })); + const isRequired = !!modelProperty.required; + const pType = this.state.project.modelsNamespace.NewResolveTypeDeclaration(modelProperty.schema, isRequired, this.state.path('schema')); + const p = this.add(new InterfaceProperty(virtualProperty.name, pType, { + description: modelProperty.language.default.description, + setAccess: internalSet ? Access.Internal : Access.Public + })); - // this.addInfoAttribute(p, pType, isRequired, internalSet, modelProperty.details.csharp.description, modelProperty.serializedName); + this.addInfoAttribute(p, pType, isRequired, internalSet, modelProperty.language.default.description, modelProperty.serializedName); - // if (!this.isInternal && modelProperty.details.csharp.constantValue !== undefined) { - // p.setAccess = Access.Internal; - // } - // } + if (!this.isInternal && (modelProperty.language.csharp).constantValue !== undefined) { + p.setAccess = Access.Internal; + } + } - // for (const virtualProperty of values(virtualProperties.inlined)) { + for (const virtualProperty of values(virtualProperties.inlined)) { - // // don't publicly expose the 'private' properties. - // if (virtualProperty.private && !this.isInternal) { - // continue; - // } + // don't publicly expose the 'private' properties. + if (virtualProperty.private && !this.isInternal) { + continue; + } - // const modelProperty = virtualProperty.property; - // const isRequired = !!modelProperty.details.csharp.required; - // const pType = this.state.project.modelsNamespace.resolveTypeDeclaration(modelProperty.schema, isRequired, this.state.path('schema')); + const modelProperty = virtualProperty.property; + const isRequired = !!modelProperty.required; + const pType = this.state.project.modelsNamespace.NewResolveTypeDeclaration(modelProperty.schema, isRequired, this.state.path('schema')); - // const internalSet = !!(!this.isInternal && (modelProperty.details.csharp.readOnly || modelProperty.details.csharp.constantValue)); + const internalSet = !!(!this.isInternal && (modelProperty.readOnly || (modelProperty.language.csharp).constantValue)); - // const p = this.add(new InterfaceProperty(virtualProperty.name, pType, { - // description: modelProperty.details.csharp.description, - // setAccess: internalSet ? Access.Internal : Access.Public - // })); - // this.addInfoAttribute(p, pType, isRequired, internalSet, modelProperty.details.csharp.description, modelProperty.serializedName); + const p = this.add(new InterfaceProperty(virtualProperty.name, pType, { + description: modelProperty.language.default.description, + setAccess: internalSet ? Access.Internal : Access.Public + })); + this.addInfoAttribute(p, pType, isRequired, internalSet, modelProperty.language.default.description, modelProperty.serializedName); - // } - // } + } + } if (!this.isInternal) { // mark it as json serializable @@ -371,7 +424,7 @@ export class NewModelInterface extends Interface implements NewEnhancedTypeDecla addInfoAttribute(p: Property, pType: TypeDeclaration, isRequired: boolean, internalSet: boolean, description: string, serializedName: string) { if (!this.isInternal) { - return addInfoAttribute(p, pType, isRequired, internalSet, description, serializedName); + return newAddInfoAttribute(p, pType, isRequired, internalSet, description, serializedName); } } } diff --git a/powershell/llcsharp/model/model-class.ts b/powershell/llcsharp/model/model-class.ts index 904c040605..e528249edd 100644 --- a/powershell/llcsharp/model/model-class.ts +++ b/powershell/llcsharp/model/model-class.ts @@ -18,7 +18,6 @@ import { PropertyOriginAttribute, DoNotFormatAttribute, FormatTableAttribute } f import { Schema } from '../code-model'; import { DictionaryImplementation } from './model-class-dictionary'; import { Languages, Language, Schema as NewSchema } from '@azure-tools/codemodel'; - import { VirtualProperty as NewVirtualProperty } from '../../utils/schema'; export function getVirtualPropertyName(vp?: VirtualProperty): string { diff --git a/powershell/llcsharp/operation/method.ts b/powershell/llcsharp/operation/method.ts index 58a7c33895..35f9f09ed7 100644 --- a/powershell/llcsharp/operation/method.ts +++ b/powershell/llcsharp/operation/method.ts @@ -37,6 +37,11 @@ function removeEncoding(pp: OperationParameter, paramName: string, kmt: KnownMed return pp.param.extensions && pp.param.extensions['x-ms-skip-url-encoding'] ? up.replace(/global::System.Uri.EscapeDataString|System.Uri.EscapeDataString/g, '') : up; } +function newRemoveEncoding(pp: NewOperationParameter, paramName: string, kmt: KnownMediaType): string { + const up = pp.typeDeclaration.serializeToNode(kmt, pp, paramName, ClientRuntime.SerializationMode.None).value; + return pp.param.extensions && pp.param.extensions['x-ms-skip-url-encoding'] ? up.replace(/global::System.Uri.EscapeDataString|System.Uri.EscapeDataString/g, '') : up; +} + export class EventListener { constructor(protected expression: Expression, protected emitSignals: boolean) { @@ -371,6 +376,7 @@ export class NewOperationMethod extends Method { this.senderParameter = this.addParameter(new Parameter('sender', ClientRuntime.ISendAsync, { description: `an instance of an ${ClientRuntime.ISendAsync} pipeline to use to make the request.` })); let rx = this.operation.requests ? this.operation.requests[0].protocol.http?.path : ''; + const path = rx; // For post API, Some URI may contain an action string .e.x '/start' at the end // of the URI, for such cases, we will drop the action string if identityCorrection // is set in the configuration @@ -399,19 +405,19 @@ export class NewOperationMethod extends Method { // + "`); // } - // for (const pp of pathParams) { - // rx = rx.replace(`{${pp.param.name}}`, `(?<${pp.param.name}>[^/]+)`); + for (const pp of pathParams) { + rx = rx.replace(`{${pp.param.language.default.name}}`, `(?<${pp.param.language.default.name}>[^/]+)`); - // if (this.viaIdentity) { - // url = url.replace(`{${pp.param.name}}`, `" - // + ${pp.name} - // + "`); - // } else { - // url = url.replace(`{${pp.param.name}}`, `" - // + ${removeEncoding(pp, '', KnownMediaType.UriParameter)} - // + "`); - // } - // } + if (this.viaIdentity) { + url = url.replace(`{${pp.param.language.default.name}}`, `" + + ${pp.name} + + "`); + } else { + url = url.replace(`{${pp.param.language.default.name}}`, `" + + ${newRemoveEncoding(pp, '', KnownMediaType.UriParameter)} + + "`); + } + } rx = `"^${rx}$"`; url = url.replace(/\s*\+ ""/gm, ''); @@ -429,30 +435,20 @@ export class NewOperationMethod extends Method { const match = Local('_match', `${System.Text.RegularExpressions.Regex.new(rx).value}.Match(${identity.value})`); yield match.declarationStatement; - yield If(`!${match}.Success`, `throw new global::System.Exception("Invalid identity for URI '${rx}'");`); + yield If(`!${match}.Success`, `throw new global::System.Exception("Invalid identity for URI '${path}'");`); yield EOL; yield '// replace URI parameters with values from identity'; - // skip-for-time-being - // for (const pp of pathParams) { - // yield `var ${pp.name} = ${match.value}.Groups["${pp.param.name}"].Value;`; - // } + for (const pp of pathParams) { + yield `var ${pp.name} = ${match.value}.Groups["${pp.param.language.default.name}"].Value;`; + } } yield '// construct URL'; - // const urlV = new LocalVariable('_url', dotnet.Var, { - // initializer: System.Uri.new(`${System.Text.RegularExpressions.Regex.declaration}.Replace( - // "${url}" - // ${queryParams.length > 0 ? '+ "?"' : ''}${queryParams.joinWith(pp => ` - // + ${removeEncoding(pp, pp.param.name, KnownMediaType.QueryParameter)}`, ` - // + "&"` - // )} - // ,"\\\\?&*$|&*$|(\\\\?)&+|(&)&+","$1$2")`.replace(/\s*\+ ""/gm, '')) - // }); const urlV = new LocalVariable('_url', dotnet.Var, { initializer: System.Uri.new(`${System.Text.RegularExpressions.Regex.declaration}.Replace( "${url}" ${queryParams.length > 0 ? '+ "?"' : ''}${queryParams.joinWith(pp => ` - + ${''}`, ` + + ${newRemoveEncoding(pp, pp.param.language.default.name, KnownMediaType.QueryParameter)}`, ` + "&"` )} ,"\\\\?&*$|&*$|(\\\\?)&+|(&)&+","$1$2")`.replace(/\s*\+ ""/gm, '')) diff --git a/powershell/main.ts b/powershell/main.ts index a338bc235c..9181414f78 100644 --- a/powershell/main.ts +++ b/powershell/main.ts @@ -13,7 +13,7 @@ import { csnamer } from './plugins/cs-namer'; import { llcsharp } from './plugins/llcsharp'; import { createInlinedPropertiesPlugin } from './plugins/plugin-create-inline-properties'; import { tweakModelPlugin } from './plugins/plugin-tweak-model'; -import { tweakModelAzurePlugin } from './plugins/plugin-tweak-model-azure'; +import { tweakModelAzurePluginV2 } from './plugins/plugin-tweak-model-azure-v2'; import { createCommandsV2 } from './plugins/create-commands-v2'; import { csnamerV2 } from './plugins/cs-namer-v2'; import { namerV2 } from './plugins/ps-namer-v2'; @@ -33,7 +33,7 @@ export async function main() { pluginHost.Add('llcsharp', llcsharp); // Following are plugins moved from remodeler pluginHost.Add('tweakcodemodel-v2', tweakModelPlugin); - pluginHost.Add('tweakcodemodelazure-v2', tweakModelAzurePlugin); + pluginHost.Add('tweakcodemodelazure-v2', tweakModelAzurePluginV2); pluginHost.Add('create-virtual-properties-v2', createInlinedPropertiesPlugin); pluginHost.Add('create-commands-v2', createCommandsV2); pluginHost.Add('csnamer-v2', csnamerV2); diff --git a/powershell/package.json b/powershell/package.json index 891838f603..62b74a21d7 100644 --- a/powershell/package.json +++ b/powershell/package.json @@ -66,4 +66,4 @@ "source-map-support": "0.5.13", "xmlbuilder": "10.1.1" } -} \ No newline at end of file +} diff --git a/powershell/plugins/llcsharp-v2.ts b/powershell/plugins/llcsharp-v2.ts index 802f820fc7..a5c14fed49 100644 --- a/powershell/plugins/llcsharp-v2.ts +++ b/powershell/plugins/llcsharp-v2.ts @@ -10,6 +10,7 @@ import { Model } from '../llcsharp/code-model'; import { State } from '../llcsharp/generator'; import { Project, NewProject } from '../llcsharp/project'; import { PwshModel } from '../utils/PwshModel'; +import { Dictionary } from '@azure-tools/linq'; const resources = `${__dirname}/../../resources`; diff --git a/powershell/plugins/llcsharp.ts b/powershell/plugins/llcsharp.ts index a166a31bc6..0d84bd2579 100644 --- a/powershell/plugins/llcsharp.ts +++ b/powershell/plugins/llcsharp.ts @@ -8,14 +8,25 @@ import { join } from 'path'; import { Model } from '../llcsharp/code-model'; import { State } from '../llcsharp/generator'; import { Project } from '../llcsharp/project'; +import { Dictionary } from '@azure-tools/linq'; const resources = `${__dirname}/../../resources`; +function outerTest(service: Host, project: Project, fname: string, content: string) { + service.WriteFile(join(project.apifolder, fname), test(content, project.overrides), undefined, 'source-file-csharp'); +} + +function test(content: string, overrides: Dictionary): string { + const a = applyOverrides(content, overrides); + return a; +} + export async function llcsharp(service: Host) { try { const project = await new Project(service).init(); - await project.writeFiles(async (fname, content) => service.WriteFile(join(project.apifolder, fname), applyOverrides(content, project.overrides), undefined, 'source-file-csharp')); + await project.writeFiles(async (fname, content) => service.WriteFile(join(project.apifolder, fname), test(content, project.overrides), undefined, 'source-file-csharp')); + await project.writeFiles(async (fname, content) => outerTest(service, project, fname, content)); // recursive copy resources await copyResources(join(resources, 'runtime', 'csharp', 'client'), async (fname, content) => service.WriteFile(join(project.runtimefolder, fname), content, undefined, 'source-file-csharp'), project.overrides); diff --git a/powershell/plugins/plugin-create-inline-properties.ts b/powershell/plugins/plugin-create-inline-properties.ts index 90cd2f1bd7..7ed0a20201 100644 --- a/powershell/plugins/plugin-create-inline-properties.ts +++ b/powershell/plugins/plugin-create-inline-properties.ts @@ -355,6 +355,7 @@ function createVirtualParameters(operation: CommandOperation) { operation.details.default.virtualParameters = virtualParameters; } + async function createVirtuals(state: State): Promise { /* A model class should provide inlined properties for anything in a property called properties @@ -363,16 +364,11 @@ async function createVirtuals(state: State): Promise { Individual models can change the $THRESHOLD for generate */ - // dolauli this plugin is used to expand the property in an object. - // dolauli inline-threshold could be set in readme.md - // skip-for-time-being const threshold = await state.getValue('inlining-threshold', 24); const conflicts = new Array(); for (const schema of values(state.model.schemas.objects)) { - - // did we already inline this object - // dolauli skip if inlined + // did we already inline this objecct if (schema.language.default.inlined) { continue; } diff --git a/powershell/plugins/plugin-tweak-model-azure-v2.ts b/powershell/plugins/plugin-tweak-model-azure-v2.ts new file mode 100644 index 0000000000..6be6d79808 --- /dev/null +++ b/powershell/plugins/plugin-tweak-model-azure-v2.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { keys, length, values } from '@azure-tools/linq'; + +import { Channel, Host } from '@azure-tools/autorest-extension-base'; +import { NewModelState } from '../utils/model-state'; +import { PwshModel } from '../utils/PwshModel'; +import { getAllProperties, ObjectSchema, Response, SchemaType, Schema } from '@azure-tools/codemodel'; +import { serialize } from '@azure-tools/codegen'; +type State = NewModelState; + +const xmsPageable = 'x-ms-pageable'; + +async function tweakModel(state: State): Promise { + const model = state.model; + + // service.Message({ Channel: Channel.Debug, Text: "THIS IS THE AZURE TWEAKER" }); + + // TODO: + // look at models, and extract out any case that has an IRESOURCE, IPROXYRESOURCE, etc. + // and use the common versions of those models. + + // Is the result marked x-ms-pagable? + // identify the next link (null means just get the results as an array) + // if nextLinkName is null, then it won't actually page, but we'd like to unroll the contents anyway. + for (const group of values(model.operationGroups)) { + for (const operation of values(group.operations)) { + if (operation.extensions && operation.extensions[xmsPageable]) { + // it's marked pagable. + operation.language.default.pageable = { + responseType: 'pageable', + nextLinkName: operation.extensions[xmsPageable].nextLinkName || undefined, + itemName: operation.extensions[xmsPageable].itemName || 'value', + operationName: operation.extensions[xmsPageable].operationName || `${operation.language.default.name}Next`, + }; + continue; + } + + // let's just check to see if it looks like it's supposed to be a collection + for (const response of values(operation.responses)) { + // does the response have a schema? + // TODO: check schema + if (getSchema(response)) { + const schema = getSchema(response); + + // is this just an array response? + if (schema.type === SchemaType.Array) { + operation.language.default.pageable = { + responseType: 'array', + }; + continue; + } + + // if it returns an object, let's see what's inside... + if (schema.type === SchemaType.Object) { + const objSchema = schema as ObjectSchema; + + // does it have a single member that is an array (ie, value : [...]) + if (length(objSchema.properties) === 1 && !objSchema.parents) { + const property = objSchema.properties?.[0]; + if (property) { + if (property.schema.type === SchemaType.Array) { + // nested array! + operation.language.default.pageable = { + responseType: 'nested-array', + itemName: property.serializedName, + }; + } + continue; + } + } + + // xichen: If response schema has only 2 properties and one of it is nextLink, the other is array + + // does it kinda look like a x-ms-pagable (value/nextlink?) + if (length(objSchema.properties) === 2 && !objSchema.parents) { + const hasNextLink = objSchema.properties?.some((prop) => prop.serializedName === 'nextLink'); + if (hasNextLink) { + const property = objSchema.properties?.find((prop) => prop.serializedName !== 'nextLink'); + if (property) { + if (property.schema.type === SchemaType.Array) { + // nested array! + operation.language.default.pageable = { + responseType: 'nested-array', + itemName: property.serializedName, + nextLinkName: 'nextLink' + }; + } + continue; + } + } + + } + + } + } + } + } + } + + + // make sure that all operations with lro have an options block. + for (const group of values(model.operationGroups)) { + for (const operation of values(group.operations)) { + if (operation.extensions && operation.extensions['x-ms-long-running-operation']) { + operation.language.default.asjob = true; + + operation.language.default.lro = operation.extensions['x-ms-long-running-operation-options'] || { + 'final-state-via': 'default' + }; + + // LRO 201 and 202 responses are handled internally, so remove any 201/202 responses in the operation + // delete operation.responses['201']; + // delete operation.responses['202']; + } + } + } + + // xichen: Cannot find 'x-ms-metadata' from swagger repo. Are we still using it? + + // Api Version parameter handling for Azure. + // if there is only a single api-version for the operation, let's just make it a constant + // otherwise, we need to make it selectable, but default to the 'latest' version there is. + // for (const group of values(model.operationGroups)) { + // for (const operation of values(group.operations)) { + // const apiVersions = operation.pathExtensions && operation.pathExtensions['x-ms-metadata'] ? operation.pathExtensions['x-ms-metadata'].apiVersions : undefined; + // for (const parameter of values(operation.parameters)) { + + // if (parameter.name === 'api-version') { + // // only set it if it hasn't been set yet. + // // if (parameter.details.default.constantValue) { + // //continue; + // //} + + // if (apiVersions) { + // // set the constant value to the first one + // if (length(apiVersions) === 1) { + // parameter.details.default.constantValue = apiVersions[0]; + // continue; + // } + + // // otherwise, the parameter can't have a constant value + // parameter.details.default.constantValue = undefined; + + // // mark it so that we can add profile support in the method generation + // parameter.details.default.apiversion = true; + // } + // } + // } + // } + // } + + // when make-sub-resources-byreference is specified, mark models with a writable id as byref. + if (await state.getValue('azure', false) && await state.getValue('make-sub-resources-byreference', false)) { + + for (const schema of values(model.schemas.objects ?? [])) { + // find schemas that have an 'id' and are not readonly + if (values(getAllProperties(schema)).any(prop => prop.serializedName === 'id' && !prop.language.default.readOnly)) { + + // look thru the operations, and the PUT methods + for (const group of model.operationGroups) { + for (const op of values(group.operations)) { + for (const request of op.requests ?? []) { + + if (request.protocol.http?.method === 'put') { + for (const response of op.responses ?? []) { + // see if any of the responses have the same schema as we are looking for + if (getSchema(response) === schema) { + // tell it not to inline that + schema.language.default.byReference = true; + break; + } + } + break; + } + + } + } + } + } + } + } + + return model; +} + +function getSchema(response: Response): Schema { + return (response).schema; +} + +// Azure version - +// Additional tweaks the code model to adjust things so that the code will generate better. + +export async function tweakModelAzurePluginV2(service: Host) { + const state = await new NewModelState(service).init(); + await service.WriteFile('code-model-v4-tweakcodemodelazure-v2.yaml', serialize(await tweakModel(state)), undefined, 'code-model-v4'); +} diff --git a/powershell/plugins/plugin-tweak-model.ts b/powershell/plugins/plugin-tweak-model.ts index 7712068e4b..2638fb1c93 100644 --- a/powershell/plugins/plugin-tweak-model.ts +++ b/powershell/plugins/plugin-tweak-model.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Property, codeModelSchema, CodeModel, Schema, StringSchema, ObjectSchema, GroupSchema, isObjectSchema, SchemaType, GroupProperty, ParameterLocation, Operation, Parameter, VirtualParameter, getAllProperties, ImplementationLocation, OperationGroup, Request, SchemaContext } from '@azure-tools/codemodel'; +import { Property, codeModelSchema, CodeModel, Schema, StringSchema, ObjectSchema, GroupSchema, isObjectSchema, SchemaType, GroupProperty, ParameterLocation, Operation, Parameter, VirtualParameter, getAllProperties, ImplementationLocation, OperationGroup, Request, SchemaContext, ChoiceSchema } from '@azure-tools/codemodel'; //import { ModelState } from '@azure-tools/codemodel-v3'; //import { KnownMediaType, knownMediaType, ParameterLocation, getPolymorphicBases, isSchemaObject, JsonType, Property, Schema, processCodeModel, StringFormat, codemodel, ModelState } from '@azure-tools/codemodel-v3'; import { pascalCase, deconstruct, fixLeadingNumber, serialize } from '@azure-tools/codegen'; @@ -75,86 +75,108 @@ async function tweakModelV2(state: State): Promise { state.setValue('service-name', serviceName); const model = state.model; + const schemas = model.schemas; + const allSchemas: Schema[] = []; + for (const prop of Object.values(schemas)) { + if (Array.isArray(prop) && prop.length > 0 && prop[0] instanceof Schema) { + allSchemas.push(...prop); + } + } + + // const universalId = new ObjectSchema(`${serviceName}Identity`, 'Resource Identity'); + // universalId.apiVersions = universalId.apiVersions || []; + // state.model.schemas.objects = state.model.schemas.objects || []; + // state.model.schemas.objects.push(universalId); + + model.commands = { + operations: new Dictionary(), + parameters: new Dictionary(), + }; + // we're going to create a schema that represents the distinct sum + // of all operation PATH parameters const universalId = new ObjectSchema(`${serviceName}Identity`, 'Resource Identity'); universalId.apiVersions = universalId.apiVersions || []; state.model.schemas.objects = state.model.schemas.objects || []; - (universalId.language.default).uid = true; + state.model.schemas.objects.push(universalId); - universalId.properties = universalId.properties || new Array(); - const urlParam = new Set(); - for (const operationGroup of values(model.operationGroups)) { - for (const operation of values(operationGroup.operations)) { - for (const param of values(operation.parameters).where(each => each.protocol.http?.in === ParameterLocation.Path)) { + for (const group of values(model.operationGroups)) { + for (const operation of values(group.operations)) { + for (const param of values(operation.parameters).where(each => each.protocol?.http?.in === ParameterLocation.Path)) { const name = param.language.default.name; - - if (!urlParam.has(name)) { - urlParam.add(name); - universalId.addProperty(new Property(name, '', param.schema, { - serializedName: name, language: { - default: { - description: param.summary, - name: name, - required: false, - readOnly: false, - uid: 'universal-parameter:resource identity' - } - } - })); + const hasName = universalId.properties?.findIndex((prop) => prop.language.default.name === name); + if (!hasName) { + if (!universalId.properties) { + universalId.properties = []; + } + const newProp = new Property(name, param.language.default.description, param.schema); + newProp.required = false; + newProp.readOnly = false; + newProp.serializedName = name; + universalId.properties.push(newProp); } } } } if (await state.getValue('azure', false)) { - universalId.addProperty(new Property('id', '', new StringSchema('_identity_type_', 'Resource identity path'), - { - serializedName: 'id', language: { - default: { - description: 'Resource identity path', - name: 'id', - required: false, - readOnly: false, - uid: 'universal-parameter:resource identity' + const idScheam = new Schema('_identity_type_', 'Resource identity path', SchemaType.String); + const idProp = new Property('id', 'Resource identity path', idScheam); + idProp.readOnly = false; + idProp.required = false; + if (!universalId.properties) { + universalId.properties = []; + } + universalId.properties.push(idProp); + } + + // identify models that are polymorphic in nature + for (const schema of allSchemas) { + if (schema instanceof ObjectSchema) { + const objSchema = schema as ObjectSchema; + // if this actual type is polymorphic, make sure we know that. + // parent class + if (objSchema.discriminator) { + objSchema.language.default.isPolymorphic = true; + if (objSchema.children) { + objSchema.language.default.polymorphicChildren = objSchema.children?.all; + } + } + + // sub class + if (objSchema.discriminatorValue) { + objSchema.language.default.discriminatorValue = objSchema.extensions?.['x-ms-discriminator-value']; + } + } + } + + // identify parameters that are constants + for (const group of values(model.operationGroups)) { + for (const operation of values(group.operations)) { + for (const parameter of values(operation.parameters)) { + if (parameter.required && parameter.schema instanceof ChoiceSchema) { + const choiceSchema = parameter.schema as ChoiceSchema; + if (choiceSchema.choices.length === 1) { + parameter.language.default.constantValue = choiceSchema.choices[0].value; } } } - )); + } } - // universalId.properties['id'] = new Property('id', { - // schema: new Schema('_identity_type_', { type: JsonType.String, description: 'Resource identity path' }), - // description: 'Resource identity path', serializedName: 'id', details: { - // default: { - // description: 'Resource identity path', - // name: 'id', - // required: false, - // readOnly: false, - // uid: 'universal-parameter:resource identity' - // } - // } - // }); - - - // if (await state.getValue('azure', false)) { - // universalId.properties['id'] = new Property('id', { - // schema: new Schema('_identity_type_', { type: JsonType.String, description: 'Resource identity path' }), - // description: 'Resource identity path', serializedName: 'id', details: { - // default: { - // description: 'Resource identity path', - // name: 'id', - // required: false, - // readOnly: false, - // uid: 'universal-parameter:resource identity' - // } - // } - // }); - // } - state.model.schemas.objects.push(universalId); - model.commands = { - operations: new Dictionary(), - parameters: new Dictionary(), - }; + // identify properties that are constants + for (const schema of values(schemas.objects)) { + for (const property of values(schema.properties)) { + if (property.required && property.schema instanceof ChoiceSchema) { + const choiceSchema = property.schema as ChoiceSchema; + if (choiceSchema.choices.length === 1) { + // properties with an enum single value are constants + // add the constant value + property.language.default.constantValue = choiceSchema.choices[0].value; + } + } + } + } return model; } diff --git a/powershell/utils/PwshModel.ts b/powershell/utils/PwshModel.ts index bae5a6932e..2f200bc7df 100644 --- a/powershell/utils/PwshModel.ts +++ b/powershell/utils/PwshModel.ts @@ -14,4 +14,4 @@ export class PwshModel extends CodeModel { this.apply(initializer); } -} \ No newline at end of file +}