diff --git a/logger/autorest.codemodel/AutoRest.CodeModel.csproj b/logger/autorest.codemodel/AutoRest.CodeModel.csproj new file mode 100644 index 0000000..5f9f562 --- /dev/null +++ b/logger/autorest.codemodel/AutoRest.CodeModel.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + + + + + + + diff --git a/logger/autorest.codemodel/CustomEnumNameGenerator.cs b/logger/autorest.codemodel/CustomEnumNameGenerator.cs new file mode 100644 index 0000000..ee5aeb8 --- /dev/null +++ b/logger/autorest.codemodel/CustomEnumNameGenerator.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NJsonSchema; +using NJsonSchema.CodeGeneration; + +namespace AutoRest.CodeModel +{ + internal class CustomEnumNameGenerator : IEnumNameGenerator + { + private readonly DefaultEnumNameGenerator _defaultEnumNameGenerator = new DefaultEnumNameGenerator(); + + public string Generate(int index, string name, object value, JsonSchema schema) => + _defaultEnumNameGenerator.Generate( + index, + // Fixes + and - enum values that cannot be generated into C# enum names + name.Equals("+") ? "plus" : name.Equals("-") ? "minus" : name, + value, + schema); + } +} diff --git a/logger/autorest.codemodel/CustomPropertyNameGenerator.cs b/logger/autorest.codemodel/CustomPropertyNameGenerator.cs new file mode 100644 index 0000000..c232df2 --- /dev/null +++ b/logger/autorest.codemodel/CustomPropertyNameGenerator.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NJsonSchema; +using NJsonSchema.CodeGeneration.CSharp; + +namespace AutoRest.CodeModel +{ + internal class CustomPropertyNameGenerator : CSharpPropertyNameGenerator + { + public override string Generate(JsonSchemaProperty property) + { + // Makes array type properties initialized with empty collections + if (property.IsArray) + { + property.IsRequired = true; + } + + // Cases CSharp properly + if (property.Name == "csharp") + { + return "CSharp"; + } + return base.Generate(property); + } + } +} diff --git a/logger/autorest.codemodel/CustomTypeNameGenerator.cs b/logger/autorest.codemodel/CustomTypeNameGenerator.cs new file mode 100644 index 0000000..032cfed --- /dev/null +++ b/logger/autorest.codemodel/CustomTypeNameGenerator.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using NJsonSchema; + +namespace AutoRest.CodeModel +{ + internal class CustomTypeNameGenerator : DefaultTypeNameGenerator + { + // Class names that conflict with project class names + private static readonly Dictionary RenameMap = new Dictionary + { + { "HttpHeader", "HttpResponseHeader" }, + { "Parameter", "RequestParameter" }, + { "Request", "ServiceRequest" }, + { "Response", "ServiceResponse" }, + { "SerializationFormat", "SerializationFormatMetadata" } + }; + + public override string Generate(JsonSchema schema, string typeNameHint, IEnumerable reservedTypeNames) + { + if (RenameMap.ContainsKey(typeNameHint)) + { + typeNameHint = RenameMap[typeNameHint]; + } + return base.Generate(schema, typeNameHint, reservedTypeNames); + } + } +} diff --git a/logger/autorest.codemodel/Program.cs b/logger/autorest.codemodel/Program.cs new file mode 100644 index 0000000..0d50f39 --- /dev/null +++ b/logger/autorest.codemodel/Program.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using NJsonSchema; +using NJsonSchema.CodeGeneration.CSharp; + +namespace AutoRest.CodeModel +{ + internal static class Program + { + private const string Path = "AutoRest.CSharp/Common/Input/Generated"; + private static readonly string Namespace = "AutoRest.CSharp.Input"; + + private static async Task Main() + { + using var webClient = new HttpClient(); + var schemaJson = await webClient.GetStringAsync(@"https://raw.githubusercontent.com/Azure/autorest/master/packages/libs/codemodel/.resources/all-in-one/json/code-model.json"); + schemaJson = schemaJson + // Makes Choices only have string values + .Replace(" \"type\": [\n \"string\",\n \"number\",\n \"boolean\"\n ]\n", $" \"type\": \"string\"\n"); + + var schema = JsonSchema.FromJsonAsync(schemaJson).GetAwaiter().GetResult(); + var settings = new CSharpGeneratorSettings + { + Namespace = Namespace, + HandleReferences = true, + TypeAccessModifier = "internal", + TypeNameGenerator = new CustomTypeNameGenerator(), + PropertyNameGenerator = new CustomPropertyNameGenerator(), + EnumNameGenerator = new CustomEnumNameGenerator(), + ExcludedTypeNames = new[] + { + "GroupSchema" + } + }; + var rawFile = new CSharpGenerator(schema, settings).GenerateFile(); + var cleanFile = String.Join(Environment.NewLine, rawFile.ToLines() + // Converts Newtonsoft attributes to YamlDotNet attributes + .Where(l => !l.Contains("Newtonsoft.Json.JsonConverter") + && !l.Contains("Newtonsoft.Json.JsonExtensionData")) + .Select(l => Regex.Replace(l, @"(.*\[)Newtonsoft\.Json\.JsonProperty\((.*""),?.*(\)\])", + "$1YamlDotNet.Serialization.YamlMember(Alias = $2$3", RegexOptions.Singleline).TrimEnd())) + // Removes AdditionalProperties property from types as they are required to derive from IDictionary for deserialization to work properly + .Replace($" private System.Collections.Generic.IDictionary _additionalProperties = new System.Collections.Generic.Dictionary();{Environment.NewLine}{Environment.NewLine} public System.Collections.Generic.IDictionary AdditionalProperties{Environment.NewLine} {{{Environment.NewLine} get {{ return _additionalProperties; }}{Environment.NewLine} set {{ _additionalProperties = value; }}{Environment.NewLine} }}{Environment.NewLine}", String.Empty) + // Fixes stray blank lines from the C# generator + .Replace($"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}", Environment.NewLine) + .Replace($"{Environment.NewLine}{Environment.NewLine} }}", $"{Environment.NewLine} }}") + // Weird generation issue workaround + .Replace($"{Namespace}.bool.True", "true"); + + var lines = cleanFile.ToLines().ToArray(); + var fileWithNullable = String.Join(Environment.NewLine, lines.Zip(lines.Skip(1).Append(String.Empty)) + .Select(ll => + { + var isNullableProperty = !ll.First.Contains("System.ComponentModel.DataAnnotations.Required") && ll.Second.Contains("{ get; set; }"); + return isNullableProperty ? Regex.Replace(ll.Second, @"( \w+ { get; set; })", "?$1") : ll.Second; + }) + .SkipLast(1) + .Prepend(lines.First())); + File.WriteAllText($"../../src/{Path}/CodeModel.cs", fileWithNullable); + } + } +} diff --git a/logger/autorest.codemodel/Properties/launchSettings.json b/logger/autorest.codemodel/Properties/launchSettings.json new file mode 100644 index 0000000..536fa07 --- /dev/null +++ b/logger/autorest.codemodel/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "AutoRest.CodeModel": { + "commandName": "Project", + "workingDirectory": "$(ProjectDir)" + } + } +} \ No newline at end of file diff --git a/logger/autorest.codemodel/StringExtensions.cs b/logger/autorest.codemodel/StringExtensions.cs new file mode 100644 index 0000000..e77fca1 --- /dev/null +++ b/logger/autorest.codemodel/StringExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace AutoRest.CodeModel +{ + internal static class StringExtensions + { + //https://stackoverflow.com/a/41176852/294804 + public static IEnumerable ToLines(this string value, bool removeEmptyLines = false) + { + using var sr = new StringReader(value); + string? line; + while ((line = sr.ReadLine()) != null) + { + if (removeEmptyLines && String.IsNullOrWhiteSpace(line)) + continue; + yield return line; + } + } + } +} diff --git a/logger/autorest.codemodel/code-model.json b/logger/autorest.codemodel/code-model.json new file mode 100644 index 0000000..8849284 --- /dev/null +++ b/logger/autorest.codemodel/code-model.json @@ -0,0 +1,3317 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ApiVersion": { + "description": "- since API version formats range from\nAzure ARM API date style (2018-01-01) to semver (1.2.3)\nand virtually any other text, this value tends to be an\nopaque string with the possibility of a modifier to indicate\nthat it is a range.\n\noptions:\n- prepend a dash or append a plus to indicate a range\n(ie, '2018-01-01+' or '-2019-01-01', or '1.0+' )\n\n- semver-range style (ie, '^1.0.0' or '~1.0.0' )", + "type": "object", + "properties": { + "version": { + "description": "the actual api version string used in the API", + "type": "string" + }, + "range": { + "enum": [ + "+", + "-" + ], + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "version" + ] + }, + "ApiVersions": { + "description": "a collection of api versions", + "type": "array", + "items": { + "$ref": "#/definitions/ApiVersion" + } + }, + "Schema": { + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for Schema" + }, + "type": { + "$ref": "#/definitions/AllSchemaTypes", + "description": "the schema type" + }, + "summary": { + "description": "a short description", + "type": "string" + }, + "example": { + "description": "example information" + }, + "defaultValue": { + "description": "If the value isn't sent on the wire, the service will assume this" + }, + "serialization": { + "$ref": "#/definitions/SerializationFormats", + "description": "per-serialization information for this Schema" + }, + "apiVersions": { + "description": "API versions that this applies to. Undefined means all versions", + "type": "array", + "items": { + "$ref": "#/definitions/ApiVersion" + } + }, + "deprecated": { + "$ref": "#/definitions/Deprecation", + "description": "Represent the deprecation information if api is deprecated.", + "default": "undefined" + }, + "origin": { + "description": "where did this aspect come from (jsonpath or 'modelerfour:')", + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation", + "description": "External Documentation Links" + }, + "protocol": { + "$ref": "#/definitions/Protocols", + "description": "per-protocol information for this aspect" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ] + }, + "Deprecation": { + "description": "Represent information about a deprecation", + "type": "object", + "properties": { + "reason": { + "description": "Reason why this was deprecated.", + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "Extensions": { + "description": "A dictionary of open-ended 'x-*' extensions propogated from the original source document.", + "type": "object", + "properties": { + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "uri": { + "description": "an URI", + "type": "string" + }, + "ExternalDocumentation": { + "description": "a reference to external documentation", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "description": "an URI", + "type": "string" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "url" + ] + }, + "Languages": { + "description": "custom extensible metadata for individual language generators", + "type": "object", + "properties": { + "default": { + "$ref": "#/definitions/Language" + }, + "csharp": { + "$ref": "#/definitions/Language" + }, + "python": { + "$ref": "#/definitions/Language" + }, + "ruby": { + "$ref": "#/definitions/Language" + }, + "go": { + "$ref": "#/definitions/Language" + }, + "typescript": { + "$ref": "#/definitions/Language" + }, + "javascript": { + "$ref": "#/definitions/Language" + }, + "powershell": { + "$ref": "#/definitions/Language" + }, + "java": { + "$ref": "#/definitions/Language" + }, + "c": { + "$ref": "#/definitions/Language" + }, + "cpp": { + "$ref": "#/definitions/Language" + }, + "swift": { + "$ref": "#/definitions/Language" + }, + "objectivec": { + "$ref": "#/definitions/Language" + }, + "sputnik": { + "$ref": "#/definitions/Language" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "default" + ] + }, + "Protocols": { + "description": "custom extensible metadata for individual protocols (ie, HTTP, etc)", + "type": "object", + "properties": { + "http": { + "$ref": "#/definitions/Protocol" + }, + "amqp": { + "$ref": "#/definitions/Protocol" + }, + "mqtt": { + "$ref": "#/definitions/Protocol" + }, + "jsonrpc": { + "$ref": "#/definitions/Protocol" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "Metadata": { + "description": "common pattern for Metadata on aspects", + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this aspect" + }, + "protocol": { + "$ref": "#/definitions/Protocols", + "description": "per-protocol information for this aspect" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol" + ] + }, + "Language": { + "description": "the bare-minimum fields for per-language metadata on a given aspect", + "type": "object", + "properties": { + "name": { + "description": "name used in actual implementation", + "type": "string" + }, + "description": { + "description": "description text - describes this node.", + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": { + "type": "object" + }, + "required": [ + "description", + "name" + ] + }, + "CSharpLanguage": { + "type": "object", + "defaultProperties": [], + "additionalProperties": false + }, + "Protocol": { + "description": "the bare-minimum fields for per-protocol metadata on a given aspect", + "type": "object", + "defaultProperties": [], + "additionalProperties": false + }, + "email": { + "type": "string" + }, + "Contact": { + "description": "contact information", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "description": "an URI", + "type": "string" + }, + "email": { + "type": "string" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "License": { + "description": "license information", + "type": "object", + "properties": { + "name": { + "description": "the nameof the license", + "type": "string" + }, + "url": { + "description": "an uri pointing to the full license text", + "type": "string" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "name" + ] + }, + "Info": { + "description": "code model information", + "type": "object", + "properties": { + "title": { + "description": "the title of this service.", + "type": "string" + }, + "description": { + "description": "a text description of the service", + "type": "string" + }, + "termsOfService": { + "description": "an uri to the terms of service specified to access the service", + "type": "string" + }, + "contact": { + "$ref": "#/definitions/Contact", + "description": "contact information for the service" + }, + "license": { + "$ref": "#/definitions/License", + "description": "license information for th service" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation", + "description": "External Documentation" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "title" + ] + }, + "XmlSerlializationFormat": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean" + }, + "wrapped": { + "type": "boolean" + }, + "text": { + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "attribute", + "text", + "wrapped" + ], + "allOf": [ + { + "$ref": "#/definitions/SerializationFormat" + } + ] + }, + "SerializationFormats": { + "description": "custom extensible metadata for individual serialization formats", + "type": "object", + "properties": { + "json": { + "$ref": "#/definitions/SerializationFormat" + }, + "xml": { + "$ref": "#/definitions/XmlSerlializationFormat" + }, + "protobuf": { + "$ref": "#/definitions/SerializationFormat" + }, + "binary": { + "$ref": "#/definitions/SerializationFormat" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "SchemaType": { + "description": "possible schema types that indicate the type of schema.", + "enum": [ + "any", + "any-object", + "arm-id", + "array", + "binary", + "boolean", + "byte-array", + "char", + "choice", + "conditional", + "constant", + "credential", + "date", + "date-time", + "dictionary", + "duration", + "flag", + "group", + "integer", + "not", + "number", + "object", + "odata-query", + "or", + "sealed-choice", + "sealed-conditional", + "string", + "time", + "unixtime", + "unknown", + "uri", + "uuid", + "xor" + ], + "type": "string" + }, + "CompoundSchemaTypes": { + "description": "Compound schemas are used to construct complex objects or offer choices of a set of schemas.\n\n(ie, allOf, anyOf, oneOf )", + "enum": [ + "or", + "xor" + ], + "type": "string" + }, + "PrimitiveSchemaTypes": { + "description": "Schema types that are primitive language values", + "enum": [ + "arm-id", + "boolean", + "char", + "credential", + "date", + "date-time", + "duration", + "integer", + "number", + "string", + "time", + "unixtime", + "uri", + "uuid" + ], + "type": "string" + }, + "ValueSchemaTypes": { + "description": "schema types that are non-object or complex types", + "enum": [ + "arm-id", + "array", + "boolean", + "byte-array", + "char", + "choice", + "conditional", + "credential", + "date", + "date-time", + "duration", + "flag", + "integer", + "number", + "sealed-choice", + "sealed-conditional", + "string", + "time", + "unixtime", + "uri", + "uuid" + ], + "type": "string" + }, + "ObjectSchemaTypes": { + "description": "schema types that can be objects", + "enum": [ + "dictionary", + "object", + "or" + ], + "type": "string" + }, + "AllSchemaTypes": { + "description": "all schema types", + "enum": [ + "any", + "any-object", + "arm-id", + "array", + "binary", + "boolean", + "byte-array", + "char", + "choice", + "conditional", + "constant", + "credential", + "date", + "date-time", + "dictionary", + "duration", + "flag", + "group", + "integer", + "not", + "number", + "object", + "odata-query", + "or", + "sealed-choice", + "sealed-conditional", + "string", + "time", + "unixtime", + "uri", + "uuid", + "xor" + ], + "type": "string" + }, + "SerializationFormat": { + "type": "object", + "properties": { + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "ValueSchema": { + "description": "schema types that are non-object or complex types", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "PrimitiveSchema": { + "description": "Schema types that are primitive language values", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "ComplexSchema": { + "description": "schema types that can be objects", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "Value": { + "description": "common base interface for properties, parameters and the like.", + "type": "object", + "properties": { + "schema": { + "$ref": "#/definitions/Schema", + "description": "the schema of this Value" + }, + "required": { + "description": "if the value is marked 'required'.", + "type": "boolean" + }, + "nullable": { + "description": "can null be passed in instead", + "type": "boolean" + }, + "assumedValue": { + "description": "the value that the remote will assume if this value is not present" + }, + "clientDefaultValue": { + "description": "the value that the client should provide if the consumer doesn't provide one" + }, + "summary": { + "description": "a short description", + "type": "string" + }, + "apiVersions": { + "description": "API versions that this applies to. Undefined means all versions", + "type": "array", + "items": { + "$ref": "#/definitions/ApiVersion" + } + }, + "deprecated": { + "$ref": "#/definitions/Deprecation", + "description": "Represent the deprecation information if api is deprecated.", + "default": "undefined" + }, + "origin": { + "description": "where did this aspect come from (jsonpath or 'modelerfour:')", + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation", + "description": "External Documentation Links" + }, + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this aspect" + }, + "protocol": { + "$ref": "#/definitions/Protocols", + "description": "per-protocol information for this aspect" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "schema" + ] + }, + "Property": { + "description": "a property is a child value in an object", + "type": "object", + "properties": { + "readOnly": { + "description": "if the property is marked read-only (ie, not intended to be sent to the service)", + "type": "boolean" + }, + "serializedName": { + "description": "the wire name of this property", + "type": "string" + }, + "flattenedNames": { + "description": "when a property is flattened, the property will be the set of serialized names to get to that target property.\n\nIf flattenedName is present, then this property is a flattened property.\n\n(ie, ['properties','name'] )", + "type": "array", + "items": { + "type": "string" + } + }, + "isDiscriminator": { + "description": "if this property is used as a discriminator for a polymorphic type", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "schema", + "serializedName" + ], + "allOf": [ + { + "$ref": "#/definitions/Value" + } + ] + }, + "ImplementationLocation": { + "enum": [ + "Client", + "Context", + "Method" + ], + "type": "string" + }, + "Parameter": { + "description": "a definition of an discrete input for an operation", + "type": "object", + "properties": { + "implementation": { + "$ref": "#/definitions/ImplementationLocation", + "description": "suggested implementation location for this parameter" + }, + "flattened": { + "description": "When a parameter is flattened, it will be left in the list, but marked hidden (so, don't generate those!)", + "type": "boolean" + }, + "groupedBy": { + "$ref": "#/definitions/Parameter", + "description": "When a parameter is grouped into another, this will tell where the parameter got grouped into." + }, + "isPartialBody": { + "description": "If this parameter is only part of the body request(for multipart and form bodies.)", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "schema" + ], + "allOf": [ + { + "$ref": "#/definitions/Value" + } + ] + }, + "VirtualParameter": { + "type": "object", + "properties": { + "originalParameter": { + "$ref": "#/definitions/Parameter", + "description": "the original body parameter that this parameter is in effect replacing" + }, + "pathToProperty": { + "description": "if this parameter is for a nested property, this is the path of properties it takes to get there", + "type": "array", + "items": { + "$ref": "#/definitions/Property" + } + }, + "targetProperty": { + "$ref": "#/definitions/Property", + "description": "the target property this virtual parameter represents" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "originalParameter", + "pathToProperty", + "protocol", + "schema", + "targetProperty" + ], + "allOf": [ + { + "$ref": "#/definitions/Parameter" + } + ] + }, + "Response": { + "description": "a response from a service.", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol" + ], + "allOf": [ + { + "$ref": "#/definitions/Metadata" + } + ] + }, + "BinaryResponse": { + "description": "a response where the content should be treated as a binary instead of a value or object", + "type": "object", + "properties": { + "binary": { + "description": "indicates that this response is a binary stream", + "type": "boolean", + "enum": [ + true + ] + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "binary", + "language", + "protocol" + ], + "allOf": [ + { + "$ref": "#/definitions/Response" + } + ] + }, + "SchemaResponse": { + "description": "a response that should be deserialized into a result of type(schema)", + "type": "object", + "properties": { + "schema": { + "$ref": "#/definitions/Schema", + "description": "the content returned by the service for a given operaiton" + }, + "nullable": { + "description": "indicates whether the response can be 'null'", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "schema" + ], + "allOf": [ + { + "$ref": "#/definitions/Response" + } + ] + }, + "Operation": { + "description": "represents a single callable endpoint with a discrete set of inputs, and any number of output possibilities (responses or exceptions)", + "type": "object", + "properties": { + "operationId": { + "description": "Original Operation ID if present.\nThis can be used to identify the original id of an operation before it is styled.\nTHIS IS NOT the name of the operation that should be used in the generator. Use `.language.default.name` for this", + "type": "string" + }, + "parameters": { + "description": "common parameters when there are multiple requests", + "type": "array", + "items": { + "$ref": "#/definitions/Parameter" + } + }, + "signatureParameters": { + "description": "a common filtered list of parameters that is (assumably) the actual method signature parameters", + "type": "array", + "items": { + "$ref": "#/definitions/Parameter" + } + }, + "requestMediaTypes": { + "$ref": "#/definitions/Record", + "description": "Mapping of all the content types available for this operation to the coresponding request." + }, + "specialHeaders": { + "description": "List of headers that parameters should not handle as parameters but with special logic.\nSee https://github.com/Azure/autorest/tree/main/packages/extensions/modelerfour for configuration `skip-special-headers` to exclude headers.", + "type": "array", + "items": { + "type": "string" + } + }, + "requests": { + "description": "the different possibilities to build the request.", + "type": "array", + "items": { + "$ref": "#/definitions/Request" + } + }, + "responses": { + "description": "responses that indicate a successful call", + "type": "array", + "items": { + "$ref": "#/definitions/Response" + } + }, + "exceptions": { + "description": "responses that indicate a failed call", + "type": "array", + "items": { + "$ref": "#/definitions/Response" + } + }, + "profile": { + "$ref": "#/definitions/Record", + "description": "the apiVersion to use for a given profile name" + }, + "summary": { + "description": "a short description", + "type": "string" + }, + "apiVersions": { + "description": "API versions that this applies to. Undefined means all versions", + "type": "array", + "items": { + "$ref": "#/definitions/ApiVersion" + } + }, + "deprecated": { + "$ref": "#/definitions/Deprecation", + "description": "Represent the deprecation information if api is deprecated.", + "default": "undefined" + }, + "origin": { + "description": "where did this aspect come from (jsonpath or 'modelerfour:')", + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation", + "description": "External Documentation Links" + }, + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this aspect" + }, + "protocol": { + "$ref": "#/definitions/Protocols", + "description": "per-protocol information for this aspect" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol" + ] + }, + "Request": { + "type": "object", + "properties": { + "parameters": { + "description": "the parameter inputs to the operation", + "type": "array", + "items": { + "$ref": "#/definitions/Parameter" + } + }, + "signatureParameters": { + "description": "a filtered list of parameters that is (assumably) the actual method signature parameters", + "type": "array", + "items": { + "$ref": "#/definitions/Parameter" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol" + ], + "allOf": [ + { + "$ref": "#/definitions/Metadata" + } + ] + }, + "OperationGroup": { + "description": "an operation group represents a container around set of operations", + "type": "object", + "properties": { + "$key": { + "type": "string" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/definitions/Operation" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "$key", + "language", + "operations", + "protocol" + ], + "allOf": [ + { + "$ref": "#/definitions/Metadata" + } + ] + }, + "AnySchema": { + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "AnyObjectSchema": { + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "ArraySchema": { + "description": "a Schema that represents and array of values", + "type": "object", + "properties": { + "elementType": { + "$ref": "#/definitions/Schema", + "description": "elementType of the array" + }, + "maxItems": { + "description": "maximum number of elements in the array", + "type": "number" + }, + "minItems": { + "description": "minimum number of elements in the array", + "type": "number" + }, + "uniqueItems": { + "description": "if the elements in the array should be unique", + "type": "boolean" + }, + "nullableItems": { + "description": "if elements in the array should be nullable", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "elementType", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "ByteArraySchema": { + "description": "a schema that represents a ByteArray value", + "type": "object", + "properties": { + "format": { + "description": "date-time format", + "enum": [ + "base64url", + "byte" + ], + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "format", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "BinarySchema": { + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "StringSchema": { + "description": "a Schema that represents a string value", + "type": "object", + "properties": { + "maxLength": { + "description": "the maximum length of the string", + "type": "number" + }, + "minLength": { + "description": "the minimum length of the string", + "type": "number" + }, + "pattern": { + "description": "a regular expression that the string must be validated against", + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "ODataQuerySchema": { + "description": "a schema that represents a ODataQuery value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "CredentialSchema": { + "description": "a schema that represents a Credential value", + "type": "object", + "properties": { + "maxLength": { + "description": "the maximum length of the string", + "type": "number" + }, + "minLength": { + "description": "the minimum length of the string", + "type": "number" + }, + "pattern": { + "description": "a regular expression that the string must be validated against", + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "UriSchema": { + "description": "a schema that represents a Uri value", + "type": "object", + "properties": { + "maxLength": { + "description": "the maximum length of the string", + "type": "number" + }, + "minLength": { + "description": "the minimum length of the string", + "type": "number" + }, + "pattern": { + "description": "a regular expression that the string must be validated against", + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "UuidSchema": { + "description": "a schema that represents a Uuid value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "ArmIdSchema": { + "description": "a schema that represents a Uuid value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "ChoiceSchema": { + "description": "a schema that represents a choice of several values (ie, an 'enum')", + "type": "object", + "properties": { + "choiceType": { + "$ref": "#/definitions/PrimitiveSchema", + "description": "the primitive type for the choices" + }, + "choices": { + "description": "the possible choices for in the set", + "type": "array", + "items": { + "$ref": "#/definitions/ChoiceValue" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "choiceType", + "choices", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "ChoiceValue": { + "description": "an individual choice in a ChoiceSchema", + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this value" + }, + "value": { + "description": "the actual value", + "type": [ + "string", + "number", + "boolean" + ] + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "value" + ] + }, + "SealedChoiceSchema": { + "description": "a schema that represents a choice of several values (ie, an 'enum')", + "type": "object", + "properties": { + "choiceType": { + "$ref": "#/definitions/PrimitiveSchema", + "description": "the primitive type for the choices" + }, + "choices": { + "description": "the possible choices for in the set", + "type": "array", + "items": { + "$ref": "#/definitions/ChoiceValue" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "choiceType", + "choices", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "ConditionalSchema": { + "description": "a schema that represents a value dependent on another", + "type": "object", + "properties": { + "conditionalType": { + "$ref": "#/definitions/PrimitiveSchema", + "description": "the primitive type for the conditional" + }, + "conditions": { + "description": "the possible conditinal values", + "type": "array", + "items": { + "$ref": "#/definitions/ConditionalValue" + } + }, + "sourceValue": { + "$ref": "#/definitions/Value", + "description": "the source value that drives the target value (property or parameter)" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "conditionalType", + "conditions", + "language", + "protocol", + "sourceValue", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "ConditionalValue": { + "description": "an individual value in a ConditionalSchema", + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this value" + }, + "target": { + "description": "the actual value", + "type": [ + "string", + "number", + "boolean" + ] + }, + "source": { + "description": "the actual value", + "type": [ + "string", + "number", + "boolean" + ] + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "source", + "target" + ] + }, + "SealedConditionalSchema": { + "description": "a schema that represents a value dependent on another (not overridable)", + "type": "object", + "properties": { + "conditionalType": { + "$ref": "#/definitions/PrimitiveSchema", + "description": "the primitive type for the condition" + }, + "conditions": { + "description": "the possible conditional values", + "type": "array", + "items": { + "$ref": "#/definitions/ConditionalValue" + } + }, + "sourceValue": { + "$ref": "#/definitions/Value", + "description": "the source value that drives the target value" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "conditionalType", + "conditions", + "language", + "protocol", + "sourceValue", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "ConstantValue": { + "description": "a container for the actual constant value", + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this value" + }, + "value": { + "description": "the actual constant value to use" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "value" + ] + }, + "ConstantSchema": { + "description": "a schema that represents a constant value", + "type": "object", + "properties": { + "valueType": { + "$ref": "#/definitions/Schema", + "description": "the schema type of the constant value (ie, StringSchema, NumberSchema, etc)" + }, + "value": { + "$ref": "#/definitions/ConstantValue", + "description": "the actual constant value" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type", + "value", + "valueType" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "DictionarySchema": { + "description": "a schema that represents a key-value collection", + "type": "object", + "properties": { + "elementType": { + "$ref": "#/definitions/Schema", + "description": "the element type of the dictionary. (Keys are always strings)" + }, + "nullableItems": { + "description": "if elements in the dictionary should be nullable", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "elementType", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ComplexSchema" + } + ] + }, + "FlagValue": { + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this value" + }, + "value": { + "type": "number" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "value" + ] + }, + "FlagSchema": { + "type": "object", + "properties": { + "choices": { + "description": "the possible choices for in the set", + "type": "array", + "items": { + "$ref": "#/definitions/FlagValue" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "choices", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ValueSchema" + } + ] + }, + "NumberSchema": { + "description": "a Schema that represents a Number value", + "type": "object", + "properties": { + "precision": { + "description": "precision (# of bits?) of the number", + "type": "number" + }, + "multipleOf": { + "description": "if present, the number must be an exact multiple of this value", + "type": "number" + }, + "maximum": { + "description": "if present, the value must be lower than or equal to this (unless exclusiveMaximum is true)", + "type": "number" + }, + "exclusiveMaximum": { + "description": "if present, the value must be lower than maximum", + "type": "boolean" + }, + "minimum": { + "description": "if present, the value must be highter than or equal to this (unless exclusiveMinimum is true)", + "type": "number" + }, + "exclusiveMinimum": { + "description": "if present, the value must be higher than minimum", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "precision", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "SchemaContext": { + "enum": [ + "exception", + "input", + "output" + ], + "type": "string" + }, + "SchemaUsage": { + "type": "object", + "properties": { + "usage": { + "description": "contexts in which the schema is used", + "type": "array", + "items": { + "$ref": "#/definitions/SchemaContext" + } + }, + "serializationFormats": { + "description": "Known media types in which this schema can be serialized", + "type": "array", + "items": { + "$ref": "#/definitions/KnownMediaType" + } + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "Relations": { + "type": "object", + "properties": { + "immediate": { + "type": "array", + "items": { + "$ref": "#/definitions/ComplexSchema" + } + }, + "all": { + "type": "array", + "items": { + "$ref": "#/definitions/ComplexSchema" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "all", + "immediate" + ] + }, + "Discriminator": { + "type": "object", + "properties": { + "property": { + "$ref": "#/definitions/Property" + }, + "immediate": { + "$ref": "#/definitions/Record" + }, + "all": { + "$ref": "#/definitions/Record" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "all", + "immediate", + "property" + ] + }, + "GroupProperty": { + "type": "object", + "properties": { + "originalParameter": { + "type": "array", + "items": { + "$ref": "#/definitions/Parameter" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "originalParameter", + "protocol", + "schema", + "serializedName" + ], + "allOf": [ + { + "$ref": "#/definitions/Property" + } + ] + }, + "GroupSchema": { + "type": "object", + "properties": { + "properties": { + "type": "array", + "items": { + "$ref": "#/definitions/GroupProperty" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/SchemaUsage" + } + ] + }, + "ObjectSchema": { + "description": "a schema that represents a type with child properties.", + "type": "object", + "properties": { + "discriminator": { + "$ref": "#/definitions/Discriminator", + "description": "the property of the polymorphic descriminator for this type, if there is one" + }, + "properties": { + "description": "the collection of properties that are in this object", + "type": "array", + "items": { + "$ref": "#/definitions/Property" + } + }, + "maxProperties": { + "description": "maximum number of properties permitted", + "type": "number" + }, + "minProperties": { + "description": "minimum number of properties permitted", + "type": "number" + }, + "parents": { + "$ref": "#/definitions/Relations" + }, + "children": { + "$ref": "#/definitions/Relations" + }, + "discriminatorValue": { + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ComplexSchema" + }, + { + "$ref": "#/definitions/SchemaUsage" + } + ] + }, + "BooleanSchema": { + "description": "a schema that represents a boolean value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "CharSchema": { + "description": "a schema that represents a Char value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "OrSchema": { + "description": "an OR relationship between several schemas", + "type": "object", + "properties": { + "anyOf": { + "description": "the set of schemas that this schema is composed of. Every schema is optional", + "type": "array", + "items": { + "$ref": "#/definitions/ComplexSchema" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "anyOf", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/ComplexSchema" + } + ] + }, + "XorSchema": { + "description": "an XOR relationship between several schemas", + "type": "object", + "properties": { + "oneOf": { + "description": "the set of schemas that this must be one and only one of.", + "type": "array", + "items": { + "$ref": "#/definitions/Schema" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "oneOf", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "NotSchema": { + "description": "a NOT relationship between schemas", + "type": "object", + "properties": { + "not": { + "$ref": "#/definitions/Schema", + "description": "the schema that this may not be." + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "not", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/Schema" + } + ] + }, + "DurationSchema": { + "description": "a schema that represents a Duration value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "DateTimeSchema": { + "description": "a schema that represents a DateTime value", + "type": "object", + "properties": { + "format": { + "description": "date-time format", + "enum": [ + "date-time", + "date-time-rfc1123" + ], + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "format", + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "DateSchema": { + "description": "a schema that represents a Date value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "TimeSchema": { + "description": "a schema that represents a Date value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "UnixTimeSchema": { + "description": "a schema that represents a UnixTime value", + "type": "object", + "properties": {}, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/PrimitiveSchema" + } + ] + }, + "Schemas": { + "description": "the full set of schemas for a given service, categorized into convenient collections", + "type": "object", + "properties": { + "arrays": { + "description": "a collection of items", + "type": "array", + "items": { + "$ref": "#/definitions/ArraySchema" + } + }, + "dictionaries": { + "description": "an associative array (ie, dictionary, hashtable, etc)", + "type": "array", + "items": { + "$ref": "#/definitions/DictionarySchema" + } + }, + "booleans": { + "description": "a true or false value", + "type": "array", + "items": { + "$ref": "#/definitions/BooleanSchema" + } + }, + "numbers": { + "description": "a number value", + "type": "array", + "items": { + "$ref": "#/definitions/NumberSchema" + } + }, + "objects": { + "description": "an object of some type", + "type": "array", + "items": { + "$ref": "#/definitions/ObjectSchema" + } + }, + "strings": { + "description": "a string of characters", + "type": "array", + "items": { + "$ref": "#/definitions/StringSchema" + } + }, + "unixtimes": { + "description": "UnixTime", + "type": "array", + "items": { + "$ref": "#/definitions/UnixTimeSchema" + } + }, + "byteArrays": { + "description": "ByteArray -- an array of bytes", + "type": "array", + "items": { + "$ref": "#/definitions/ByteArraySchema" + } + }, + "streams": { + "type": "array", + "items": { + "$ref": "#/definitions/Schema" + } + }, + "chars": { + "description": "a single character", + "type": "array", + "items": { + "$ref": "#/definitions/CharSchema" + } + }, + "dates": { + "description": "a Date", + "type": "array", + "items": { + "$ref": "#/definitions/DateSchema" + } + }, + "times": { + "description": "a time", + "type": "array", + "items": { + "$ref": "#/definitions/TimeSchema" + } + }, + "dateTimes": { + "description": "a DateTime", + "type": "array", + "items": { + "$ref": "#/definitions/DateTimeSchema" + } + }, + "durations": { + "description": "a Duration", + "type": "array", + "items": { + "$ref": "#/definitions/DurationSchema" + } + }, + "uuids": { + "description": "a universally unique identifier", + "type": "array", + "items": { + "$ref": "#/definitions/UuidSchema" + } + }, + "uris": { + "description": "an URI of some kind", + "type": "array", + "items": { + "$ref": "#/definitions/UriSchema" + } + }, + "armIds": { + "description": "an URI of some kind", + "type": "array", + "items": { + "$ref": "#/definitions/ArmIdSchema" + } + }, + "credentials": { + "description": "a password or credential", + "type": "array", + "items": { + "$ref": "#/definitions/CredentialSchema" + } + }, + "odataQueries": { + "description": "OData Query", + "type": "array", + "items": { + "$ref": "#/definitions/ODataQuerySchema" + } + }, + "choices": { + "description": "- this is essentially can be thought of as an 'enum'\nthat is a choice between one of several items, but an unspecified value is permitted.", + "type": "array", + "items": { + "$ref": "#/definitions/ChoiceSchema" + } + }, + "sealedChoices": { + "description": "- this is essentially can be thought of as an 'enum'\nthat is a choice between one of several items, but an unknown value is not allowed.", + "type": "array", + "items": { + "$ref": "#/definitions/SealedChoiceSchema" + } + }, + "conditionals": { + "description": "ie, when 'profile' is 'production', use '2018-01-01' for apiversion", + "type": "array", + "items": { + "$ref": "#/definitions/ConditionalSchema" + } + }, + "sealedConditionals": { + "type": "array", + "items": { + "$ref": "#/definitions/SealedConditionalSchema" + } + }, + "flags": { + "type": "array", + "items": { + "$ref": "#/definitions/FlagSchema" + } + }, + "constants": { + "description": "a constant value", + "type": "array", + "items": { + "$ref": "#/definitions/ConstantSchema" + } + }, + "ors": { + "type": "array", + "items": { + "$ref": "#/definitions/OrSchema" + } + }, + "xors": { + "type": "array", + "items": { + "$ref": "#/definitions/XorSchema" + } + }, + "binaries": { + "type": "array", + "items": { + "$ref": "#/definitions/BinarySchema" + } + }, + "unknowns": { + "description": "it's possible that we just may make this an error\nin representation.", + "type": "array", + "items": { + "$ref": "#/definitions/Schema" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/GroupSchema" + } + }, + "any": { + "type": "array", + "items": { + "$ref": "#/definitions/AnySchema" + } + }, + "anyObjects": { + "type": "array", + "items": { + "$ref": "#/definitions/AnyObjectSchema" + } + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "Security": { + "description": "The security information for the API surface", + "type": "object", + "properties": { + "authenticationRequired": { + "description": "indicates that the API surface requires authentication", + "type": "boolean" + }, + "schemes": { + "items": { + "type": "SecuritySchemeFull" + }, + "type": "array" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "authenticationRequired", + "schemes" + ] + }, + "SecurityScheme": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "type" + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "scopes", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/SecurityScheme" + } + ] + }, + "KeySecurityScheme": { + "type": "object", + "properties": { + "in": { + "type": "string", + "enum": [ + "header" + ] + }, + "name": { + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "in", + "name", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/SecurityScheme" + } + ] + }, + "ValueOrFactory": { + "anyOf": [ + { + "$ref": "#/definitions/ApiVersion" + }, + { + "type": "object", + "defaultProperties": [], + "additionalProperties": false + } + ] + }, + "Example": { + "description": "example data [UNFINISHED]", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": {}, + "externalValue": { + "description": "an URI", + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "AADTokenSecurityScheme": { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "scopes", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/SecurityScheme" + } + ] + }, + "AzureKeySecurityScheme": { + "type": "object", + "properties": { + "headerName": { + "type": "string" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "headerName", + "type" + ], + "allOf": [ + { + "$ref": "#/definitions/SecurityScheme" + } + ] + }, + "HttpMethod": { + "description": "standard HTTP protocol methods", + "enum": [ + "delete", + "get", + "head", + "options", + "patch", + "post", + "put", + "trace" + ], + "type": "string" + }, + "ParameterLocation": { + "description": "the location that this parameter is placed in the http request", + "enum": [ + "body", + "cookie", + "header", + "none", + "path", + "query", + "uri", + "virtual" + ], + "type": "string" + }, + "Scheme": { + "type": "string", + "enum": [ + "bearer" + ] + }, + "SecurityType": { + "enum": [ + "apiKey", + "http", + "oauth2", + "openIdConnect" + ], + "type": "string" + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "properties": { + "authorizationUrl": { + "description": "an URI", + "type": "string" + }, + "tokenUrl": { + "description": "an URI", + "type": "string" + }, + "refreshUrl": { + "description": "an URI", + "type": "string" + }, + "scopes": { + "$ref": "#/definitions/Record" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "authorizationUrl", + "scopes", + "tokenUrl" + ] + }, + "BearerHTTPSecurityScheme": { + "type": "object", + "properties": { + "scheme": { + "type": "string", + "enum": [ + "bearer" + ] + }, + "bearerFormat": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "description": { + "type": "string" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "scheme", + "type" + ] + }, + "ClientCredentialsFlow": { + "type": "object", + "properties": { + "tokenUrl": { + "description": "an URI", + "type": "string" + }, + "refreshUrl": { + "description": "an URI", + "type": "string" + }, + "scopes": { + "$ref": "#/definitions/Record" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "scopes", + "tokenUrl" + ] + }, + "ImplicitOAuthFlow": { + "type": "object", + "properties": { + "authorizationUrl": { + "description": "an URI", + "type": "string" + }, + "refreshUrl": { + "description": "an URI", + "type": "string" + }, + "scopes": { + "$ref": "#/definitions/Record" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "authorizationUrl", + "scopes" + ] + }, + "NonBearerHTTPSecurityScheme": { + "type": "object", + "properties": { + "scheme": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "scheme", + "type" + ] + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "anyOf": [ + { + "$ref": "#/definitions/BearerHTTPSecurityScheme" + }, + { + "$ref": "#/definitions/NonBearerHTTPSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "$ref": "#/definitions/ParameterLocation" + }, + "description": { + "type": "string" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "in", + "name", + "type" + ] + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "description": "an URI", + "type": "string" + }, + "description": { + "type": "string" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "openIdConnectUrl", + "type" + ] + }, + "PasswordOAuthFlow": { + "type": "object", + "properties": { + "tokenUrl": { + "description": "an URI", + "type": "string" + }, + "refreshUrl": { + "description": "an URI", + "type": "string" + }, + "scopes": { + "$ref": "#/definitions/Record" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "scopes", + "tokenUrl" + ] + }, + "SecurityRequirement": { + "description": "common ways of serializing simple parameters", + "type": "object", + "defaultProperties": [], + "additionalProperties": false + }, + "SerializationStyle": { + "description": "The Serialization Style used for the parameter.\n\nDescribes how the parameter value will be serialized depending on the type of the parameter value.", + "enum": [ + "binary", + "deepObject", + "form", + "json", + "label", + "matrix", + "pipeDelimited", + "simple", + "spaceDelimited", + "tabDelimited", + "xml" + ], + "type": "string" + }, + "QueryEncodingStyle": { + "enum": [ + "deepObject", + "form", + "pipeDelimited", + "spaceDelimited" + ], + "type": "string" + }, + "PathEncodingStyle": { + "enum": [ + "label", + "matrix", + "simple" + ], + "type": "string" + }, + "Default": { + "description": "A catch-all for all un-handled response codes.", + "type": "string", + "enum": [ + "default" + ] + }, + "StatusCode": { + "enum": [ + 100, + 101, + 102, + 103, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 418, + 421, + 422, + 423, + 424, + 425, + 426, + 428, + 429, + 431, + 451, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 510, + 511, + "default" + ] + }, + "HttpParameter": { + "description": "extended metadata for HTTP operation parameters", + "type": "object", + "properties": { + "in": { + "description": "the location that this parameter is placed in the http request", + "enum": [ + "body", + "cookie", + "header", + "none", + "path", + "query", + "uri", + "virtual" + ], + "type": "string" + }, + "style": { + "$ref": "#/definitions/SerializationStyle", + "description": "the Serialization Style used for the parameter." + }, + "explode": { + "description": "when set, 'form' style parameters generate separate parameters for each value of an array.", + "type": "boolean" + }, + "skipUriEncoding": { + "description": "when set, this indicates that the content of the parameter should not be subject to URI encoding rules.", + "type": "boolean" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "in" + ], + "allOf": [ + { + "$ref": "#/definitions/Protocol" + } + ] + }, + "HttpRequest": { + "description": "HTTP operation protocol data", + "type": "object", + "properties": { + "path": { + "description": "A relative path to an individual endpoint.\n\nThe field name MUST begin with a slash.\nThe path is appended (no relative URL resolution) to the expanded URL from the Server Object's url field in order to construct the full URL.\nPath templating is allowed.\n\nWhen matching URLs, concrete (non-templated) paths would be matched before their templated counterparts.", + "type": "string" + }, + "uri": { + "description": "the base URI template for the operation. This will be a template that has Uri parameters to craft the base url to use.", + "type": "string" + }, + "method": { + "$ref": "#/definitions/HttpMethod", + "description": "the HTTP Method used to process this operation" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "method", + "path", + "uri" + ], + "allOf": [ + { + "$ref": "#/definitions/Protocol" + } + ] + }, + "HttpWithBodyRequest": { + "type": "object", + "properties": { + "knownMediaType": { + "$ref": "#/definitions/KnownMediaType", + "description": "a normalized value for the media type (ie, distills down to a well-known moniker (ie, 'json'))" + }, + "mediaTypes": { + "description": "must contain at least one media type to send for the body", + "type": "array", + "items": { + "type": "string" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "knownMediaType", + "mediaTypes", + "method", + "path", + "uri" + ], + "allOf": [ + { + "$ref": "#/definitions/HttpRequest" + } + ] + }, + "HttpBinaryRequest": { + "type": "object", + "properties": { + "binary": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "binary", + "knownMediaType", + "mediaTypes", + "method", + "path", + "uri" + ], + "allOf": [ + { + "$ref": "#/definitions/HttpWithBodyRequest" + } + ] + }, + "HttpMultipartRequest": { + "type": "object", + "properties": { + "multipart": { + "description": "indicates that the HTTP Request should be a multipart request\n\nie, that it has multiple requests in a single request.", + "type": "boolean", + "enum": [ + true + ] + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "knownMediaType", + "mediaTypes", + "method", + "multipart", + "path", + "uri" + ], + "allOf": [ + { + "$ref": "#/definitions/HttpWithBodyRequest" + } + ] + }, + "HttpHeader": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "schema": { + "$ref": "#/definitions/Schema" + }, + "language": { + "$ref": "#/definitions/Languages" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "header", + "language", + "schema" + ] + }, + "HttpResponse": { + "type": "object", + "properties": { + "statusCodes": { + "description": "the possible HTTP status codes that this response MUST match one of.", + "type": "array", + "items": { + "enum": [ + 100, + 101, + 102, + 103, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 418, + 421, + 422, + 423, + 424, + 425, + 426, + 428, + 429, + 431, + 451, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 510, + 511, + "default" + ] + } + }, + "knownMediaType": { + "$ref": "#/definitions/KnownMediaType", + "description": "canonical response type (ie, 'json')." + }, + "mediaTypes": { + "description": "The possible media types that this response MUST match one of.", + "type": "array", + "items": { + "type": "string" + } + }, + "headers": { + "description": "content returned by the service in the HTTP headers", + "type": "array", + "items": { + "$ref": "#/definitions/HttpHeader" + } + }, + "headerGroups": { + "description": "sets of HTTP headers grouped together into a single schema", + "type": "array", + "items": { + "$ref": "#/definitions/GroupSchema" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "statusCodes" + ], + "allOf": [ + { + "$ref": "#/definitions/Protocol" + } + ] + }, + "HttpBinaryResponse": { + "type": "object", + "properties": { + "binary": { + "description": "binary responses", + "type": "boolean", + "enum": [ + true + ] + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "binary", + "statusCodes" + ], + "allOf": [ + { + "$ref": "#/definitions/HttpResponse" + } + ] + }, + "HttpModel": { + "description": "code model metadata for HTTP protocol", + "type": "object", + "properties": { + "security": { + "description": "a collection of security requirements for the service", + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + } + }, + "defaultProperties": [], + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/Protocol" + } + ] + }, + "Record": { + "type": "object", + "defaultProperties": [], + "additionalProperties": { + "type": "object" + } + }, + "Record": { + "type": "object", + "defaultProperties": [], + "additionalProperties": false + }, + "Record": { + "type": "object", + "defaultProperties": [], + "additionalProperties": false + }, + "ConstantType": { + "type": "object", + "properties": { + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for Schema" + }, + "type": { + "$ref": "#/definitions/AllSchemaTypes", + "description": "the schema type" + }, + "summary": { + "description": "a short description", + "type": "string" + }, + "example": { + "description": "example information" + }, + "defaultValue": { + "description": "If the value isn't sent on the wire, the service will assume this" + }, + "serialization": { + "$ref": "#/definitions/SerializationFormats", + "description": "per-serialization information for this Schema" + }, + "apiVersions": { + "description": "API versions that this applies to. Undefined means all versions", + "type": "array", + "items": { + "$ref": "#/definitions/ApiVersion" + } + }, + "deprecated": { + "$ref": "#/definitions/Deprecation", + "description": "Represent the deprecation information if api is deprecated.", + "default": "undefined" + }, + "origin": { + "description": "where did this aspect come from (jsonpath or 'modelerfour:')", + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation", + "description": "External Documentation Links" + }, + "protocol": { + "$ref": "#/definitions/Protocols", + "description": "per-protocol information for this aspect" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "language", + "protocol", + "type" + ] + }, + "Record": { + "type": "object", + "defaultProperties": [], + "additionalProperties": { + "$ref": "#/definitions/ComplexSchema" + } + }, + "Record": { + "type": "object", + "defaultProperties": [], + "additionalProperties": { + "type": "string" + } + }, + "KnownMediaType": { + "enum": [ + "binary", + "form", + "json", + "multipart", + "text", + "unknown", + "xml" + ], + "type": "string" + } + }, + "description": "the model that contains all the information required to generate a service api", + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/Info", + "description": "Code model information" + }, + "schemas": { + "$ref": "#/definitions/Schemas", + "description": "All schemas for the model" + }, + "operationGroups": { + "description": "All operations", + "type": "array", + "items": { + "$ref": "#/definitions/OperationGroup" + } + }, + "globalParameters": { + "description": "all global parameters (ie, ImplementationLocation = client )", + "type": "array", + "items": { + "$ref": "#/definitions/Parameter" + } + }, + "security": { + "$ref": "#/definitions/Security" + }, + "language": { + "$ref": "#/definitions/Languages", + "description": "per-language information for this aspect" + }, + "protocol": { + "$ref": "#/definitions/Protocols", + "description": "per-protocol information for this aspect" + }, + "extensions": { + "$ref": "#/definitions/Record", + "description": "additional metadata extensions dictionary" + } + }, + "defaultProperties": [], + "additionalProperties": false, + "required": [ + "info", + "language", + "operationGroups", + "protocol", + "schemas", + "security" + ], + "title": "CodeModel" +} \ No newline at end of file diff --git a/logger/autorest.csharp/build/CodeGeneration.targets b/logger/autorest.csharp/build/CodeGeneration.targets new file mode 100644 index 0000000..cff7487 --- /dev/null +++ b/logger/autorest.csharp/build/CodeGeneration.targets @@ -0,0 +1,99 @@ + + + + pwsh + <_TypeSpecProjectSyncAndGenerateCommand>npx --no-install --package=@azure-tools/typespec-client-generator-cli --yes tsp-client update --no-prompt --output-dir $(MSBuildProjectDirectory)/../ + <_TypeSpecProjectGenerateCommand>npx --no-install --package=@azure-tools/typespec-client-generator-cli --yes tsp-client generate --no-prompt --output-dir $(MSBuildProjectDirectory)/../ + <_DefaultInputName Condition="Exists('$(MSBuildProjectDirectory)/autorest.md')">$(MSBuildProjectDirectory)/autorest.md + $(MSBuildProjectDirectory)/../tsp-location.yaml + $(_DefaultInputName) + $(MSBuildProjectDirectory)/autorest.tests.md + $(MSBuildThisFileDirectory)../tools/autorest/entrypoints/app.js + + + + + true + $(MSBuildThisFileDirectory)../content/Azure.Core.Shared/ + $(MSBuildThisFileDirectory)../content/Generator.Shared/ + $(MSBuildThisFileDirectory)../content/Management.Shared/ + + <_GenerateCode Condition="'$(AutoRestInput)' != '' OR '$(TypeSpecInput)' != ''">true + true + <_AutoRestCommand>npx autorest@$(AutoRestVersion) --max-memory-size=8192 --skip-csproj --skip-upgrade-check --version=$(AutoRestCoreVersion) $(AutoRestInput) $(AutoRestAdditionalParameters) --use=$(MSBuildThisFileDirectory)../tools/net8.0/any/ --clear-output-folder=true --shared-source-folders="$(AzureCoreSharedCodeDirectory);$(AutoRestSharedCodeDirectory)" + <_AutoRestCommand Condition="'$(UseDefaultNamespaceAndOutputFolder)' == 'true'">$(_AutoRestCommand) --output-folder=$(MSBuildProjectDirectory)/Generated --namespace=$(RootNamespace) + <_SaveInputs Condition="'$(SaveInputs)' == 'true'">--save-inputs + + $(TypespecAdditionalOptions)%3Bgenerate-test-project=true + generate-test-project=true + <_TypespecAdditionalOptions Condition="'$(TypespecAdditionalOptions)' != ''">--emitter-options "$(TypespecAdditionalOptions)" + <_LocalSpecRepo Condition="'$(LocalSpecRepo)' != ''">--local-spec-repo $(LocalSpecRepo) + <_Debug Condition="'$(Debug)' == 'true'">"--debug" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(NoWarn);CA1812 + + + + + + + + + + + + diff --git a/logger/autorest.csharp/common/AutoRest/Communication/IPluginCommunication.cs b/logger/autorest.csharp/common/AutoRest/Communication/IPluginCommunication.cs new file mode 100644 index 0000000..1084679 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/IPluginCommunication.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication.Serialization.Models; + +namespace AutoRest.CSharp.AutoRest.Communication +{ + internal interface IPluginCommunication + { + string PluginName { get; } + Task ReadFile(string filename); + Task GetValue(string key); + Task ListInputs(string? artifactType = null); + Task WriteFile(string filename, string content, string artifactType, RawSourceMap? sourceMap = null); + Task Fatal(string text); + Task Warning(string text); + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/JsonRpcCommunication.cs b/logger/autorest.csharp/common/AutoRest/Communication/JsonRpcCommunication.cs new file mode 100644 index 0000000..eab44ed --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/JsonRpcCommunication.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication.MessageHandling; +using AutoRest.CSharp.AutoRest.Communication.Serialization; +using AutoRest.CSharp.AutoRest.Communication.Serialization.Models; + +namespace AutoRest.CSharp.AutoRest.Communication +{ + internal class JsonRpcCommunication : IPluginCommunication + { + private readonly JsonRpcConnection _connection; + private readonly string _sessionId; + public string PluginName { get; } + + public JsonRpcCommunication(JsonRpcConnection connection, string pluginName, string sessionId) + { + _connection = connection; + PluginName = pluginName; + _sessionId = sessionId; + } + + // Basic Interfaces + public Task ReadFile(string filename) => ProcessRequest(requestId => OutgoingMessageSerializer.ReadFile(requestId, _sessionId, filename)); + public Task GetValue(string key) => ProcessRequest(requestId => OutgoingMessageSerializer.GetValue(requestId, _sessionId, key)); + public Task ListInputs(string? artifactType = null) => ProcessRequest(requestId => OutgoingMessageSerializer.ListInputs(requestId, _sessionId, artifactType)); + public Task ProtectFiles(string path) => ProcessRequest(requestId => OutgoingMessageSerializer.ProtectFiles(requestId, _sessionId, path)); + public Task Message(IMessage message) => _connection.Notification(OutgoingMessageSerializer.Message(_sessionId, message)); + + public Task WriteFile(string filename, string content, string artifactType, RawSourceMap? sourceMap = null) => + _connection.Notification(OutgoingMessageSerializer.WriteFile(_sessionId, filename, content, artifactType, sourceMap)); + public Task WriteFile(string filename, string content, string artifactType, Mapping[] sourceMap) => + _connection.Notification(OutgoingMessageSerializer.WriteFile(_sessionId, filename, content, artifactType, sourceMap)); + + public Task Fatal(string text) + { + return Message(text, Channel.Fatal); + } + + public Task Warning(string text) + { + return Message(text, Channel.Warning); + } + + // Convenience Interfaces + public Task Message(string text, Channel channel = Channel.Warning) => Message(new Message { Channel = channel, Text = text }); + + private Task ProcessRequest(Func requestMethod) + { + var requestId = Guid.NewGuid().ToString(); + return _connection.Request(requestId, requestMethod(requestId)); + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/IncomingMessageHandler.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/IncomingMessageHandler.cs new file mode 100644 index 0000000..a4c2a24 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/IncomingMessageHandler.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Text.Json; +using AutoRest.CSharp.AutoRest.Communication.MessageHandling.Models; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling +{ + internal delegate void IncomingRequestProcess(IncomingRequest request); + internal delegate void IncomingResponseProcess(IncomingResponse request); + + internal class IncomingMessageHandler + { + private readonly PeekableBinaryStream _stream; + private readonly IncomingRequestProcess _requestProcess; + private readonly IncomingResponseProcess _responseProcess; + + public IncomingMessageHandler(PeekableBinaryStream stream, IncomingRequestProcess requestProcess, IncomingResponseProcess responseProcess) + { + _stream = stream; + _requestProcess = requestProcess; + _responseProcess = responseProcess; + } + + public bool ProcessStream() + { + var currentByte = _stream.CurrentByte; + if (currentByte == null) return false; + + if (currentByte.IsJsonBlock()) + { + ProcessMessage(_stream.ReadJson()); + return true; + } + + return ProcessHeaders(); + } + + private bool ProcessHeaders() + { + var headers = _stream.ReadAllAsciiLines(l => !l.IsNullOrWhiteSpace()).Select(l => + { + var parts = l!.Split(":", 2).Select(p => p.Trim()).ToArray(); + return (Key: parts[0], Value: parts[1]); + }).ToDictionary(h => h.Key, h => h.Value); + + // After the headers are read, the next byte should be the content block. + if (_stream.CurrentByte.IsJsonBlock() && headers.TryGetValue("Content-Length", out var value) && Int32.TryParse(value, out var contentLength)) + { + ProcessMessage(_stream.ReadJson(contentLength)); + return true; + } + return false; + } + + // Determines if the incoming message is a request or a response. + private void ProcessMessage(JsonElement? element) + { + if (element == null || element.Value.ValueKind != JsonValueKind.Object) return; + + var properties = element.Value.EnumerateObject().Select(p => (JsonProperty?)p).ToArray(); + var id = properties.GetPropertyOrNull("id")?.Value.ToString(); + var method = properties.GetPropertyOrNull("method")?.Value.GetString(); + if (!method.IsNullOrEmpty()) + { + var parameters = properties.GetPropertyOrNull("params")?.Value; + var request = new IncomingRequest { Id = id, Method = method, Params = parameters }; + _requestProcess(request); + return; + } + + var result = properties.GetPropertyOrNull("result")?.Value.GetRawText(); + if (!result.IsNullOrEmpty()) + { + var response = new IncomingResponse { Id = id, Result = result }; + _responseProcess(response); + } + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/JsonRpcConnection.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/JsonRpcConnection.cs new file mode 100644 index 0000000..55aea45 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/JsonRpcConnection.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication.MessageHandling.Models; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling +{ + internal delegate string IncomingRequestAction(JsonRpcConnection connection, IncomingRequest request); + +#pragma warning disable IDE0069 // Disposable fields should be disposed + internal sealed class JsonRpcConnection : IDisposable + { + private readonly Stream _outputStream; + private readonly PeekableBinaryStream _inputStream; + private readonly Task _listener; + + public CancellationTokenSource CancellationTokenSource { get; private set; } = new CancellationTokenSource(); + private readonly CancellationToken _cancellationToken; + + private readonly ConcurrentDictionary> _responses = new ConcurrentDictionary>(); + private readonly Dictionary _incomingRequestActions; + private readonly IncomingMessageHandler _incomingMessageHandler; + private readonly OutgoingMessageHandler _outgoingMessageHandler; + + public JsonRpcConnection(Stream inputStream, Stream outputStream, Dictionary? incomingRequestActions = null) + { + _cancellationToken = CancellationTokenSource.Token; + _inputStream = new PeekableBinaryStream(inputStream); + _outputStream = outputStream; + _incomingRequestActions = incomingRequestActions ?? new Dictionary(); + _incomingMessageHandler = new IncomingMessageHandler(_inputStream, HandleIncomingRequest, HandleIncomingResponse); + _outgoingMessageHandler = new OutgoingMessageHandler(_outputStream, _cancellationToken); + _listener = Task.Factory.StartNew(Listen).Unwrap(); + } + + public void Start() => _listener.GetAwaiter().GetResult(); + + private Task Listen() + { + bool IsAlive() => !_cancellationToken.IsCancellationRequested; + while (IsAlive() && _incomingMessageHandler.ProcessStream()) { } + return Task.FromResult(false); + } + + private void HandleIncomingRequest(IncomingRequest request) + { + Task.Factory.StartNew(() => + { + if (_incomingRequestActions.TryGetValue(request.Method ?? String.Empty, out var requestAction)) + { + var result = requestAction(this, request); + if (!request.Id.IsNullOrEmpty()) + { + _outgoingMessageHandler.Respond(request.Id!, result).GetAwaiter().GetResult(); + } + } + }, _cancellationToken); + } + + private void HandleIncomingResponse(IncomingResponse response) + { + Task.Factory.StartNew(() => + { + if (!response.Id.IsNullOrEmpty()) + { + _responses.Remove(response.Id!, out var responseTask); + responseTask?.TrySetResult(response.Result ?? String.Empty); + } + }, _cancellationToken); + } + + public async Task Notification(string json) => await _outgoingMessageHandler.Send(json).ConfigureAwait(false); + + public async Task Request(string id, string json) + { + var response = new TaskCompletionSource(); + _responses.AddOrUpdate(id, response, (k, e) => response); + await _outgoingMessageHandler.Send(json).ConfigureAwait(false); + return (await response.Task.ConfigureAwait(false)).Parse().ToType(); + } + + public void Dispose() + { + foreach (var t in _responses.Values) + { + t.SetCanceled(); + } + + _outputStream?.Dispose(); + _inputStream?.Dispose(); + CancellationTokenSource?.Dispose(); + } + } +#pragma warning restore IDE0069 // Disposable fields should be disposed +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/Models/IncomingRequest.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/Models/IncomingRequest.cs new file mode 100644 index 0000000..749a340 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/Models/IncomingRequest.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text.Json; + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling.Models +{ + internal class IncomingRequest + { + public string JsonRpc { get; } = "2.0"; + public string? Method { get; set; } + public JsonElement? Params { get; set; } + public string? Id { get; set; } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/Models/IncomingResponse.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/Models/IncomingResponse.cs new file mode 100644 index 0000000..a6f22f0 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/Models/IncomingResponse.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling.Models +{ + internal class IncomingResponse + { + public string JsonRpc { get; } = "2.0"; + public string? Result { get; set; } + public string? Id { get; set; } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/OutgoingMessageHandler.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/OutgoingMessageHandler.cs new file mode 100644 index 0000000..735ddb2 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/OutgoingMessageHandler.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication.Serialization; + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling +{ +#pragma warning disable IDE0069 // Disposable fields should be disposed + internal class OutgoingMessageHandler : IDisposable + { + private readonly Stream _stream; + private readonly CancellationToken _cancellationToken; + private readonly Semaphore _streamSemaphore = new Semaphore(1, 1); + + public OutgoingMessageHandler(Stream stream, CancellationToken cancellationToken) + { + _stream = stream; + _cancellationToken = cancellationToken; + } + + public async Task Send(string json) + { + _streamSemaphore.WaitOne(); + + var buffer = Encoding.UTF8.GetBytes(json); + var header = Encoding.ASCII.GetBytes(OutgoingMessageSerializer.Header(buffer.Length)); + await _stream.WriteAsync(header, 0, header.Length, _cancellationToken); + await _stream.WriteAsync(buffer, 0, buffer.Length, _cancellationToken); + + _streamSemaphore.Release(); + } + + public async Task Respond(string id, string json) + { + await Send(OutgoingMessageSerializer.Response(id, json)).ConfigureAwait(false); + } + + public void Dispose() + { + _streamSemaphore?.Dispose(); + } + } +#pragma warning restore IDE0069 // Disposable fields should be disposed +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/PeekableBinaryStream.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/PeekableBinaryStream.cs new file mode 100644 index 0000000..1489984 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/PeekableBinaryStream.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling +{ + [SuppressMessage("ReSharper", "IdentifierTypo")] + internal class PeekableBinaryStream : IDisposable + { +#pragma warning disable IDE0069 // Disposable fields should be disposed + private readonly Stream _stream; +#pragma warning restore IDE0069 // Disposable fields should be disposed + + private byte? _currentByte; + public byte? CurrentByte + { + get + { + if (_currentByte.HasValue) return _currentByte.Value; + + var newByte = GetByte(); + if (newByte.HasValue) + { + _currentByte = newByte; + } + return newByte; + } + } + + private const int EndOfStream = -1; + + public PeekableBinaryStream(Stream stream) + { + _stream = stream; + } + + public void Dispose() + { + _stream.Dispose(); + } + + private byte? PopCurrentByte() + { + if (!_currentByte.HasValue) return null; + + var result = _currentByte.Value; + _currentByte = null; + return result; + } + + // Pops the current byte or reads a new one if there is no current byte + // Returns null if end-of-stream + private byte? GetByte() + { + if (_currentByte.HasValue) return PopCurrentByte(); + + var streamByte = _stream.ReadByte(); + return streamByte != EndOfStream ? (byte?)streamByte : null; + } + + public byte[] ReadBytes(int count) + { + var buffer = new byte[count]; + var index = 0; + if (count > 0 && _currentByte.HasValue) + { + // ReSharper disable once PossibleInvalidOperationException + // ReSharper disable once PossibleNullReferenceException + buffer[index++] = PopCurrentByte()!.Value; + } + while (index < count) + { + index += _stream.Read(buffer, index, count - index); + } + return buffer; + } + + public string? ReadAsciiLine() + { + var sb = new StringBuilder(); + byte? character; + while ((character = GetByte()).HasValue && character != '\r' && character != '\n') + { + sb.Append((char)character.Value); + } + + // CurrentByte will only ever be null or a non-line-ending value when this method returns since we read until line ending characters, + // and discard the last value if it is a '\n' preceded by a '\r'. + if (character == '\r' && CurrentByte == '\n') + { + GetByte(); + } + + return sb.Length != 0 ? sb.ToString() : null; + } + + public IEnumerable ReadAllAsciiLines(Predicate? condition = null) + { + condition ??= s => s == null; + string? line; + while (condition(line = ReadAsciiLine())) + { + yield return line; + } + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/PeekableBinaryStreamExtensions.cs b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/PeekableBinaryStreamExtensions.cs new file mode 100644 index 0000000..dc3ac54 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/MessageHandling/PeekableBinaryStreamExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.MessageHandling +{ + [SuppressMessage("ReSharper", "IdentifierTypo")] + internal static class PeekableBinaryStreamExtensions + { + public static bool IsJsonBlock(this byte? value) => '{' == value || '[' == value; + + public static JsonElement? ReadJson(this PeekableBinaryStream stream, int contentLength) => Encoding.UTF8.GetString(stream.ReadBytes(contentLength)).Parse(); + public static JsonElement? ReadJson(this PeekableBinaryStream stream) + { + var sb = new StringBuilder(); + // ReSharper disable once IteratorMethodResultIsIgnored + stream.ReadAllAsciiLines(l => sb.Append(l).ToString().Parse() != null); + return sb.ToString().Parse(); + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/IncomingMessageSerializer.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/IncomingMessageSerializer.cs new file mode 100644 index 0000000..6fc1e63 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/IncomingMessageSerializer.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Threading; +using AutoRest.CSharp.AutoRest.Communication.MessageHandling; +using AutoRest.CSharp.AutoRest.Communication.MessageHandling.Models; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization +{ + internal delegate bool ProcessAction(JsonRpcConnection connection, string pluginName, string sessionId); + + internal static class IncomingMessageSerializer + { + public static string GetPluginNames(this IncomingRequest _, params string[] pluginNames) => pluginNames.ToJsonArray(); + + public static string Process(this IncomingRequest request, JsonRpcConnection connection, ProcessAction processAction) + { + var parameters = request.Params.ToStringArray(); + var (pluginName, sessionId) = (parameters![0], parameters![1]); + return processAction(connection, pluginName, sessionId).ToJsonBool(); + } + + public static string Shutdown(this IncomingRequest _, CancellationTokenSource tokenSource) + { + tokenSource.Cancel(); + return String.Empty; + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactMapping.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactMapping.cs new file mode 100644 index 0000000..ed79f21 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactMapping.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class ArtifactMapping : IArtifact + { + public string? Uri { get; set; } + public string? Type { get; set; } + public string? Content { get; set; } + public Mapping[]? SourceMap { get; set; } = null; + + public override string ToString() => $@"{{""uri"":""{Uri}"",""type"":""{Type}"",""content"":""{Content.ToStringLiteral()}""{SourceMap.TextOrEmpty($@",""sourceMap"":{SourceMap.ToJsonArray()}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactMessage.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactMessage.cs new file mode 100644 index 0000000..d3031ad --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactMessage.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class ArtifactMessage : IMessage + { + public Channel Channel { get; set; } + public string[]? Key { get; set; } = null; + public IArtifact? Details { get; set; } + public string? Text { get; set; } + public SourceLocation[]? Source { get; set; } = null; + + public override string ToString() => + $@"{{""Channel"":""{Channel.ToString().ToLowerInvariant()}""{Key.TextOrEmpty($@",""Key"":{Key.ToJsonArray()}")},""Details"":{Details},""Text"":""{Text.ToStringLiteral()}""{Source.TextOrEmpty($@",""Source"":{Source.ToJsonArray()}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactRaw.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactRaw.cs new file mode 100644 index 0000000..1f66d02 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/ArtifactRaw.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class ArtifactRaw : IArtifact + { + public string? Uri { get; set; } + public string? Type { get; set; } + public string? Content { get; set; } + public RawSourceMap? SourceMap { get; set; } = null; + + public override string ToString() => $@"{{""uri"":""{Uri}"",""type"":""{Type}"",""content"":""{Content.ToStringLiteral()}""{SourceMap.TextOrEmpty($@",""sourceMap"":{SourceMap}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Channel.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Channel.cs new file mode 100644 index 0000000..9f61d97 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Channel.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + // The Channel that a message is registered with. + internal enum Channel + { + // Information is considered the mildest of responses; not necessarily actionable. + Information, + // Warnings are considered important for best practices, but not catastrophic in nature. + Warning, + // Errors are considered blocking issues that block a successful operation. + Error, + // Debug messages are designed for the developer to communicate internal AutoRest implementation details. + Debug, + // Verbose messages give the user additional clarity on the process. + Verbose, + // Catastrophic failure, likely aborting the process. + Fatal, + // Hint messages offer guidance or support without forcing action. + Hint, + // File represents a file output from an extension. Details are a Artifact and are required. + File, + // Content represents an update/creation of a configuration file. The final URI will be in the same folder as the primary config file. + Configuration, + // Protect is a path to not remove during a clear-output-folder. + Protect + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IArtifact.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IArtifact.cs new file mode 100644 index 0000000..6252ce5 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IArtifact.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal interface IArtifact { } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IMessage.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IMessage.cs new file mode 100644 index 0000000..840b5b7 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IMessage.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal interface IMessage { } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IPosition.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IPosition.cs new file mode 100644 index 0000000..496d2d3 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/IPosition.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal interface IPosition { } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Mapping.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Mapping.cs new file mode 100644 index 0000000..925a60b --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Mapping.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class Mapping + { + public Position? Generated { get; set; } + public Position? Original { get; set; } + public string? Source { get; set; } + public string? Name { get; set; } = null; + + public override string ToString() => $@"{{""generated"":{Generated},""original"":{Original},""source"":""{Source}""{Name.TextOrEmpty($@",""name"":""{Name}""")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Message.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Message.cs new file mode 100644 index 0000000..7de6eed --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Message.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class Message : IMessage + { + public Channel Channel { get; set; } + public string[]? Key { get; set; } = null; + public object? Details { get; set; } = null; + public string? Text { get; set; } + public SourceLocation[]? Source { get; set; } = null; + + public override string ToString() => + $@"{{""Channel"":""{Channel.ToString().ToLowerInvariant()}""{Key.TextOrEmpty($@",""Key"":{Key.ToJsonArray()}")}{Details.TextOrEmpty($@",""Details"":{Details}")},""Text"":""{Text.ToStringLiteral()}""{Source.TextOrEmpty($@",""Source"":{Source.ToJsonArray()}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Position.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Position.cs new file mode 100644 index 0000000..e2f7e09 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/Position.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class Position : IPosition + { + // 1-based + public int Line { get; set; } + // 0-based + public int Column { get; set; } + + public override string ToString() => $@"{{""line"":{Line},""column"":{Column}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/PositionIntPath.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/PositionIntPath.cs new file mode 100644 index 0000000..97139a2 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/PositionIntPath.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class PositionIntPath : IPosition + { + public int[]? Path { get; set; } = null; + + public override string ToString() => $@"{{{Path.TextOrEmpty($@"""path"":{Path.ToJsonArray()}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/PositionStringPath.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/PositionStringPath.cs new file mode 100644 index 0000000..6479e90 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/PositionStringPath.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class PositionStringPath : IPosition + { + public string[]? Path { get; set; } = null; + + public override string ToString() => $@"{{{Path.TextOrEmpty($@"""path"":{Path.ToJsonArray()}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/RawSourceMap.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/RawSourceMap.cs new file mode 100644 index 0000000..c297604 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/RawSourceMap.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class RawSourceMap + { + public string? File { get; set; } = null; + public string? SourceRoot { get; set; } = null; + public string? Version { get; set; } + public string[]? Sources { get; set; } + public string[]? Names { get; set; } + public string[]? SourcesContent { get; set; } = null; + public string? Mappings { get; set; } + + public override string ToString() => $@"{{""version"":""{Version}"",""sources"":{Sources.ToJsonArray()},""names"":{Names.ToJsonArray()},""mappings"":""{Mappings}""{File.TextOrEmpty($@",""file"":""{File}""")}{SourceRoot.TextOrEmpty($@",""sourceRoot"":""{SourceRoot}""")}{SourcesContent.TextOrEmpty($@",""sourcesContent"":{SourcesContent.ToJsonArray()}")}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/SourceLocation.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/SourceLocation.cs new file mode 100644 index 0000000..4087d56 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/Models/SourceLocation.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization.Models +{ + internal class SourceLocation + { + public string? Document { get; set; } + public IPosition? Position { get; set; } + + public override string ToString() => $@"{{""document"":""{Document}"",""Position"":{Position}}}"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/Serialization/OutgoingMessageSerializer.cs b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/OutgoingMessageSerializer.cs new file mode 100644 index 0000000..8c3ffa0 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/Serialization/OutgoingMessageSerializer.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.AutoRest.Communication.Serialization.Models; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Communication.Serialization +{ + internal static class OutgoingMessageSerializer + { + private const string BasicRequestFormat = @"{{""jsonrpc"":""2.0"",""method"":{1},""params"":[{2},{3}],""id"":{0}}}"; + private const string BasicNotificationFormat = @"{{""jsonrpc"":""2.0"",""method"":{0},""params"":[{1},{2}]}}"; + + private static string CreateRequestString(string requestId, string method, string sessionId, string? other) => + String.Format(BasicRequestFormat, requestId.ToJsonStringOrNull(), method.ToJsonStringOrNull(), sessionId.ToJsonStringOrNull(), other.ToStringLiteral().ToJsonStringOrNull()); + + public static string ReadFile(string requestId, string sessionId, string filename) => CreateRequestString(requestId, nameof(ReadFile), sessionId, filename); + public static string GetValue(string requestId, string sessionId, string key) => CreateRequestString(requestId, nameof(GetValue), sessionId, key); + public static string ListInputs(string requestId, string sessionId, string? artifactType = null) => CreateRequestString(requestId, nameof(ListInputs), sessionId, artifactType); + public static string ProtectFiles(string requestId, string sessionId, string path) => CreateRequestString(requestId, nameof(ProtectFiles), sessionId, path); + public static string Message(string sessionId, IMessage message) => String.Format(BasicNotificationFormat, nameof(Message).ToJsonStringOrNull(), sessionId.ToJsonStringOrNull(), message); + + // Custom Messages + public static string WriteFile(string sessionId, string filename, string content, string artifactType, RawSourceMap? sourceMap = null) + { + var artifact = new ArtifactRaw { Content = content, SourceMap = sourceMap, Type = artifactType, Uri = filename }; + var message = new ArtifactMessage { Channel = Channel.File, Details = artifact, Text = String.Empty }; + return Message(sessionId, message); + } + public static string WriteFile(string sessionId, string filename, string content, string artifactType, Mapping[] sourceMap) + { + var artifact = new ArtifactMapping { Content = content, SourceMap = sourceMap, Type = artifactType, Uri = filename }; + var message = new ArtifactMessage { Channel = Channel.File, Details = artifact, Text = String.Empty }; + return Message(sessionId, message); + } + + private const string BasicResponseFormat = @"{{""jsonrpc"":""2.0"",""result"":{1},""id"":{0}}}"; + + public static string Response(string responseId, string result) => String.Format(BasicResponseFormat, responseId.ToJsonStringOrNull(), result); + public static string Header(int contentLength) => $"Content-Length: {contentLength}\r\n\r\n"; + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Communication/StandaloneGeneratorRunner.cs b/logger/autorest.csharp/common/AutoRest/Communication/StandaloneGeneratorRunner.cs new file mode 100644 index 0000000..14bca34 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Communication/StandaloneGeneratorRunner.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Plugins; +using AutoRest.CSharp.Common.AutoRest.Plugins; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.AutoRest.Communication +{ + internal class StandaloneGeneratorRunner + { + private static readonly string[] keepFiles = new string[] { "CodeModel.yaml", "Configuration.json", "tspCodeModel.json" }; + public static async Task RunAsync(CommandLineOptions options) + { + string? projectPath = null; + string outputPath; + string generatedTestOutputPath; + bool wasProjectPathPassedIn = options.ProjectPath is not null; + if (options.Standalone is not null) + { + //TODO this is only here for back compat we should consider removing it + outputPath = options.Standalone; + } + else + { + projectPath = options.ProjectPath!; + if (!projectPath!.EndsWith("src", StringComparison.Ordinal)) + projectPath = Path.Combine(projectPath, "src"); + outputPath = Path.Combine(projectPath, "Generated"); + } + generatedTestOutputPath = Path.Combine(outputPath, "..", "..", "tests", "Generated"); + var configurationPath = options.ConfigurationPath; + if (configurationPath == null) + { + configurationPath = Path.Combine(outputPath, "Configuration.json"); + if (!File.Exists(configurationPath)) + { + configurationPath = Path.Combine(outputPath, "..", "..", "Configuration.json"); + } + } + LoadConfiguration(projectPath, outputPath, options.ExistingProjectFolder, File.ReadAllText(configurationPath)); + + var codeModelInputPath = Path.Combine(outputPath, "CodeModel.yaml"); + var tspInputFile = Path.Combine(outputPath, "tspCodeModel.json"); + if (!File.Exists(tspInputFile)) + { + tspInputFile = Path.Combine(outputPath, "..", "..", "tspCodeModel.json"); + } + + GeneratedCodeWorkspace workspace; + if (File.Exists(tspInputFile)) + { + var json = await File.ReadAllTextAsync(tspInputFile); + var rootNamespace = TypeSpecSerialization.Deserialize(json) ?? throw new InvalidOperationException($"Deserializing {tspInputFile} has failed."); + workspace = await new CSharpGen().ExecuteAsync(rootNamespace); + if (options.IsNewProject) + { + bool needAzureKeyAuth = rootNamespace.Auth?.ApiKey != null; + // TODO - add support for DataFactoryElement lookup + await new NewProjectScaffolding(needAzureKeyAuth, false).Execute(); + } + } + else if (File.Exists(codeModelInputPath)) + { + var yaml = await File.ReadAllTextAsync(codeModelInputPath); + var codeModel = CodeModelSerialization.DeserializeCodeModel(yaml); + workspace = await new CSharpGen().ExecuteAsync(codeModel); + if (options.IsNewProject) + { + bool needAzureKeyAuth = codeModel.Security.Schemes.Any(scheme => scheme is KeySecurityScheme); + bool includeDfe = yaml.Contains("x-ms-format: dfe-", StringComparison.Ordinal); + await new NewProjectScaffolding(needAzureKeyAuth, includeDfe).Execute(); + } + } + else + { + throw new InvalidOperationException($"Neither CodeModel.yaml nor tspCodeModel.json exist in {outputPath} folder."); + } + + if (options.ClearOutputFolder) + { + DeleteDirectory(outputPath, keepFiles); + DeleteDirectory(generatedTestOutputPath, keepFiles); + } + + await foreach (var file in workspace.GetGeneratedFilesAsync()) + { + if (string.IsNullOrEmpty(file.Text)) + { + continue; + } + var filename = Path.Combine(outputPath, file.Name); + Console.WriteLine($"Writing {filename}"); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Text); + } + } + + private static void DeleteDirectory(string path, string[] keepFiles) + { + var directoryInfo = new DirectoryInfo(path); + if (!directoryInfo.Exists) + { + return; + } + foreach (FileInfo file in directoryInfo.GetFiles()) + { + if (keepFiles.Contains(file.Name)) + { + continue; + } + file.Delete(); + } + + foreach (DirectoryInfo directory in directoryInfo.GetDirectories()) + { + DeleteDirectory(directory.FullName, keepFiles); + } + + if (!directoryInfo.EnumerateFileSystemInfos().Any()) + { + directoryInfo.Delete(); + } + } + + internal static void LoadConfiguration(string? projectPath, string outputPath, string? existingProjectFolder, string json) + { + var root = JsonDocument.Parse(json).RootElement; + Configuration.LoadConfiguration(root, projectPath, outputPath, existingProjectFolder); + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/CSharpGen.cs b/logger/autorest.csharp/common/AutoRest/Plugins/CSharpGen.cs new file mode 100644 index 0000000..0661338 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/CSharpGen.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication; +using AutoRest.CSharp.Common.AutoRest.Plugins; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + [PluginName("csharpgen")] + internal class CSharpGen : IPlugin + { + public async Task ExecuteAsync(CodeModel codeModel) + { + ValidateConfiguration(); + + Directory.CreateDirectory(Configuration.OutputFolder); + var project = await GeneratedCodeWorkspace.Create(Configuration.AbsoluteProjectFolder, Configuration.OutputFolder, Configuration.SharedSourceFolders); + var sourceInputModel = new SourceInputModel(await project.GetCompilationAsync()); + + var schemaUsageProvider = new SchemaUsageProvider(codeModel); // Create schema usage before transformation applied + if (Configuration.Generation1ConvenienceClient) + { + CodeModelTransformer.TransformForDataPlane(codeModel); + var inputNamespace = new CodeModelConverter(codeModel, schemaUsageProvider).CreateNamespace(); + DataPlaneTarget.Execute(project, inputNamespace, sourceInputModel); + } + else if (Configuration.AzureArm) + { + CodeModelTransformer.TransformForMgmt(codeModel); + var inputNamespace = new CodeModelConverter(codeModel, schemaUsageProvider).CreateNamespace(); + MgmtContext.Initialize(new BuildContext(inputNamespace, sourceInputModel)); + await MgmtTarget.ExecuteAsync(project); + if (Configuration.MgmtTestConfiguration is not null && !Configuration.MgmtConfiguration.MgmtDebug.ReportOnly) + await MgmtTestTarget.ExecuteAsync(project, inputNamespace, sourceInputModel); + GenerateMgmtReport(project); + } + else + { + var inputNamespace = new CodeModelConverter(codeModel, schemaUsageProvider).CreateNamespace(); + await LowLevelTarget.ExecuteAsync(project, inputNamespace, sourceInputModel, false); + } + return project; + } + + private void GenerateMgmtReport(GeneratedCodeWorkspace project) + { + MgmtReport.Instance.TransformSection.ForEachTransform((t, usages) => + { + string[] ignoreNoUsage = new string[] + { + TransformTypeName.AcronymMapping, + TransformTypeName.FormatByNameRules + }; + if (usages.Count == 0 && !ignoreNoUsage.Contains(t.TransformType)) + AutoRestLogger.Warning($"No usage transform detected: {t}").Wait(); + }); + if (Configuration.MgmtConfiguration.MgmtDebug.GenerateReport) + { + string report = MgmtReport.Instance.GenerateReport(Configuration.MgmtConfiguration.MgmtDebug.ReportFormat); + project.AddPlainFiles("_mgmt-codegen-report.log", report); + } + } + + public async Task ExecuteAsync(InputNamespace rootNamespace) + { + ValidateConfiguration(); + + Directory.CreateDirectory(Configuration.OutputFolder); + var project = await GeneratedCodeWorkspace.Create(Configuration.AbsoluteProjectFolder, Configuration.OutputFolder, Configuration.SharedSourceFolders); + var sourceInputModel = new SourceInputModel(await project.GetCompilationAsync(), await ProtocolCompilationInput.TryCreate()); + + if (Configuration.AzureArm) + { + InputNamespaceTransformer.Transform(rootNamespace); + MgmtContext.Initialize(new BuildContext(rootNamespace, sourceInputModel)); + await MgmtTarget.ExecuteAsync(project); + if (Configuration.GenerateSampleProject) + await MgmtTestTarget.ExecuteAsync(project, rootNamespace, sourceInputModel); + } + else + { + await LowLevelTarget.ExecuteAsync(project, rootNamespace, sourceInputModel, true); + } + return project; + } + + private static void ValidateConfiguration() + { + if (Configuration.Generation1ConvenienceClient && Configuration.AzureArm) + { + throw new Exception("Enabling both 'generation1-convenience-client' and 'azure-arm' at the same time is not supported."); + } + } + + public async Task Execute(IPluginCommunication autoRest) + { + Console.SetOut(Console.Error); //if you send anything to stdout there is an autorest error so this protects us against that happening + string? codeModelFileName = (await autoRest.ListInputs()).FirstOrDefault(); + if (string.IsNullOrEmpty(codeModelFileName)) + throw new Exception("Generator did not receive the code model file."); + + string codeModelYaml = await autoRest.ReadFile(codeModelFileName); + CodeModel codeModel = CodeModelSerialization.DeserializeCodeModel(codeModelYaml); + + Configuration.Initialize(autoRest, codeModel.Language.Default.Name, codeModel.Language.Default.Name); + + if (!Path.IsPathRooted(Configuration.OutputFolder)) + { + await AutoRestLogger.Warning("output-folder path should be an absolute path"); + } + if (Configuration.SaveInputs) + { + await autoRest.WriteFile("Configuration.json", Configuration.SaveConfiguration(), "source-file-csharp"); + await autoRest.WriteFile("CodeModel.yaml", codeModelYaml, "source-file-csharp"); + } + + try + { + // generate source code + var project = await ExecuteAsync(codeModel); + await foreach (var file in project.GetGeneratedFilesAsync()) + { + // format all \ to / in filename, otherwise they will be treated as escape char when sending to autorest service + var filename = file.Name.Replace('\\', '/'); + await autoRest.WriteFile(filename, file.Text, "source-file-csharp"); + } + + // generate csproj if necessary + if (!Configuration.SkipCSProj) + { + bool needAzureKeyAuth = codeModel.Security.Schemes.Any(scheme => scheme is KeySecurityScheme); + bool includeDfe = codeModelYaml.Contains("x-ms-format: dfe-", StringComparison.Ordinal); + if (Configuration.OutputFolder.EndsWith("/src/Generated")) + { + await new NewProjectScaffolding(needAzureKeyAuth, includeDfe).Execute(); + } + else + { + new CSharpProj(needAzureKeyAuth, includeDfe).Execute(autoRest); + } + } + } + catch (ErrorHelpers.ErrorException e) + { + await AutoRestLogger.Fatal(e.ErrorText); + return false; + } + catch (Exception) + { + try + { + if (Configuration.SaveInputs) + { + // We are unsuspectingly crashing, so output anything that might help us reproduce the issue + File.WriteAllText(Path.Combine(Configuration.OutputFolder, "Configuration.json"), Configuration.SaveConfiguration()); + File.WriteAllText(Path.Combine(Configuration.OutputFolder, "CodeModel.yaml"), codeModelYaml); + } + } + catch + { + // Ignore any errors while trying to output crash information + } + throw; + } + + return true; + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/CSharpProj.cs b/logger/autorest.csharp/common/AutoRest/Plugins/CSharpProj.cs new file mode 100644 index 0000000..77c13de --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/CSharpProj.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Reflection; +using AutoRest.CSharp.AutoRest.Communication; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + // TODO -- move this somewhere else because it is no longer a "plugin" + internal class CSharpProj + { + private readonly bool _needAzureKeyAuth; + private readonly bool _includeDfe; + + public CSharpProj(bool needAzureKeyAuth, bool includeDfe) + { + _needAzureKeyAuth = needAzureKeyAuth; + _includeDfe = includeDfe; + } + + private static string GetVersion() + { + Assembly clientAssembly = Assembly.GetExecutingAssembly(); + + AssemblyInformationalVersionAttribute? versionAttribute = clientAssembly.GetCustomAttribute(); + if (versionAttribute == null) + { + throw new InvalidOperationException($"{nameof(AssemblyInformationalVersionAttribute)} is required on client SDK assembly '{clientAssembly.FullName}'"); + } + + string version = versionAttribute.InformationalVersion; + + int hashSeparator = version.IndexOf('+'); + if (hashSeparator != -1) + { + return version.Substring(0, hashSeparator); + } + + return version; + } + + public void Execute(IPluginCommunication autoRest) + => WriteCSProjFiles(async (filename, text) => + { + await autoRest.WriteFile(Path.Combine(Configuration.RelativeProjectFolder, filename), text, "source-file-csharp"); + }); + + public void Execute() + => WriteCSProjFiles(async (filename, text) => + { + //TODO adding to workspace makes the formatting messed up since its a raw xml document + //somewhere it tries to parse it as a syntax tree and when it converts back to text + //its no longer valid xml. We should consider a "raw files" concept in the work space + //so the file writing can still remain in one place + await File.WriteAllTextAsync(Path.Combine(Configuration.AbsoluteProjectFolder, filename), text); + }); + + private void WriteCSProjFiles(Action writeFile) + { + // write src csproj + var csprojContent = Configuration.SkipCSProjPackageReference ? GetCSProj() : GetExternalCSProj(); + writeFile($"{Configuration.Namespace}.csproj", csprojContent); + + // write test csproj when needed + if (Configuration.MgmtTestConfiguration is not null) + { + var testCSProjContent = GetTestCSProj(); + string testGenProjectFolder; + if (Configuration.MgmtTestConfiguration.OutputFolder is { } testGenProjectOutputFolder) + { + testGenProjectFolder = Path.Combine(testGenProjectOutputFolder, "../"); + } + else + { + testGenProjectFolder = "../"; + } + Console.WriteLine(Path.Combine(testGenProjectFolder, $"{Configuration.Namespace}.Tests.csproj")); + writeFile(FormatPath(Path.Combine(testGenProjectFolder, $"{Configuration.Namespace}.Tests.csproj")), testCSProjContent); + } + } + + private static string FormatPath(string? path) + { + if (string.IsNullOrEmpty(path)) + return path ?? ""; + return Path.GetFullPath(path.TrimEnd('/', '\\')).Replace("\\", "/"); + } + + private string GetTestCSProj() + { + var writer = new CSProjWriter() + { + TargetFramework = "netstandard2.0", + TreatWarningsAsErrors = true, + Nullable = "annotations", + IncludeManagementSharedCode = Configuration.AzureArm ? true : null, + }; + + writer.ProjectReferences.Add(new($"..\\src\\{Configuration.Namespace}.csproj")); + + writer.PackageReferences.Add(new("NUnit")); + writer.PackageReferences.Add(new("Azure.Identity")); + + writer.CompileIncludes.Add(new("..\\..\\..\\..\\src\\assets\\TestFramework\\MockTestBase.cs")); + writer.CompileIncludes.Add(new("..\\..\\..\\..\\src\\assets\\TestFramework\\RecordedTestAttribute.cs")); + + return writer.Write(); + } + + private string GetCSProj() + { + var builder = new CSProjWriter() + { + TargetFramework = "netstandard2.0", + TreatWarningsAsErrors = true, + Nullable = "annotations", + IncludeManagementSharedCode = Configuration.AzureArm ? true : null, + DefineConstants = !Configuration.AzureArm && !Configuration.Generation1ConvenienceClient ? new("$(DefineConstants);EXPERIMENTAL") : null + }; + builder.PackageReferences.Add(new("Azure.Core")); + if (_includeDfe) + { + builder.PackageReferences.Add(new("Azure.Core.Expressions.DataFactory")); + } + + if (Configuration.AzureArm) + { + builder.PackageReferences.Add(new("Azure.ResourceManager")); + } + else if (!Configuration.Generation1ConvenienceClient) + { + builder.PackageReferences.Add(new("Azure.Core.Experimental")); + } + + if (Configuration.UseModelReaderWriter) + { + builder.PackageReferences.Add(new("System.ClientModel")); + } + + if (_needAzureKeyAuth) + { + builder.CompileIncludes.Add(new("$(AzureCoreSharedSources)AzureKeyCredentialPolicy.cs", "Shared/Core")); + } + + return builder.Write(); + } + + private string GetExternalCSProj() + { + var writer = new CSProjWriter() + { + TargetFramework = "netstandard2.0", + TreatWarningsAsErrors = true, + Nullable = "annotations", + IncludeManagementSharedCode = Configuration.AzureArm ? true : null, + DefineConstants = !Configuration.AzureArm && !Configuration.Generation1ConvenienceClient ? new("$(DefineConstants);EXPERIMENTAL") : null, + LangVersion = "11.0", + IncludeGeneratorSharedCode = true, + RestoreAdditionalProjectSources = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json" + }; + writer.PackageReferences.Add(new("Azure.Core")); + if (_includeDfe) + { + writer.PackageReferences.Add(new("Azure.Core.Expressions.DataFactory")); + } + + if (Configuration.UseModelReaderWriter) + { + writer.PackageReferences.Add(new("System.ClientModel")); + } + + var version = GetVersion(); + + writer.PrivatePackageReferences.Add(new("Microsoft.Azure.AutoRest.CSharp", version)); + + return writer.Write(); + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/GeneratedCodeWorkspace.cs b/logger/autorest.csharp/common/AutoRest/Plugins/GeneratedCodeWorkspace.cs new file mode 100644 index 0000000..322badc --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/GeneratedCodeWorkspace.cs @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.AutoRest.Plugins; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.PostProcessing; +using Azure; +using Azure.Core; +using Azure.ResourceManager; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal class GeneratedCodeWorkspace + { + public static readonly string SharedFolder = "shared"; + public static readonly string GeneratedFolder = "Generated"; + public static readonly string GeneratedTestFolder = "GeneratedTests"; + + private static readonly IReadOnlyList AssemblyMetadataReferences; + + private static readonly CSharpSyntaxRewriter SA1505Rewriter = new SA1505Rewriter(); + + static GeneratedCodeWorkspace() + { + var references = new List + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Response).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ClientResult).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ArmResource).Assembly.Location), + }; + + var trustedAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? "").Split(Path.PathSeparator); + foreach (var tpl in trustedAssemblies) + { + references.Add(MetadataReference.CreateFromFile(tpl)); + } + + AssemblyMetadataReferences = references; + } + + private static readonly string[] SharedFolders = { SharedFolder }; + private static readonly string[] GeneratedFolders = { GeneratedFolder }; + private static readonly string[] GeneratedTestFolders = { GeneratedFolder, GeneratedTestFolder }; + private static Task? _cachedProject; + + private Project _project; + private Dictionary _xmlDocFiles { get; } + private Dictionary _plainFiles { get; } + + private GeneratedCodeWorkspace(Project generatedCodeProject) + { + _project = generatedCodeProject; + _xmlDocFiles = new(); + _plainFiles = new(); + } + + /// + /// Creating AdHoc workspace and project takes a while, we'd like to preload this work + /// to the generator startup time + /// + public static void Initialize() + { + _cachedProject = Task.Run(CreateGeneratedCodeProject); + } + + public void AddGeneratedFile(string name, string text) => AddGeneratedFile(name, text, GeneratedFolders); + + public void AddGeneratedTestFile(string name, string text) => AddGeneratedFile(name, text, GeneratedTestFolders); + + private void AddGeneratedFile(string name, string text, string[] folders) + { + var document = _project.AddDocument(name, text, folders); + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + Debug.Assert(root != null); + + root = root.WithAdditionalAnnotations(Simplifier.Annotation); + document = document.WithSyntaxRoot(root); + _project = document.Project; + } + + /// + /// Add generated doc file. + /// + /// Name of the doc file, including the relative path to the "Generated" folder. + /// Content of the doc file. + public void AddGeneratedDocFile(string name, XmlDocumentFile xmlDocument) + { + _xmlDocFiles.Add(name, xmlDocument); + } + + public void AddPlainFiles(string name, string content) + { + _plainFiles.Add(name, content); + } + + public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() + { + var compilation = await _project.GetCompilationAsync(); + Debug.Assert(compilation != null); + + var suppressedTypeNames = GetSuppressedTypeNames(compilation); + List> documents = new List>(); + foreach (Document document in _project.Documents) + { + // Skip writing shared files or originals + if (!IsGeneratedDocument(document)) + { + continue; + } + + documents.Add(ProcessDocument(compilation, document, suppressedTypeNames)); + } + var docs = await Task.WhenAll(documents); + var needProcessGeneratedDocs = _xmlDocFiles.Any(); + var generatedDocs = new Dictionary(); + + foreach (var doc in docs) + { + var processed = doc; + + var text = await processed.GetSyntaxTreeAsync(); + yield return (processed.Name, text!.ToString()); + if (needProcessGeneratedDocs) // TODO -- this is a workaround. In HLC, in some cases, there are multiple documents with the same name added in this list, and we get "dictionary same key has been added" exception + generatedDocs.Add(processed.Name, text); + } + + foreach (var (docName, doc) in _xmlDocFiles) + { + var xmlWriter = doc.XmlDocWriter; + if (generatedDocs.TryGetValue(doc.TestFileName, out var testDocument)) + { + var content = await XmlFormatter.FormatAsync(xmlWriter, testDocument); + yield return (docName, content); + } + } + + foreach (var (file, content) in _plainFiles) + { + yield return (file, content); + } + } + + private async Task ProcessDocument(Compilation compilation, Document document, ImmutableHashSet suppressedTypeNames) + { + var syntaxTree = await document.GetSyntaxTreeAsync(); + if (syntaxTree != null) + { + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var modelRemoveRewriter = new MemberRemoverRewriter(_project, semanticModel, suppressedTypeNames); + document = document.WithSyntaxRoot(SA1505Rewriter.Visit(modelRemoveRewriter.Visit(await syntaxTree.GetRootAsync()))); + } + + document = await Simplifier.ReduceAsync(document); + document = await Formatter.FormatAsync(document); + return document; + } + + internal static ImmutableHashSet GetSuppressedTypeNames(Compilation compilation) + { + var suppressTypeAttribute = compilation.GetTypeByMetadataName(typeof(CodeGenSuppressTypeAttribute).FullName!)!; + return compilation.Assembly.GetAttributes() + .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, suppressTypeAttribute)) + .Select(a => a.ConstructorArguments[0].Value) + .OfType() + .ToImmutableHashSet(); + } + + /// + /// Add some additional files into this project + /// + /// + /// + /// + public void AddDirectory(string directory, Func? skipPredicate = null, IEnumerable? folders = null) + { + _project = AddDirectory(_project, directory, skipPredicate, folders); + } + + /// + /// Add the files in the directory to a project per a given predicate with the folders specified + /// + /// + /// + /// + /// + /// + internal static Project AddDirectory(Project project, string directory, Func? skipPredicate = null, IEnumerable? folders = null) + { + foreach (string sourceFile in Directory.GetFiles(directory, "*.cs", SearchOption.AllDirectories)) + { + if (skipPredicate != null && skipPredicate(sourceFile)) + continue; + + project = project.AddDocument(sourceFile, File.ReadAllText(sourceFile), folders ?? Array.Empty(), sourceFile).Project; + } + + return project; + } + + public static async Task Create(string projectDirectory, string outputDirectory, string[] sharedSourceFolders) + { + var projectTask = Interlocked.Exchange(ref _cachedProject, null); + var generatedCodeProject = projectTask != null ? await projectTask : CreateGeneratedCodeProject(); + + if (Path.IsPathRooted(projectDirectory) && Path.IsPathRooted(outputDirectory)) + { + projectDirectory = Path.GetFullPath(projectDirectory); + outputDirectory = Path.GetFullPath(outputDirectory); + + generatedCodeProject = AddDirectory(generatedCodeProject, projectDirectory, skipPredicate: sourceFile => sourceFile.StartsWith(outputDirectory)); + } + + foreach (var sharedSourceFolder in sharedSourceFolders) + { + generatedCodeProject = AddDirectory(generatedCodeProject, sharedSourceFolder, folders: SharedFolders); + } + + generatedCodeProject = generatedCodeProject.WithParseOptions(new CSharpParseOptions(preprocessorSymbols: new[] { "EXPERIMENTAL" })); + return new GeneratedCodeWorkspace(generatedCodeProject); + } + + // TODO: Currently the outputDirectory is expected to be generated folder. We will handle the customization folder if there is a case. + public static GeneratedCodeWorkspace CreateExistingCodeProject(string outputDirectory) + { + var workspace = new AdhocWorkspace(); + Project project = workspace.AddProject("ExistingCode", LanguageNames.CSharp); + + if (Path.IsPathRooted(outputDirectory)) + { + outputDirectory = Path.GetFullPath(outputDirectory); + project = AddDirectory(project, outputDirectory, null); + } + + project = project + .AddMetadataReferences(AssemblyMetadataReferences) + .WithCompilationOptions(new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Disable)); + + return new GeneratedCodeWorkspace(project); + } + + public static async Task CreatePreviousContractFromDll(string xmlDocumentationpath, string dllPath) + { + var workspace = new AdhocWorkspace(); + Project project = workspace.AddProject("PreviousContract", LanguageNames.CSharp); + project = project + .AddMetadataReferences(AssemblyMetadataReferences) + .WithCompilationOptions(new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Disable)); + project = project.AddMetadataReference(MetadataReference.CreateFromFile(dllPath, documentation: XmlDocumentationProvider.CreateFromFile(xmlDocumentationpath))); + return await project.GetCompilationAsync(); + } + + private static Project CreateGeneratedCodeProject() + { + var workspace = new AdhocWorkspace(); + // TODO: This is not the right way to construct the workspace but it works + Project generatedCodeProject = workspace.AddProject("GeneratedCode", LanguageNames.CSharp); + + generatedCodeProject = generatedCodeProject + .AddMetadataReferences(AssemblyMetadataReferences) + .WithCompilationOptions(new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Disable)); + return generatedCodeProject; + } + + public async Task GetCompilationAsync() + { + var compilation = await _project.GetCompilationAsync() as CSharpCompilation; + Debug.Assert(compilation != null); + return compilation; + } + + public static bool IsCustomDocument(Document document) => !IsGeneratedDocument(document) && !IsSharedDocument(document); + public static bool IsSharedDocument(Document document) => document.Folders.Contains(SharedFolder); + public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); + public static bool IsGeneratedTestDocument(Document document) => document.Folders.Contains(GeneratedTestFolder); + + /// + /// This method delegates the caller to do something on the generated code project + /// + /// + /// + public async Task PostProcess(Func> processor) + { + _project = await processor(_project); + } + + /// + /// This method invokes the postProcessor to do some post processing work + /// Depending on the configuration, it will either remove + internalize, just internalize or do nothing + /// + /// + /// + public async Task PostProcessAsync(PostProcessor? postProcessor = null) + { + postProcessor ??= new PostProcessor(ImmutableHashSet.Empty); + switch (Configuration.UnreferencedTypesHandling) + { + case Configuration.UnreferencedTypesHandlingOption.KeepAll: + break; + case Configuration.UnreferencedTypesHandlingOption.Internalize: + _project = await postProcessor.InternalizeAsync(_project); + break; + case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: + _project = await postProcessor.InternalizeAsync(_project); + _project = await postProcessor.RemoveAsync(_project); + break; + } + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/IPlugin.cs b/logger/autorest.csharp/common/AutoRest/Plugins/IPlugin.cs new file mode 100644 index 0000000..15dd7d0 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/IPlugin.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal interface IPlugin + { + Task Execute(IPluginCommunication autoRest); + } + + [AttributeUsage(AttributeTargets.Class)] + internal class PluginNameAttribute : Attribute + { + public string PluginName { get; } + + public PluginNameAttribute(string pluginName) + { + PluginName = pluginName; + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/MemberRemoverRewriter.cs b/logger/autorest.csharp/common/AutoRest/Plugins/MemberRemoverRewriter.cs new file mode 100644 index 0000000..c9601ee --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/MemberRemoverRewriter.cs @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ +#pragma warning disable RS1024 + internal class MemberRemoverRewriter : CSharpSyntaxRewriter + { + private static readonly SymbolDisplayFormat _fullyQualifiedNameFormat + = new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + private readonly Project _project; + private readonly SemanticModel _semanticModel; + private readonly ImmutableHashSet _suppressedTypeNames; + private readonly Dictionary> _suppressionCache; + + public MemberRemoverRewriter(Project project, SemanticModel semanticModel, ImmutableHashSet suppressedTypeNames) + { + _project = project; + _semanticModel = semanticModel; + _suppressedTypeNames = suppressedTypeNames; + _suppressionCache = new Dictionary>(); + } + + public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node) + { + var visitedNode = base.VisitCompilationUnit(node); + return visitedNode is CompilationUnitSyntax cu && !cu.Members.Any() ? SyntaxFactory.CompilationUnit() : visitedNode; + } + + public override SyntaxNode? VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) + { + var visitedNode = base.VisitNamespaceDeclaration(node); + return visitedNode is NamespaceDeclarationSyntax ns && !ns.Members.Any() ? null : visitedNode; + } + + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + => IsSuppressedType(node) ? null : base.VisitClassDeclaration(node); + + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) + => IsSuppressedType(node) ? null : base.VisitStructDeclaration(node); + + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) + => IsSuppressedType(node) ? null : base.VisitEnumDeclaration(node); + + public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + var symbol = _semanticModel.GetDeclaredSymbol(node); + return ShouldRemoveMember(symbol) ? null : base.VisitConstructorDeclaration(node); + } + + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + var symbol = _semanticModel.GetDeclaredSymbol(node); + return ShouldRemoveMember(symbol) ? null : base.VisitPropertyDeclaration(node); + } + + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + var symbol = _semanticModel.GetDeclaredSymbol(node); + return ShouldRemoveMember(symbol) ? null : base.VisitMethodDeclaration(node); + } + + public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node) + { + var symbol = _semanticModel.GetDeclaredSymbol(node); + return ShouldRemoveMember(symbol) ? null : base.VisitOperatorDeclaration(node); + } + + public override SyntaxNode? VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) + { + var symbol = _semanticModel.GetDeclaredSymbol(node); + return ShouldRemoveMember(symbol) ? null : base.VisitConversionOperatorDeclaration(node); + } + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) + { + var symbol = node.Declaration.Variables.Count == 1 + ? _semanticModel.GetDeclaredSymbol(node.Declaration.Variables[0]) + : null; + + return ShouldRemoveMember(symbol) ? null : base.VisitFieldDeclaration(node); + } + + private List? GetSupressions(INamedTypeSymbol namedTypeSymbol) + { + if (_suppressionCache.TryGetValue(namedTypeSymbol, out var suppressions)) + { + return suppressions; + } + + foreach (var attributeData in namedTypeSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.Name.Equals(CodeGenAttributes.CodeGenSuppressAttributeName) == true) + { + ValidateArguments(namedTypeSymbol, attributeData); + + suppressions ??= new List(); + var name = attributeData.ConstructorArguments[0].Value as string; + var parameterTypes = attributeData.ConstructorArguments[1].Values.Select(v => (ISymbol?)v.Value).ToArray(); + suppressions.Add(new Supression(name, parameterTypes)); + } + } + + if (suppressions != null) + { + _suppressionCache.Add(namedTypeSymbol, suppressions); + } + return suppressions; + + static void ValidateArguments(INamedTypeSymbol typeSymbol, AttributeData attributeData) + { + var arguments = attributeData.ConstructorArguments; + if (arguments.Length == 0) + { + var fullName = typeSymbol.ToDisplayString(_fullyQualifiedNameFormat); + ErrorHelpers.ThrowError($"CodeGenSuppress attribute on {fullName} must specify a method name as its first argument."); + } + + if (arguments.Length == 1 || arguments[0].Kind != TypedConstantKind.Primitive || arguments[0].Value is not string) + { + var attribute = attributeData.ApplicationSyntaxReference.GetText(); + var fullName = typeSymbol.ToDisplayString(_fullyQualifiedNameFormat); + ErrorHelpers.ThrowError($"{attribute} attribute on {fullName} must specify a method name as its first argument."); + } + + if (arguments.Length == 2 && arguments[1].Kind == TypedConstantKind.Array) + { + ValidateTypeArguments(typeSymbol, attributeData, arguments[1].Values); + } + else + { + ValidateTypeArguments(typeSymbol, attributeData, arguments.Skip(1)); + } + } + + static void ValidateTypeArguments(INamedTypeSymbol typeSymbol, AttributeData attributeData, IEnumerable arguments) + { + foreach (var argument in arguments) + { + if (argument.Kind == TypedConstantKind.Type) + { + if (argument.Value is IErrorTypeSymbol errorType) + { + var attribute = attributeData.ApplicationSyntaxReference.GetText(); + var fileLinePosition = attributeData.ApplicationSyntaxReference.GetFileLinePosition(); + var filePath = fileLinePosition.Path; + var line = fileLinePosition.StartLinePosition.Line + 1; + ErrorHelpers.ThrowError($"The undefined type '{errorType.Name}' is referenced in the '{attribute}' attribute ({filePath}, line: {line}). Please define this type or remove it from the attribute."); + } + } + else + { + var fullName = typeSymbol.ToDisplayString(_fullyQualifiedNameFormat); + var attribute = attributeData.ApplicationSyntaxReference.GetText(); + ErrorHelpers.ThrowError($"Argument '{argument.ToCSharpString()}' in attribute '{attribute}' applied to '{fullName}' must be a type."); + } + } + } + } + + private bool IsSuppressedType(BaseTypeDeclarationSyntax typeSyntax) + { + if (_suppressedTypeNames.IsEmpty) + { + return false; + } + + var typeSymbol = _semanticModel.GetDeclaredSymbol(typeSyntax); + while (typeSymbol != null) + { + var fullName = typeSymbol.ToDisplayString(_fullyQualifiedNameFormat); + if (_suppressedTypeNames.Contains(typeSymbol.Name) || _suppressedTypeNames.Contains(fullName)) + { + return true; + } + + typeSymbol = SymbolEqualityComparer.Default.Equals(typeSymbol.BaseType?.ContainingAssembly, typeSymbol.ContainingAssembly) + ? typeSymbol.BaseType + : null; + } + + return false; + } + + private bool ShouldRemoveMember(ISymbol? symbol) + { + if (symbol != null) + { + INamedTypeSymbol? containingType = symbol.ContainingType; + IMethodSymbol? methodSymbol = symbol as IMethodSymbol; + + var suppressions = GetSupressions(symbol.ContainingType); + if (suppressions != null) + { + foreach (var suppression in suppressions) + { + if (suppression.Matches(symbol)) + { + return true; + } + } + } + + while (containingType != null) + { + var members = containingType.GetMembers(symbol.Name); + foreach (var member in members) + { + if (!member.Equals(symbol) && + IsDeclaredInNonGeneratedCode(member)) + { + if (methodSymbol != null && + member is IMethodSymbol memberMethodSymbol && + !methodSymbol.Parameters.SequenceEqual(memberMethodSymbol.Parameters, (s1, s2) => s1.Type.Equals(s2.Type))) + { + continue; + } + + return true; + } + } + + // Skip traversing parents for constructors and explicit interface implementations + if (methodSymbol != null && + (methodSymbol.MethodKind == MethodKind.Constructor || + !methodSymbol.ExplicitInterfaceImplementations.IsEmpty)) + { + break; + } + containingType = containingType.BaseType; + } + } + + return false; + } + + private bool IsDeclaredInNonGeneratedCode(ISymbol member) + { + var references = member.DeclaringSyntaxReferences; + + if (references.Length == 0) + { + return false; + } + + foreach (var reference in references) + { + Document? document = _project.GetDocument(reference.SyntaxTree); + + if (document != null && GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + return false; + } + } + + return true; + } + + private readonly struct Supression + { + private readonly string? _name; + private readonly ISymbol?[] _types; + + public Supression(string? name, ISymbol?[] types) + { + _name = name; + _types = types; + } + + public bool Matches(ISymbol symbol) + { + if (symbol is IMethodSymbol methodSymbol) + { + string name = methodSymbol.Name; + // Use friendly name for ctors + if (methodSymbol.MethodKind == MethodKind.Constructor) + { + name = methodSymbol.ContainingType.Name; + } + + return _name == name && + _types.SequenceEqual(methodSymbol.Parameters.Select(p => p.Type), SymbolEqualityComparer.Default); + } + else + { + return symbol.Name == _name; + } + } + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/NewProjectScaffolding.cs b/logger/autorest.csharp/common/AutoRest/Plugins/NewProjectScaffolding.cs new file mode 100644 index 0000000..1ddf303 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/NewProjectScaffolding.cs @@ -0,0 +1,510 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.AutoRest.Plugins +{ + internal class NewProjectScaffolding + { + private string _serviceDirectoryName; + private string _projectDirectory; + private string _testDirectory; + private string _serviceDirectory; + private bool _isAzureSdk; + private bool _needAzureKeyAuth; + private bool _includeDfe; + + public NewProjectScaffolding(bool needAzureKeyAuth, bool includeDfe) + { + _serviceDirectoryName = Path.GetFileName(Path.GetFullPath(Path.Combine(Configuration.AbsoluteProjectFolder, "..", ".."))); + _projectDirectory = Path.Combine(Configuration.AbsoluteProjectFolder, ".."); + _testDirectory = Path.Combine(Configuration.AbsoluteProjectFolder, "..", "tests"); + _serviceDirectory = Path.Combine(Configuration.AbsoluteProjectFolder, "..", ".."); + _isAzureSdk = Configuration.Namespace.StartsWith("Azure."); + _needAzureKeyAuth = needAzureKeyAuth; + _includeDfe = includeDfe; + } + + public async Task Execute() + { + if (!_isAzureSdk) + { + //clean up old sln and csproj files + foreach (var file in Directory.GetFiles(_projectDirectory, "*.csproj", SearchOption.AllDirectories)) + { + File.Delete(file); + } + foreach (var file in Directory.GetFiles(_projectDirectory, "*.sln", SearchOption.AllDirectories)) + { + File.Delete(file); + } + } + + if (_isAzureSdk) + await WriteServiceDirectoryFiles(); + + await WriteSolutionFiles(); + + await WriteProjectFiles(); + + await WriteTestFiles(); + + return true; + } + + private async Task WriteServiceDirectoryFiles() + { + //TODO handle existing ci where multiple projects are in the same service directory + string ciYmlFile = Path.Combine(_serviceDirectory, "ci.yml"); + if (!File.Exists(ciYmlFile)) + await File.WriteAllBytesAsync(ciYmlFile, Encoding.ASCII.GetBytes(GetCiYml())); + } + + private async Task WriteTestFiles() + { + if (!Configuration.GenerateTestProject && !Configuration.GenerateSampleProject) + return; + + if (_isAzureSdk) + { + Directory.CreateDirectory(Path.Combine(_testDirectory, "SessionRecords")); + } + if (!Directory.Exists(_testDirectory)) + Directory.CreateDirectory(_testDirectory); + + await File.WriteAllBytesAsync(Path.Combine(_testDirectory, $"{Configuration.Namespace}.Tests.csproj"), Encoding.ASCII.GetBytes(GetTestCSProj())); + } + + private async Task WriteProjectFiles() + { + await File.WriteAllBytesAsync(Path.Combine(Configuration.AbsoluteProjectFolder, $"{Configuration.Namespace}.csproj"), Encoding.ASCII.GetBytes(GetSrcCSProj())); + Directory.CreateDirectory(Path.Combine(Configuration.AbsoluteProjectFolder, "Properties")); + await File.WriteAllBytesAsync(Path.Combine(Configuration.AbsoluteProjectFolder, "Properties", "AssemblyInfo.cs"), Encoding.ASCII.GetBytes(GetAssemblyInfo())); + } + + private async Task WriteSolutionFiles() + { + await File.WriteAllBytesAsync(Path.Combine(_projectDirectory, $"{Configuration.Namespace}.sln"), Encoding.ASCII.GetBytes(GetSln())); + if (_isAzureSdk) + { + await File.WriteAllBytesAsync(Path.Combine(_projectDirectory, "Directory.Build.props"), Encoding.ASCII.GetBytes(GetDirectoryBuildProps())); + await File.WriteAllBytesAsync(Path.Combine(_projectDirectory, "README.md"), Encoding.ASCII.GetBytes(GetReadme())); + await File.WriteAllBytesAsync(Path.Combine(_projectDirectory, "CHANGELOG.md"), Encoding.ASCII.GetBytes(GetChangeLog())); + } + } + + private string GetAssemblyInfo() + { + const string publicKey = ", PublicKey = 0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4"; + const string assemblyInfoContent = @"// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo(""{0}.Tests{1}"")] +{2}"; + const string azureResourceProvider = @" +// Replace Microsoft.Test with the correct resource provider namepace for your service and uncomment. +// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-services-resource-providers +// for the list of possible values. +[assembly: Azure.Core.AzureResourceProviderNamespace(""Microsoft.Template"")] +"; + return string.Format(assemblyInfoContent, Configuration.Namespace, Configuration.IsBranded ? publicKey : string.Empty, _isAzureSdk ? azureResourceProvider : string.Empty); + } + + private string GetChangeLog() + { + const string changeLogContent = @"# Release History + +## 1.0.0-beta.1 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes +"; + return changeLogContent; + } + + private string GetReadme() + { + const string multipleApiVersionContent = @" + +### Service API versions + +The client library targets the latest service API version by default. A client instance accepts an optional service API version parameter from its options to specify which API version service to communicate. + +#### Select a service API version + +You have the flexibility to explicitly select a supported service API version when instantiating a client by configuring its associated options. This ensures that the client can communicate with services using the specified API version. + +For example, + +```C# Snippet:CreateClientForSpecificApiVersion +Uri endpoint = new Uri(""""); +DefaultAzureCredential credential = new DefaultAzureCredential(); +ClientOptions options = new ClientOptions(ClientOptions.ServiceVersion.) +var client = new Client(endpoint, credential, options); +``` + +When selecting an API version, it's important to verify that there are no breaking changes compared to the latest API version. If there are significant differences, API calls may fail due to incompatibility. + +Always ensure that the chosen API version is fully supported and operational for your specific use case and that it aligns with the service's versioning policy."; + const string readmeContent = @"# {0} client library for .NET + +{0} is a managed service that helps developers get secret simply and securely. + +Use the client library for to: + +* [Get secret](https://docs.microsoft.com/azure) + +[Source code][source_root] | [Package (NuGet)][package] | [API reference documentation][reference_docs] | [Product documentation][azconfig_docs] | [Samples][source_samples] + + [Source code](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/{1}/{0}/src) | [Package (NuGet)](https://www.nuget.org/packages) | [API reference documentation](https://azure.github.io/azure-sdk-for-net) | [Product documentation](https://docs.microsoft.com/azure) + +## Getting started + +This section should include everything a developer needs to do to install and create their first client connection *very quickly*. + +### Install the package + +First, provide instruction for obtaining and installing the package or library. This section might include only a single line of code, like `dotnet add package package-name`, but should enable a developer to successfully install the package from NuGet, npm, or even cloning a GitHub repository. + +Install the client library for .NET with [NuGet](https://www.nuget.org/ ): + +```dotnetcli +dotnet add package {0} --prerelease +``` + +### Prerequisites + +Include a section after the install command that details any requirements that must be satisfied before a developer can [authenticate](#authenticate-the-client) and test all of the snippets in the [Examples](#examples) section. For example, for Cosmos DB: + +> You must have an [Azure subscription](https://azure.microsoft.com/free/dotnet/) and [Cosmos DB account](https://docs.microsoft.com/azure/cosmos-db/account-overview) (SQL API). In order to take advantage of the C# 8.0 syntax, it is recommended that you compile using the [.NET Core SDK](https://dotnet.microsoft.com/download) 3.0 or higher with a [language version](https://docs.microsoft.com/dotnet/csharp/language-reference/configure-language-version#override-a-default) of `latest`. It is also possible to compile with the .NET Core SDK 2.1.x using a language version of `preview`. + +### Authenticate the client + +If your library requires authentication for use, such as for Azure services, include instructions and example code needed for initializing and authenticating. + +For example, include details on obtaining an account key and endpoint URI, setting environment variables for each, and initializing the client object.{2} + +## Key concepts + +The *Key concepts* section should describe the functionality of the main classes. Point out the most important and useful classes in the package (with links to their reference pages) and explain how those classes work together. Feel free to use bulleted lists, tables, code blocks, or even diagrams for clarity. + +Include the *Thread safety* and *Additional concepts* sections below at the end of your *Key concepts* section. You may remove or add links depending on what your library makes use of: + +### Thread safety + +We guarantee that all client instance methods are thread-safe and independent of each other ([guideline](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-service-methods-thread-safety)). This ensures that the recommendation of reusing client instances is always safe, even across threads. + +### Additional concepts + +[Client options](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#configuring-service-clients-using-clientoptions) | +[Accessing the response](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#accessing-http-response-details-using-responset) | +[Long-running operations](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#consuming-long-running-operations-using-operationt) | +[Handling failures](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#reporting-errors-requestfailedexception) | +[Diagnostics](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/Diagnostics.md) | +[Mocking](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#mocking) | +[Client lifetime](https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/) + + +## Examples + +You can familiarize yourself with different APIs using [Samples](https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/{1}/{0}/samples). + +## Troubleshooting + +Describe common errors and exceptions, how to ""unpack"" them if necessary, and include guidance for graceful handling and recovery. + +Provide information to help developers avoid throttling or other service-enforced errors they might encounter. For example, provide guidance and examples for using retry or connection policies in the API. + +If the package or a related package supports it, include tips for logging or enabling instrumentation to help them debug their code. + +## Next steps + +* Provide a link to additional code examples, ideally to those sitting alongside the README in the package's `/samples` directory. +* If appropriate, point users to other packages that might be useful. +* If you think there's a good chance that developers might stumble across your package in error (because they're searching for specific functionality and mistakenly think the package provides that functionality), point them to the packages they might be looking for. + +## Contributing + +This is a template, but your SDK readme should include details on how to contribute code to the repo/package. + + +[style-guide-msft]: https://docs.microsoft.com/style-guide/capitalization +[style-guide-cloud]: https://aka.ms/azsdk/cloud-style-guide + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-net/sdk/{1}/{0}/README.png) +"; + return string.Format(readmeContent, Configuration.Namespace, _serviceDirectoryName, (Configuration.AzureArm || Configuration.Generation1ConvenienceClient) ? "" : multipleApiVersionContent); + } + + private string GetCiYml() + { + string safeName = Configuration.Namespace.Replace(".", ""); + const string ciYmlContent = @"# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - hotfix/* + - release/* + paths: + include: + - sdk/{0} + - sdk/{0}/ci.yml + - sdk/{0}/{1} + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/{0} + - sdk/{0}/ci.yml + - sdk/{0}/{1} + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: {0} + ArtifactName: packages + Artifacts: + - name: {1} + safeName: {2} +"; + return string.Format(ciYmlContent, _serviceDirectoryName, Configuration.Namespace, safeName); + } + + private string GetDirectoryBuildProps() + { + const string directoryBuildPropsContent = @" + + + +"; + return directoryBuildPropsContent; + } + + private string GetBrandedSrcCSProj() + { + var builder = new CSProjWriter() + { + Description = $"This is the {Configuration.Namespace} client library for developing .NET applications with rich experience.", + AssemblyTitle = $"Azure SDK Code Generation {Configuration.Namespace} for Azure Data Plane", + Version = "1.0.0-beta.1", + PackageTags = Configuration.Namespace, + TargetFrameworks = "$(RequiredTargetFrameworks)", + IncludeOperationsSharedSource = true, + }; + if (_needAzureKeyAuth) + builder.CompileIncludes.Add(new("$(AzureCoreSharedSources)AzureKeyCredentialPolicy.cs", "Shared/Core")); + if (!Configuration.AzureArm) + { + // only branded library will add these shared code compilation lines + builder.CompileIncludes.Add(new("$(AzureCoreSharedSources)AzureResourceProviderNamespaceAttribute.cs", "Shared/Core")); + foreach (var packages in _brandedDependencyPackages) + { + builder.PackageReferences.Add(packages); + } + } + + if (_includeDfe) + { + builder.PackageReferences.Add(new("Azure.Core.Expressions.DataFactory")); + } + + return builder.Write(); + } + + private string GetUnbrandedSrcCSProj() + { + var builder = new CSProjWriter() + { + Description = $"This is the {Configuration.Namespace} client library for developing .NET applications with rich experience.", + AssemblyTitle = $"SDK Code Generation {Configuration.Namespace}", + Version = "1.0.0-beta.1", + PackageTags = Configuration.Namespace, + TargetFramework = "netstandard2.0", + LangVersion = "latest", + GenerateDocumentationFile = true, + }; + foreach (var packages in _unbrandedDependencyPackages) + { + builder.PackageReferences.Add(packages); + } + + return builder.Write(); + } + + private string GetSrcCSProj() => Configuration.IsBranded ? GetBrandedSrcCSProj() : GetUnbrandedSrcCSProj(); + + private static readonly IReadOnlyList _brandedDependencyPackages = new CSProjWriter.CSProjDependencyPackage[] + { + new("Azure.Core"), + new("System.Text.Json") + }; + private static readonly IReadOnlyList _unbrandedDependencyPackages = new CSProjWriter.CSProjDependencyPackage[] + { + new("System.ClientModel", "1.1.0-beta.3"), + new("System.Text.Json", "4.7.2") + }; + + private static readonly IReadOnlyList _brandedTestDependencyPackages = new CSProjWriter.CSProjDependencyPackage[] + { + new("Azure.Identity"), + new("NUnit"), + new("NUnit3TestAdapter"), + new("Microsoft.NET.Test.Sdk"), + new("Moq") + }; + private static readonly IReadOnlyList _unbrandedTestDependencyPackages = new CSProjWriter.CSProjDependencyPackage[] + { + new("NUnit", "3.13.2"), + new("NUnit3TestAdapter", "4.4.2"), + new("Microsoft.NET.Test.Sdk", "17.0.0"), + new("Moq", "[4.18.2]") + }; + + private string GetBrandedTestCSProj() + { + var writer = new CSProjWriter() + { + TargetFrameworks = "$(RequiredTargetFrameworks)", + NoWarn = new("$(NoWarn);CS1591", "We don't care about XML doc comments on test types and members") + }; + + // add the project references + if (_isAzureSdk) + { + writer.ProjectReferences.Add(new("$(AzureCoreTestFramework)")); + } + writer.ProjectReferences.Add(new($"..\\src\\{Configuration.Namespace}.csproj")); + // add the package references + if (!Configuration.AzureArm) + { + foreach (var package in _brandedTestDependencyPackages) + { + writer.PackageReferences.Add(package); + } + } + + return writer.Write(); + } + + private string GetUnbrandedTestCSProj() + { + var writer = new CSProjWriter() + { + TargetFramework = "net8.0", + NoWarn = new("$(NoWarn);CS1591", "Ignore XML doc comments on test types and members") + }; + + // add the project references + writer.ProjectReferences.Add(new($"..\\src\\{Configuration.Namespace}.csproj")); + // add the package references + foreach (var package in _unbrandedTestDependencyPackages) + { + writer.PackageReferences.Add(package); + } + + return writer.Write(); + } + + private string GetTestCSProj() => Configuration.IsBranded ? GetBrandedTestCSProj() : GetUnbrandedTestCSProj(); + + private string GetSln() + { + string slnContent = @"Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 +MinimumVisualStudioVersion = 10.0.40219.1 +"; + if (_isAzureSdk) + { + slnContent += @"Project(""{{9A19103F-16F7-4668-BE54-9A1E7A4F7556}}"") = ""Azure.Core.TestFramework"", ""..\..\core\Azure.Core.TestFramework\src\Azure.Core.TestFramework.csproj"", ""{{ECC730C1-4AEA-420C-916A-66B19B79E4DC}}"" +EndProject +"; + } + slnContent += @"Project(""{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}"") = ""{0}"", ""src\{0}.csproj"", ""{{28FF4005-4467-4E36-92E7-DEA27DEB1519}}"" +EndProject +"; + if (Configuration.GenerateTestProject || Configuration.GenerateSampleProject) + { + slnContent += @"Project(""{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}"") = ""{0}.Tests"", ""tests\{0}.Tests.csproj"", ""{{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}}"" +EndProject +"; + } + slnContent += @"Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {{B0C276D1-2930-4887-B29A-D1A33E7009A2}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{B0C276D1-2930-4887-B29A-D1A33E7009A2}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{B0C276D1-2930-4887-B29A-D1A33E7009A2}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{B0C276D1-2930-4887-B29A-D1A33E7009A2}}.Release|Any CPU.Build.0 = Release|Any CPU + {{8E9A77AC-792A-4432-8320-ACFD46730401}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{8E9A77AC-792A-4432-8320-ACFD46730401}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{8E9A77AC-792A-4432-8320-ACFD46730401}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{8E9A77AC-792A-4432-8320-ACFD46730401}}.Release|Any CPU.Build.0 = Release|Any CPU +"; + if (_isAzureSdk) + { + slnContent += @" {{ECC730C1-4AEA-420C-916A-66B19B79E4DC}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{ECC730C1-4AEA-420C-916A-66B19B79E4DC}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{ECC730C1-4AEA-420C-916A-66B19B79E4DC}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{ECC730C1-4AEA-420C-916A-66B19B79E4DC}}.Release|Any CPU.Build.0 = Release|Any CPU +"; + } + slnContent += @" {{A4241C1F-A53D-474C-9E4E-075054407E74}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{A4241C1F-A53D-474C-9E4E-075054407E74}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{A4241C1F-A53D-474C-9E4E-075054407E74}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{A4241C1F-A53D-474C-9E4E-075054407E74}}.Release|Any CPU.Build.0 = Release|Any CPU + {{FA8BD3F1-8616-47B6-974C-7576CDF4717E}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{FA8BD3F1-8616-47B6-974C-7576CDF4717E}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{FA8BD3F1-8616-47B6-974C-7576CDF4717E}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{FA8BD3F1-8616-47B6-974C-7576CDF4717E}}.Release|Any CPU.Build.0 = Release|Any CPU + {{85677AD3-C214-42FA-AE6E-49B956CAC8DC}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{85677AD3-C214-42FA-AE6E-49B956CAC8DC}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{85677AD3-C214-42FA-AE6E-49B956CAC8DC}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{85677AD3-C214-42FA-AE6E-49B956CAC8DC}}.Release|Any CPU.Build.0 = Release|Any CPU + {{28FF4005-4467-4E36-92E7-DEA27DEB1519}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{28FF4005-4467-4E36-92E7-DEA27DEB1519}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{28FF4005-4467-4E36-92E7-DEA27DEB1519}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{28FF4005-4467-4E36-92E7-DEA27DEB1519}}.Release|Any CPU.Build.0 = Release|Any CPU + {{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}}.Debug|Any CPU.Build.0 = Debug|Any CPU + {{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}}.Release|Any CPU.ActiveCfg = Release|Any CPU + {{1F1CD1D4-9932-4B73-99D8-C252A67D4B46}}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {{A97F4B90-2591-4689-B1F8-5F21FE6D6CAE}} + EndGlobalSection +EndGlobal +"; + return string.Format(slnContent, Configuration.Namespace); + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/PluginProcessor.cs b/logger/autorest.csharp/common/AutoRest/Plugins/PluginProcessor.cs new file mode 100644 index 0000000..1d0b44d --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/PluginProcessor.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal static class PluginProcessor + { + //https://stackoverflow.com/a/26750/294804 + private static readonly Type[] PluginTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.Namespace == typeof(IPlugin).Namespace && t.IsClass && !t.IsAbstract && typeof(IPlugin).IsAssignableFrom(t) && t.GetCustomAttribute(true) != null) + .ToArray(); + public static readonly Dictionary> Plugins = PluginTypes + .ToDictionary(pt => pt.GetCustomAttribute(true)!.PluginName, pt => (Func)(() => (IPlugin)Activator.CreateInstance(pt)!)); + public static readonly string[] PluginNames = Plugins.Keys.ToArray(); + + public static async Task Start(IPluginCommunication autoRest) + { + try + { + IPlugin plugin = Plugins[autoRest.PluginName](); + var shouldAttach = await autoRest.GetValue(string.Format(Configuration.Options.AttachDebuggerFormat, autoRest.PluginName)); + if (shouldAttach.ToBoolean() ?? false) + { + Console.Error.WriteLine("Attempting to attach debugger."); + System.Diagnostics.Debugger.Launch(); + } + AutoRestLogger.Initialize(autoRest); + return await plugin.Execute(autoRest); + } + catch (Exception e) + { + await autoRest.Fatal(e.ToString()); + return false; + } + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/SA1505Rewriter.cs b/logger/autorest.csharp/common/AutoRest/Plugins/SA1505Rewriter.cs new file mode 100644 index 0000000..380ce20 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/SA1505Rewriter.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AutoRest.CSharp.Common.AutoRest.Plugins +{ + /// + /// This Roslyn CSharpSyntaxRewriter will delete extra newlines after the + /// open brace of a class/struct/enum definition. It is for resolving SA1505 error. + /// + internal class SA1505Rewriter : CSharpSyntaxRewriter + { + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + => base.VisitClassDeclaration((ClassDeclarationSyntax)RemoveEOLAfterOpenBrace(node)); + + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) + => base.VisitStructDeclaration((StructDeclarationSyntax)RemoveEOLAfterOpenBrace(node)); + + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) + => base.VisitEnumDeclaration((EnumDeclarationSyntax)RemoveEOLAfterOpenBrace(node)); + + private BaseTypeDeclarationSyntax RemoveEOLAfterOpenBrace(BaseTypeDeclarationSyntax node) + { + // all extra EOL after open brace are the leading trivia of the next token + var nextToken = node.OpenBraceToken.GetNextToken(); + if (nextToken.IsMissing) + { + return node; + } + + var leadingTrivia = nextToken.LeadingTrivia; + if (leadingTrivia.Count == 0 || !leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + return node; + } + + var newLeadingTrvia = leadingTrivia.SkipWhile(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); + var newNextToken = nextToken.WithLeadingTrivia(newLeadingTrvia); + return node.ReplaceToken(nextToken, newNextToken); + } + } +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/XmlDocumentFile.cs b/logger/autorest.csharp/common/AutoRest/Plugins/XmlDocumentFile.cs new file mode 100644 index 0000000..65fbd33 --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/XmlDocumentFile.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal record XmlDocumentFile(string TestFileName, XmlDocWriter XmlDocWriter); +} diff --git a/logger/autorest.csharp/common/AutoRest/Plugins/XmlFormatter.cs b/logger/autorest.csharp/common/AutoRest/Plugins/XmlFormatter.cs new file mode 100644 index 0000000..5bf8ffd --- /dev/null +++ b/logger/autorest.csharp/common/AutoRest/Plugins/XmlFormatter.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using AutoRest.CSharp.Generation.Writers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal class XmlFormatter + { + internal static async Task FormatAsync(XmlDocWriter writer, SyntaxTree syntaxTree) + { + var document = writer.Document; + var methods = await GetMethodsAsync(syntaxTree); + // first we need to get the members + var members = writer.Members; + + foreach (var member in members) + { + // get the example element + var exampleElement = member.Element("example"); + if (exampleElement == null) + continue; + + foreach (var codeElement in exampleElement.Elements("code")) + { + var testMethodName = codeElement.Value; + // find the magic comment and replace it with real code + if (methods.TryGetValue(testMethodName, out var methodDeclaration)) + { + var lines = GetLines(methodDeclaration.Body!); + var content = FormatContent(lines); + // this will give you + // <[[CDATA + // var our = code; + // ]]> + codeElement.ReplaceAll(new XCData(content)); + } + } + } + + var swriter = new XmlStringWriter(); + XmlWriterSettings settings = new XmlWriterSettings { OmitXmlDeclaration = false, Indent = true }; + using (XmlWriter xw = XmlWriter.Create(swriter, settings)) + { + document.Save(xw); + } + + return swriter.ToString(); + } + + private static string[] GetLines(BlockSyntax methodBlock) + { + if (!methodBlock.Statements.Any()) + return Array.Empty(); + + // here we have to get the string of all statements and then split by new lines + // this is because in the StatementSyntax, the NewLines (\n or \r\n) could appear at the end of the statement or at the beginning of the statement + // therefore to keep it simple, we just combine all the text together and then split by the new lines to trim the extra spaces in front of every line + var builder = new StringBuilder(); + foreach (var statement in methodBlock.Statements) + { + builder.Append(statement.ToFullString()); + } + + return builder.ToString().Split(Environment.NewLine); + } + + /// + /// This method trims the leading spaces of the lines, and it also adds proper amount of spaces to the content of spaces because of the bug of Roslyn: https://github.com/dotnet/roslyn/issues/8269 + /// + /// + /// + internal static string FormatContent(string[] lines) + { + if (!lines.Any()) + return string.Empty; + + var builder = new StringBuilder(); + + // find the first non-empty line + int lineNumber = -1; + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + if (!string.IsNullOrWhiteSpace(line)) + { + lineNumber = i; + break; + } + if (i > 0) + builder.AppendLine(); // we do not need a new line when it is the first line + builder.Append(line); + } + + if (lineNumber < 0) + return string.Empty; // every line is whitespaces + + // the following code is a temporarily workaround the Roslyn's format issue around collection initializers: https://github.com/dotnet/roslyn/issues/8269 + // if Roslyn could properly format the collection initializers and everything, this code should be as simple as: take a amount of spaces on the first line, trim spaces with the same amount on every line + // since the code we are processing here has been formatted by Roslyn, we only take the cases that lines starts or ends with { or } to format. + var stack = new Stack(); + stack.Push(0); + const int spaceIncrement = 4; + + for (int i = lineNumber; i < lines.Length; i++) + { + var line = lines[i].AsSpan(); + // first we count how many leading spaces we are having on this line + int count = 0; + while (count < line.Length && char.IsWhiteSpace(line[count])) + { + count++; + } + var spaceCount = count; + // if the rest part of the line leads by a }, we should decrease the amount of leading spaces + if (count < line.Length && line[count] == '}') + { + stack.Pop(); + } + // find out how many spaces we would like to prepend + var leadingSpaces = stack.Peek(); + // if the rest part of the line leads by a {, we increment the leading space + if (count < line.Length && line[count] == '{') + { + stack.Push(stack.Peek() + spaceIncrement); + } + builder.AppendLine(); + while (leadingSpaces > 0) + { + builder.Append(' '); + leadingSpaces--; + } + builder.Append(line.Slice(spaceCount)); + } + + return builder.ToString(); + } + + private static async Task> GetMethodsAsync(SyntaxTree syntaxTree) + { + var result = new Dictionary(); + var root = await syntaxTree.GetRootAsync(); + + foreach (var method in root.DescendantNodes().OfType()) + { + result.Add(method.Identifier.Text, method); + } + + return result; + } + + private class XmlStringWriter : StringWriter + { + public override Encoding Encoding => Encoding.UTF8; + } + } +} diff --git a/logger/autorest.csharp/common/Decorator/ConstantSchemaTransformer.cs b/logger/autorest.csharp/common/Decorator/ConstantSchemaTransformer.cs new file mode 100644 index 0000000..59c68a4 --- /dev/null +++ b/logger/autorest.csharp/common/Decorator/ConstantSchemaTransformer.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Common.Decorator +{ + internal static class ConstantSchemaTransformer + { + public static void Transform(CodeModel codeModel) + { + var constantSchemas = new HashSet(codeModel.Schemas.Constants); + if (!constantSchemas.Any()) + return; + + Dictionary convertedChoiceSchemas = new(); + + foreach (var operation in codeModel.OperationGroups.SelectMany(og => og.Operations)) + { + // change the schema on operations (only for optional) + foreach (var parameter in operation.Parameters) + { + if (parameter.IsRequired || parameter.Schema is not ConstantSchema constantSchema || ShouldSkipReplace(constantSchema)) + continue; + + var choiceSchema = ComputeIfAbsent(convertedChoiceSchemas, constantSchema, ConvertToChoiceSchema); + constantSchema.ValueType = choiceSchema; + operation.SignatureParameters.Add(parameter); + } + + foreach (var request in operation.Requests) + { + foreach (var parameter in request.Parameters) + { + if (parameter.IsRequired || parameter.Schema is not ConstantSchema constantSchema || ShouldSkipReplace(constantSchema)) + continue; + + var choiceSchema = ComputeIfAbsent(convertedChoiceSchemas, constantSchema, ConvertToChoiceSchema); + constantSchema.ValueType = choiceSchema; + request.SignatureParameters.Add(parameter); + } + } + + // change the schema on models (optional and required) + foreach (var obj in codeModel.Schemas.Objects) + { + foreach (var property in obj.Properties) + { + if (property.Schema is not ConstantSchema constantSchema || ShouldSkipReplace(constantSchema) || CheckPropertyExtension(property)) + continue; + + var choiceSchema = ComputeIfAbsent(convertedChoiceSchemas, constantSchema, ConvertToChoiceSchema); + constantSchema.ValueType = choiceSchema; + } + } + } + + foreach (var choiceSchema in convertedChoiceSchemas.Values) + codeModel.Schemas.Choices.Add(choiceSchema); + } + + // we skip this process when the underlying type of the constant is boolean + private static bool ShouldSkipReplace(ConstantSchema constantSchema) + => constantSchema.ValueType is BooleanSchema; + + private static bool CheckPropertyExtension(Property property) + { + if (property.Extensions?.TryGetValue("x-ms-constant", out var value) ?? false) + { + return "true".Equals(value.ToString()); + } + return false; + } + + private static V ComputeIfAbsent(Dictionary dict, K key, Func generator) where K : notnull + { + if (dict.TryGetValue(key, out var value)) + { + return value; + } + var generated = generator(key); + dict.Add(key, generated); + return generated; + } + + private static ChoiceSchema ConvertToChoiceSchema(ConstantSchema constantSchema) + { + var choiceValue = constantSchema.Value.Value.ToString(); + ChoiceValue choice = new() + { + Value = choiceValue, + Language = constantSchema.Value.Language != null ? + constantSchema.Value.Language : + new Languages + { + Default = new Language + { + Name = choiceValue, + } + } + }; + + ChoiceSchema choiceSchema = new() + { + Type = AllSchemaTypes.Choice, + ChoiceType = (PrimitiveSchema)constantSchema.ValueType, + DefaultValue = constantSchema.DefaultValue, + Language = constantSchema.Language, + Choices = new[] { choice } + }; + return choiceSchema; + } + } +} diff --git a/logger/autorest.csharp/common/Decorator/ModelPropertyClientDefaultValueTransformer.cs b/logger/autorest.csharp/common/Decorator/ModelPropertyClientDefaultValueTransformer.cs new file mode 100644 index 0000000..74818ba --- /dev/null +++ b/logger/autorest.csharp/common/Decorator/ModelPropertyClientDefaultValueTransformer.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Common.Decorator +{ + /// + /// This class is used to eliminate the value of ClientDefaultValue which is defined in the "x-ms-client-default" property attribute. + /// We will not generate any client default value into the SDK for model property, so we remove it from the code model. + /// + internal class ModelPropertyClientDefaultValueTransformer + { + public static void Transform(CodeModel codeModel) + { + foreach (var schema in codeModel.AllSchemas.OfType()) + { + foreach (var property in schema.Properties) + { + /* eliminate the client default value of model property */ + property.ClientDefaultValue = null; + } + } + } + + } +} diff --git a/logger/autorest.csharp/common/Decorator/SchemaUsageTransformer.cs b/logger/autorest.csharp/common/Decorator/SchemaUsageTransformer.cs new file mode 100644 index 0000000..f55dc79 --- /dev/null +++ b/logger/autorest.csharp/common/Decorator/SchemaUsageTransformer.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Common.Decorator +{ + /// + /// This class is used to transform usages for definitions listed in the "x-ms-format-element-type" property attribute. + /// It must be run before the transform (or any transforms that depend on the usages) + /// so that the default derived schema logic is working with the correct usages. + /// + internal static class SchemaUsageTransformer + { + private const string FormatElementDefinition = "x-ms-format-element-type"; + private const string CSharpUsage = "x-csharp-usage"; + + public static void Transform(CodeModel codeModel) + { + Dictionary> schemaToPropertyMap = new(); + Dictionary> schemaToEnclosingSchemasMap = new(); + + foreach (var schema in codeModel.AllSchemas.OfType()) + { + foreach (var property in schema.Properties) + { + if (property.Extensions?.TryGetValue(FormatElementDefinition, out var value) == true) + { + string valueString = (string)value; + + if (valueString.StartsWith("#/definitions/")) + { + valueString = valueString.Substring("#/definitions/".Length); + } + + if (!schemaToPropertyMap.ContainsKey(valueString)) + { + schemaToPropertyMap.Add(valueString, new List()); + } + + if (!schemaToEnclosingSchemasMap.ContainsKey(valueString)) + { + schemaToEnclosingSchemasMap.Add(valueString, new List()); + } + + schemaToPropertyMap[valueString].Add(property); + schemaToEnclosingSchemasMap[valueString].Add(schema); + } + } + } + + if (schemaToPropertyMap.Count == 0) + return; + + foreach (var schema in codeModel.AllSchemas.OfType()) + { + if (!schemaToPropertyMap.TryGetValue(schema.Name, out var propertyList)) continue; + + schemaToPropertyMap.Remove(schema.Name); + foreach (var property in propertyList) + { + property.Extensions![FormatElementDefinition] = schema; + } + + schema.Extensions ??= new RecordOfStringAndAny(); + + // apply usages and media types based on the enclosing schemas for the properties that reference + // the "x-ms-format-element-type" schema + + HashSet additionalUsages = new(); + HashSet additionalMediaTypes = new(); + foreach (var enclosingSchema in schemaToEnclosingSchemasMap[schema.Name]) + { + if (enclosingSchema is ObjectSchema objectSchema) + { + foreach (SchemaContext schemaUsage in objectSchema.Usage) + { + if (schemaUsage == SchemaContext.Exception) continue; + additionalUsages.Add(schemaUsage == SchemaContext.Input ? "input" : "output"); + } + + foreach (KnownMediaType mediaType in objectSchema.SerializationFormats) + { + additionalMediaTypes.Add(mediaType); + } + } + } + + if (additionalUsages.Count > 0) + { + // This is a hack to avoid needing to update the SchemaUsageProvider logic to look up the property schema using "x-ms-format-element-type" + // The problem with doing this here is that we don't know for sure if this should be a public model, but if we don't mark is as a model + // here then it will be generated as internal, since it will not necessarily be included in the SchemaUsageProvider logic that + // loops through model properties. + additionalUsages.Add("model"); + } + + // apply converter usage to any schemas that are referenced with "x-ms-format-element-type" in a property + additionalUsages.Add("converter"); + + // recursively apply the usages and media types to the schema and all property schemas on the schema + Apply(schema, string.Join(",", additionalUsages), additionalMediaTypes, new HashSet()); + } + + if (schemaToPropertyMap.Count > 0) + { + var schemaList = schemaToPropertyMap.Keys.Aggregate((a, b) => $"{a}, {b}"); + throw new InvalidOperationException($"The following schemas were referenced by properties with the '{FormatElementDefinition}' attribute, but were not found in any definitions: " + schemaList); + } + } + + private static void Apply(ObjectSchema schema, string usages, HashSet mediaTypes, HashSet appliedSchemas) + { + if (appliedSchemas.Contains(schema)) + return; + + appliedSchemas.Add(schema); + + + schema.Extensions ??= new RecordOfStringAndAny(); + if (!schema.Extensions!.TryGetValue(CSharpUsage, out var existingUsages)) + { + schema.Extensions.Add(CSharpUsage, usages); + } + else + { + if (existingUsages is string usage && !string.IsNullOrEmpty(usage)) + { + schema.Extensions![CSharpUsage] = usage + "," + usages; + } + else + { + schema.Extensions![CSharpUsage] = usages; + } + } + + foreach (var mediaType in mediaTypes) + { + schema.SerializationFormats.Add(mediaType); + } + + foreach (var property in schema.Properties) + { + if (property.Schema is ObjectSchema propertySchema) + { + Apply(propertySchema, usages, mediaTypes, appliedSchemas); + } + } + + if (schema.Children != null) + { + foreach (var child in schema.Children!.Immediate) + { + if (child is ObjectSchema propertySchema) + { + Apply(propertySchema, usages, mediaTypes, appliedSchemas); + } + } + } + + if (schema.Parents != null) + { + foreach (var parent in schema.Parents!.Immediate) + { + if (parent is ObjectSchema propertySchema) + { + Apply(propertySchema, usages, mediaTypes, appliedSchemas); + } + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Types/CSharpType.cs b/logger/autorest.csharp/common/Generation/Types/CSharpType.cs new file mode 100644 index 0000000..4ccf7d8 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Types/CSharpType.cs @@ -0,0 +1,681 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure; + +namespace AutoRest.CSharp.Generation.Types +{ + /// + /// CSharpType represents the C# type of an input type. + /// It is constructed from a and its properties. + /// + public class CSharpType + { + private readonly TypeProvider? _implementation; + private readonly Type? _type; + private string _name; + private string _namespace; + private CSharpType? _declaringType; + private bool _isValueType; + private bool _isEnum; + private bool _isNullable; + private bool _isPublic; + private bool? _isUnion; + private IReadOnlyList _arguments; + private IReadOnlyList _unionItemTypes; + + private bool? _isReadOnlyMemory; + private bool? _isList; + private bool? _isArray; + private bool? _isReadOnlyList; + private bool? _isReadWriteList; + private bool? _isDictionary; + private bool? _isReadOnlyDictionary; + private bool? _isReadWriteDictionary; + private bool? _isCollection; + private bool? _isIEnumerableOfT; + private bool? _isIAsyncEnumerableOfT; + private bool? _isResponse; + private bool? _isResponseOfT; + private bool? _isPageable; + private bool? _isAsyncPageable; + private bool? _isOperation; + private bool? _isOperationOfT; + private bool? _isOperationOfAsyncPageable; + private bool? _isOperationOfPageable; + private int? _hashCode; + private CSharpType? _initializationType; + private CSharpType? _propertyInitializationType; + private CSharpType? _elementType; + private CSharpType? _inputType; + private CSharpType? _outputType; + internal bool IsReadOnlyMemory => _isReadOnlyMemory ??= TypeIsReadOnlyMemory(); + internal bool IsList => _isList ??= TypeIsList(); + internal bool IsArray => _isArray ??= TypeIsArray(); + internal bool IsReadOnlyList => _isReadOnlyList ??= TypeIsReadOnlyList(); + internal bool IsReadWriteList => _isReadWriteList ??= TypeIsReadWriteList(); + internal bool IsDictionary => _isDictionary ??= TypeIsDictionary(); + internal bool IsReadOnlyDictionary => _isReadOnlyDictionary ??= TypeIsReadOnlyDictionary(); + internal bool IsReadWriteDictionary => _isReadWriteDictionary ??= TypeIsReadWriteDictionary(); + internal bool IsIEnumerableOfT => _isIEnumerableOfT ??= TypeIsIEnumerableOfT(); + internal bool IsIAsyncEnumerableOfT => _isIAsyncEnumerableOfT ??= TypeIsIAsyncEnumerableOfT(); + + #region AzureTypes + // TODO -- these are azure-specific. We might need to generalize these using ApiTypes + internal bool IsResponse => _isResponse ??= TypeIsResponse(); + internal bool IsResponseOfT => _isResponseOfT ??= TypeIsResponseOfT(); + internal bool IsPageable => _isPageable ??= TypeIsPageable(); + internal bool IsAsyncPageable => _isAsyncPageable ??= TypeIsAsyncPageable(); + internal bool IsOperation => _isOperation ??= TypeIsOperation(); + internal bool IsOperationOfT => _isOperationOfT ??= TypeIsOperationOfT(); + internal bool IsOperationOfAsyncPageable => _isOperationOfAsyncPageable ??= TypeIsOperationOfAsyncPageable(); + internal bool IsOperationOfPageable => _isOperationOfPageable ??= TypeIsOperationOfPageable(); + #endregion + + /// + /// Constructs a from a . + /// + /// The base system type. + /// Optional flag to determine if the constructed type should be nullable. Defaults to false. + public CSharpType(Type type, bool isNullable = false) : this( + type, + isNullable, + type.IsGenericType ? type.GetGenericArguments().Select(p => new CSharpType(p)).ToArray() : Array.Empty()) + { } + + /// + /// Constructs a non-nullable from a with arguments + /// + /// The base system type. + /// The type's arguments. + public CSharpType(Type type, params CSharpType[] arguments) : this(type, arguments, false) + { } + + /// + /// Constructs a from a with arguments. + /// + /// The base system type. + /// Optional flag to determine if the constructed type should be nullable. Defaults to false. + /// The type's arguments. + public CSharpType(Type type, bool isNullable, params CSharpType[] arguments) : this(type, arguments, isNullable) + { } + + /// + /// Constructs a from a . + /// + /// The base system type. + /// The type's arguments. + /// Optional flag to determine if the constructed type should be nullable. Defaults to false. + public CSharpType(Type type, IReadOnlyList arguments, bool isNullable = false) + { + Debug.Assert(type.Namespace != null, "type.Namespace != null"); + + // handle nullable value types explicitly because they are implemented using generic type `System.Nullable` + var underlyingValueType = Nullable.GetUnderlyingType(type); + if (underlyingValueType != null) + { + // in this block, we are converting input like `typeof(int?)` into the way as if they input: `typeof(int), isNullable: true` + type = underlyingValueType; + arguments = type.IsGenericType ? type.GetGenericArguments().Select(p => new CSharpType(p)).ToArray() : Array.Empty(); + isNullable = true; + } + + _type = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + ValidateArguments(_type, arguments); + + var name = type.IsGenericType ? type.Name.Substring(0, type.Name.IndexOf('`')) : type.Name; + var isValueType = type.IsValueType; + var isEnum = type.IsEnum; + var ns = type.Namespace; + var isPublic = type.IsPublic && arguments.All(t => t.IsPublic); + // open generic parameter such as the `T` in `List` is considered as declared inside the `List` type as well, but we just want this to be the pure nested type, therefore here we exclude the open generic parameter scenario + // for a closed generic parameter such as the `string` in `List`, it is just an ordinary type without a `DeclaringType`. + var declaringType = type.DeclaringType is not null && !type.IsGenericParameter ? new CSharpType(type.DeclaringType) : null; + + Initialize(name, isValueType, isEnum, isNullable, ns, declaringType, arguments, isPublic); + } + + [Conditional("DEBUG")] + private static void ValidateArguments(Type type, IReadOnlyList arguments) + { + if (type.IsGenericTypeDefinition) + { + Debug.Assert(arguments.Count == type.GetGenericArguments().Length, $"the count of arguments given ({string.Join(", ", arguments.Select(a => a.ToString()))}) does not match the arguments in the definition {type}"); + } + else + { + Debug.Assert(arguments.Count == 0, "arguments can be added only to the generic type definition."); + } + } + + internal CSharpType(TypeProvider implementation, IReadOnlyList? arguments = null, bool isNullable = false) + { + _implementation = implementation; + _arguments = arguments ?? Array.Empty(); + + var isPublic = implementation is ExpressionTypeProvider expressionTypeProvider + ? expressionTypeProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) + : implementation.Declaration.Accessibility == "public"; + + isPublic = isPublic && Arguments.All(t => t.IsPublic); + + var ns = implementation.Declaration.Namespace; + var name = implementation.Declaration.Name; + var declaringType = implementation.DeclaringTypeProvider?.Type; + var isValueType = implementation.IsValueType; + var isEnum = implementation.IsEnum; + + Initialize(name, isValueType, isEnum, isNullable, ns, declaringType, arguments, isPublic); + + SerializeAs = _implementation?.SerializeAs; + } + + [MemberNotNull(nameof(_name))] + [MemberNotNull(nameof(_isValueType))] + [MemberNotNull(nameof(_isEnum))] + [MemberNotNull(nameof(_isNullable))] + [MemberNotNull(nameof(_namespace))] + [MemberNotNull(nameof(_arguments))] + [MemberNotNull(nameof(_isPublic))] + [MemberNotNull(nameof(_unionItemTypes))] + private void Initialize(string? name, bool isValueType, bool isEnum, bool isNullable, string? ns, + CSharpType? declaringType, IReadOnlyList? args, bool isPublic) + { + _name = name ?? string.Empty; + _isValueType = isValueType; + _isEnum = isEnum; + _isNullable = isNullable; + _namespace = ns ?? string.Empty; + _declaringType = declaringType; + _arguments = args ?? Array.Empty(); + _isPublic = isPublic; + _unionItemTypes ??= Array.Empty(); + } + + public virtual string Namespace { get { return _namespace; } } + public virtual string Name { get { return _name; } } + public CSharpType? DeclaringType { get { return _declaringType; } } + public bool IsValueType { get { return _isValueType; } } + public bool IsEnum { get { return _isEnum; } } + public bool IsLiteral => Literal is not null; + public bool IsUnion => _isUnion ??= UnionItemTypes.Count > 0; + public bool IsPublic { get { return _isPublic; } } + public bool IsFrameworkType => _type != null; + public bool IsNullable { get { return _isNullable; } } + public bool IsGenericType => Arguments.Count > 0; + public bool IsCollection => _isCollection ??= TypeIsCollection(); + public Type FrameworkType => _type ?? throw new InvalidOperationException("Not a framework type"); + internal Constant? Literal { get; private init; } + internal TypeProvider Implementation => _implementation ?? throw new InvalidOperationException($"Not implemented type: '{Namespace}.{Name}'"); + public IReadOnlyList Arguments { get { return _arguments; } } + public CSharpType InitializationType => _initializationType ??= GetImplementationType(); + public CSharpType PropertyInitializationType => _propertyInitializationType ??= GetPropertyImplementationType(); + public CSharpType ElementType => _elementType ??= GetElementType(); + public CSharpType InputType => _inputType ??= GetInputType(); + public CSharpType OutputType => _outputType ??= GetOutputType(); + public Type? SerializeAs { get; init; } + public IReadOnlyList UnionItemTypes { get { return _unionItemTypes; } private init { _unionItemTypes = value; } } + + private bool TypeIsReadOnlyMemory() + => IsFrameworkType && _type == typeof(ReadOnlyMemory<>); + + private bool TypeIsReadOnlyList() + => IsFrameworkType && (_type == typeof(IEnumerable<>) || _type == typeof(IReadOnlyList<>)); + + private bool TypeIsReadWriteList() + => IsFrameworkType && (_type == typeof(IList<>) || _type == typeof(ICollection<>) || _type == typeof(List<>)); + + private bool TypeIsList() + => IsReadOnlyList || IsReadWriteList || IsReadOnlyMemory; + + private bool TypeIsArray() + => IsFrameworkType && FrameworkType.IsArray; + + private bool TypeIsReadOnlyDictionary() + => IsFrameworkType && _type == typeof(IReadOnlyDictionary<,>); + + private bool TypeIsReadWriteDictionary() + => IsFrameworkType && (_type == typeof(IDictionary<,>)); + + private bool TypeIsDictionary() + => IsReadOnlyDictionary || IsReadWriteDictionary; + + private bool TypeIsCollection() + => IsFrameworkType && (IsDictionary || IsList); + + /// + /// Retrieves the implementation type for the . + /// + /// The implementation type . + private CSharpType GetImplementationType() + { + if (IsFrameworkType) + { + if (IsReadOnlyMemory) + { + return new CSharpType(Arguments[0].FrameworkType.MakeArrayType()); + } + + if (IsList) + { + return new CSharpType(typeof(List<>), Arguments); + } + + if (IsDictionary) + { + return new CSharpType(typeof(Dictionary<,>), Arguments); + } + } + + return this; + } + + /// + /// Retrieves the implementation type for the . + /// + /// The implementation type . + private CSharpType GetPropertyImplementationType() + { + if (IsFrameworkType) + { + if (IsReadOnlyMemory) + { + return new CSharpType(typeof(ReadOnlyMemory<>), Arguments); + } + + if (IsList) + { + return new CSharpType(ChangeTrackingListProvider.Instance, Arguments); + } + + if (IsDictionary) + { + return new CSharpType(ChangeTrackingDictionaryProvider.Instance, Arguments); + } + } + + return this; + } + + /// + /// Retrieves the element type for the . If the type is not an array, list, or dictionary, an exception is thrown. + /// + /// The element type for the . + /// Thrown when the type is not a framework type, array, list, or dictionary. + private CSharpType GetElementType() + { + if (IsFrameworkType) + { + if (FrameworkType.IsArray) + { + return new CSharpType(FrameworkType.GetElementType()!); + } + + if (IsReadOnlyMemory || IsList) + { + return Arguments[0]; + } + + if (IsDictionary) + { + return Arguments[1]; + } + } + + throw new NotSupportedException(Name); + } + + /// + /// Retrieves the input type for the . + /// + /// The input type. + private CSharpType GetInputType() + { + if (IsFrameworkType) + { + if (IsReadOnlyMemory) + { + return new CSharpType(typeof(ReadOnlyMemory<>), isNullable: IsNullable, arguments: Arguments); + } + + if (IsList) + { + return new CSharpType( + typeof(IEnumerable<>), + isNullable: IsNullable, + arguments: Arguments); + } + } + + return this; + } + + /// + /// Retrieves the output type for the . + /// + /// The output type. + private CSharpType GetOutputType() + { + if (IsFrameworkType) + { + if (IsReadOnlyMemory) + { + return new CSharpType(typeof(ReadOnlyMemory<>), isNullable: IsNullable, arguments: Arguments); + } + + if (IsList) + { + return new CSharpType( + typeof(IReadOnlyList<>), + isNullable: IsNullable, + arguments: Arguments); + } + + if (IsDictionary) + { + return new CSharpType( + typeof(IReadOnlyDictionary<,>), + isNullable: IsNullable, + arguments: Arguments); + } + } + + return this; + } + + private bool TypeIsIEnumerableOfT() => IsFrameworkType && FrameworkType == typeof(IEnumerable<>); + + private bool TypeIsIAsyncEnumerableOfT() => IsFrameworkType && FrameworkType == typeof(IAsyncEnumerable<>); + + private bool TypeIsResponse() => IsFrameworkType && FrameworkType == typeof(Response); + + private bool TypeIsResponseOfT() => IsFrameworkType && FrameworkType == typeof(Response<>); + + private bool TypeIsPageable() => IsFrameworkType && FrameworkType == typeof(Pageable<>); + + private bool TypeIsAsyncPageable() => IsFrameworkType && FrameworkType == typeof(AsyncPageable<>); + + private bool TypeIsOperation() => IsFrameworkType && FrameworkType == typeof(Operation); + + private bool TypeIsOperationOfT() => IsFrameworkType && FrameworkType == typeof(Operation<>); + + private bool TypeIsOperationOfAsyncPageable() => IsOperationOfT && Arguments.Count == 1 && Arguments[0].IsAsyncPageable; + + private bool TypeIsOperationOfPageable() => IsOperationOfT && Arguments.Count == 1 && Arguments[0].IsPageable; + + /// + /// Validates if the current type is equal to . + /// + /// The type to compare. + /// Flag used to control if nullability should be ignored during comparison. + /// true if the types are equal, false otherwise. + protected bool Equals(CSharpType other, bool ignoreNullable = false) + => Equals(_implementation, other._implementation) && + _type == other._type && + Arguments.SequenceEqual(other.Arguments) && + (ignoreNullable || IsNullable == other.IsNullable); + + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + return obj is CSharpType csType && Equals(csType, ignoreNullable: false); + } + + public bool EqualsIgnoreNullable(CSharpType other) => Equals(other, ignoreNullable: true); + + public bool Equals(Type type) => + IsFrameworkType && (type.IsGenericType ? type.GetGenericTypeDefinition() == FrameworkType && AreArgumentsEqual(type.GetGenericArguments()) : type == FrameworkType); + + /// + /// Types that the provided generic arguments match the type's arguments. + /// + /// The arguments to compare. + /// true if the arguments are equal to the type's arguments. Otherwise, false. + private bool AreArgumentsEqual(IReadOnlyList genericArguments) + { + if (Arguments.Count != genericArguments.Count) + { + return false; + } + + for (int i = 0; i < Arguments.Count; i++) + { + if (!Arguments[i].Equals(genericArguments[i])) + { + return false; + } + } + + return true; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed override int GetHashCode() + { + // we cache the hashcode since `CSharpType` is meant to be immuttable. + if (_hashCode != null) + return _hashCode.Value; + + var hashCode = new HashCode(); + foreach (var arg in Arguments) + { + hashCode.Add(arg); + } + _hashCode = HashCode.Combine(_implementation, _type, hashCode.ToHashCode(), IsNullable); + + return _hashCode.Value; + } + + public CSharpType GetGenericTypeDefinition() + => _type is null + ? throw new NotSupportedException($"{nameof(TypeProvider)} doesn't support generics.") + : new(_type, IsNullable); + + /// + /// Constructs a new with the given nullability . + /// + /// Flag to determine if the new type is nullable. + /// The existing if it is nullable, otherwise a new instance of . + public virtual CSharpType WithNullable(bool isNullable) => + isNullable == IsNullable ? this : IsFrameworkType + ? new CSharpType(FrameworkType, Arguments, isNullable) + { + Literal = Literal, + UnionItemTypes = UnionItemTypes + } + : new CSharpType(Implementation, arguments: Arguments, isNullable: isNullable) + { + Literal = Literal, + UnionItemTypes = UnionItemTypes + }; + + public static implicit operator CSharpType(Type type) => new CSharpType(type); + + public sealed override string ToString() + { + return new CodeWriter().Append($"{this}").ToString(false); + } + + internal bool TryGetCSharpFriendlyName([MaybeNullWhen(false)] out string name) + { + name = _type switch + { + null => null, + var t when t.IsGenericParameter => t.Name, + //if we have an array type and the element is defined in the same assembly then its a generic param array. + var t when t.IsArray && t.Assembly.Equals(GetType().Assembly) => t.Name, + var t when t == typeof(bool) => "bool", + var t when t == typeof(byte) => "byte", + var t when t == typeof(sbyte) => "sbyte", + var t when t == typeof(short) => "short", + var t when t == typeof(ushort) => "ushort", + var t when t == typeof(int) => "int", + var t when t == typeof(uint) => "uint", + var t when t == typeof(long) => "long", + var t when t == typeof(ulong) => "ulong", + var t when t == typeof(char) => "char", + var t when t == typeof(double) => "double", + var t when t == typeof(float) => "float", + var t when t == typeof(object) => "object", + var t when t == typeof(decimal) => "decimal", + var t when t == typeof(string) => "string", + _ => null + }; + + return name != null; + } + + /// + /// Method checks if object of "from" type can be converted to "to" type by calling `ToList` extension method. + /// It returns true if "from" is and "to" is or . + /// + internal static bool RequiresToList(CSharpType from, CSharpType to) + { + if (!to.IsFrameworkType || !from.IsFrameworkType || from.FrameworkType != typeof(IEnumerable<>)) + { + return false; + } + + return to.FrameworkType == typeof(IReadOnlyList<>) || to.FrameworkType == typeof(IList<>); + } + + internal static CSharpType FromSystemType(Type type, string defaultNamespace, SourceInputModel? sourceInputModel, IEnumerable? backingProperties = null) + { + var systemObjectType = SystemObjectType.Create(type, defaultNamespace, sourceInputModel, backingProperties); + return systemObjectType.Type; + } + + /// + /// This function is used to create a new CSharpType instance with a literal value. + /// If the type is a framework type, the CSharpType will be created with the literal value Constant + /// object. + /// + /// The original type to create a new CSharpType instance from. + /// The literal value of the type, if any. + /// An instance of CSharpType with a literal value property. + public static CSharpType FromLiteral(CSharpType type, object literalValue) + { + if (type.IsFrameworkType) + { + Constant? literal; + try + { + literal = new Constant(literalValue, type); + } + catch + { + literal = null; + } + + return new CSharpType(type.FrameworkType, type.IsNullable) + { + Literal = literal + }; + } + + throw new NotSupportedException("Literals are not supported in non-framework type"); + } + + /// + /// Constructs a CSharpType that represents a union type. + /// + /// The list of union item types. + /// Flag used to determine if a type is nullable. + /// A instance representing those unioned types. + public static CSharpType FromUnion(IReadOnlyList unionItemTypes, bool isNullable = false) + { + return new CSharpType(typeof(BinaryData), isNullable) + { + UnionItemTypes = unionItemTypes + }; + } + + internal static CSharpType FromSystemType(BuildContext context, Type type, IEnumerable? backingProperties = null) + => FromSystemType(type, context.DefaultNamespace, context.SourceInputModel, backingProperties); + + /// + /// Check whether two CSharpType instances equal or not + /// This is not the same as left.Equals(right) because this function only checks the names. + /// + /// The instance to compare to. + /// true if the instance are equal. false otherwise. + public bool AreNamesEqual(CSharpType? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + if (Namespace != other.Namespace) + return false; + + if (Name != other.Name) + return false; + + if (Arguments.Count != other.Arguments.Count) + return false; + + for (int i = 0; i < Arguments.Count; i++) + { + if (!Arguments[i].AreNamesEqual(other.Arguments[i])) + return false; + } + + return true; + } + + public CSharpType MakeGenericType(IReadOnlyList arguments) + { + if (IsFrameworkType) + { + return new CSharpType(FrameworkType, arguments, IsNullable); + } + else + { + return new CSharpType(Implementation, arguments, IsNullable); + } + } + + internal bool CanBeInitializedInline(Constant? defaultValue) + { + Debug.Assert(defaultValue.HasValue); + + if (!Equals(defaultValue.Value.Type) && !defaultValue.Value.Type.CanBeInitializedInline(defaultValue)) + { + return false; + } + + if (Equals(typeof(string)) || Equals(typeof(Uri))) + { + return true; + } + + if (this is { IsFrameworkType: false, Implementation: EnumType { IsExtensible: true } } && defaultValue.Value.Value != null) + { + return defaultValue.Value.IsNewInstanceSentinel; + } + + return IsValueType || defaultValue.Value.Value == null; + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Types/TypeFactory.cs b/logger/autorest.csharp/common/Generation/Types/TypeFactory.cs new file mode 100644 index 0000000..23323e8 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Types/TypeFactory.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Models.Types; +using Azure; +using Azure.Core; +using Azure.Core.Expressions.DataFactory; +using Azure.ResourceManager.Models; +using Azure.ResourceManager.Resources.Models; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Generation.Types +{ + internal class TypeFactory + { + private readonly OutputLibrary _library; + public Type UnknownType { get; } + + public TypeFactory(OutputLibrary library, Type unknownType) + { + _library = library; + UnknownType = unknownType; + } + + private static readonly Type _azureResponseErrorType = typeof(ResponseError); + private static readonly Type _managedIdentity = typeof(ManagedServiceIdentity); + private static readonly Type _userAssignedIdentity = typeof(UserAssignedIdentity); + private static readonly Type _extendedLocation = typeof(ExtendedLocation); + + /// + /// This method will attempt to retrieve the of the input type. + /// + /// The input type to convert. + /// The of the input type. + public CSharpType CreateType(InputType inputType) => inputType switch + { + InputLiteralType literalType => CSharpType.FromLiteral(CreateType(literalType.ValueType), literalType.Value), + InputUnionType unionType => CSharpType.FromUnion(unionType.VariantTypes.Select(CreateType).ToArray()), + InputListType { CrossLanguageDefinitionId: "Azure.Core.EmbeddingVector" } listType => new CSharpType(typeof(ReadOnlyMemory<>), CreateType(listType.ValueType)), + InputListType listType => new CSharpType(typeof(IList<>), CreateType(listType.ValueType)), + InputDictionaryType dictionaryType => new CSharpType(typeof(IDictionary<,>), typeof(string), CreateType(dictionaryType.ValueType)), + InputEnumType enumType => _library.ResolveEnum(enumType), + InputModelType model => CreateModelType(model), + InputNullableType nullableType => CreateType(nullableType.Type).WithNullable(true), + InputPrimitiveType primitiveType => CreatePrimitiveType(primitiveType), + InputDateTimeType dateTimeType => new CSharpType(typeof(DateTimeOffset)), + InputDurationType durationType => new CSharpType(typeof(TimeSpan)), + _ => throw new InvalidOperationException($"Unknown type: {inputType}") + }; + + private CSharpType CreatePrimitiveType(in InputPrimitiveType inputType) + { + InputPrimitiveType? primitiveType = inputType; + while (primitiveType != null) + { + if (_knownAzurePrimitiveTypes.TryGetValue(primitiveType.CrossLanguageDefinitionId, out var knownType)) + { + return knownType; + } + + primitiveType = primitiveType.BaseType; + } + + return inputType.Kind switch + { + InputPrimitiveTypeKind.Boolean => new CSharpType(typeof(bool)), + InputPrimitiveTypeKind.Bytes => Configuration.ShouldTreatBase64AsBinaryData ? new CSharpType(typeof(BinaryData)) : new CSharpType(typeof(byte[])), + InputPrimitiveTypeKind.PlainDate => new CSharpType(typeof(DateTimeOffset)), + InputPrimitiveTypeKind.Decimal => new CSharpType(typeof(decimal)), + InputPrimitiveTypeKind.Decimal128 => new CSharpType(typeof(decimal)), + InputPrimitiveTypeKind.PlainTime => new CSharpType(typeof(TimeSpan)), + InputPrimitiveTypeKind.Float32 => new CSharpType(typeof(float)), + InputPrimitiveTypeKind.Float64 => new CSharpType(typeof(double)), + InputPrimitiveTypeKind.Int8 => new CSharpType(typeof(sbyte)), + InputPrimitiveTypeKind.UInt8 => new CSharpType(typeof(byte)), + InputPrimitiveTypeKind.Int32 => new CSharpType(typeof(int)), + InputPrimitiveTypeKind.Int64 => new CSharpType(typeof(long)), + InputPrimitiveTypeKind.SafeInt => new CSharpType(typeof(long)), + InputPrimitiveTypeKind.Integer => new CSharpType(typeof(long)), // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Float => new CSharpType(typeof(double)), // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Numeric => new CSharpType(typeof(double)), // in typespec, numeric is the base type of number types, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Stream => new CSharpType(typeof(Stream)), + InputPrimitiveTypeKind.String => new CSharpType(typeof(string)), + InputPrimitiveTypeKind.Url => new CSharpType(typeof(Uri)), + InputPrimitiveTypeKind.Unknown => UnknownType, + _ => new CSharpType(typeof(object)), + }; + } + + private static readonly IReadOnlyDictionary _knownAzurePrimitiveTypes = new Dictionary + { + [InputPrimitiveType.UuidId] = typeof(Guid), + [InputPrimitiveType.IPv4AddressId] = typeof(IPAddress), + [InputPrimitiveType.IPv6AddressId] = typeof(IPAddress), + [InputPrimitiveType.ETagId] = typeof(ETag), + [InputPrimitiveType.AzureLocationId] = typeof(AzureLocation), + [InputPrimitiveType.ArmIdId] = typeof(ResourceIdentifier), + + [InputPrimitiveType.CharId] = typeof(char), + [InputPrimitiveType.ContentTypeId] = typeof(ContentType), + [InputPrimitiveType.ResourceTypeId] = typeof(ResourceType), + + [InputPrimitiveType.ObjectId] = typeof(object), + [InputPrimitiveType.RequestMethodId] = typeof(RequestMethod), + [InputPrimitiveType.IPAddressId] = typeof(IPAddress), + }; + + private IReadOnlyDictionary? _knownAzureModelTypes; + private IReadOnlyDictionary KnownAzureModelTypes => _knownAzureModelTypes ??= new Dictionary + { + ["Azure.Core.Foundations.Error"] = SystemObjectType.Create(_azureResponseErrorType, _azureResponseErrorType.Namespace!, null).Type, + ["Azure.ResourceManager.CommonTypes.ManagedServiceIdentity"] = SystemObjectType.Create(_managedIdentity, _managedIdentity.Namespace!, null).Type, + ["Azure.ResourceManager.CommonTypes.UserAssignedIdentity"] = SystemObjectType.Create(_userAssignedIdentity, _userAssignedIdentity.Namespace!, null).Type, + ["Azure.ResourceManager.CommonTypes.ExtendedLocation"] = SystemObjectType.Create(_extendedLocation, _extendedLocation.Namespace!, null).Type, + }; + + private CSharpType CreateModelType(in InputModelType model) + { + // special handling data factory element + if (model.CrossLanguageDefinitionId == "Azure.Core.Resources.DataFactoryElement") + { + return new CSharpType(typeof(DataFactoryElement<>), CreateTypeForDataFactoryElement(model.ArgumentTypes![0])); + } + // handle other known model types + if (KnownAzureModelTypes.TryGetValue(model.CrossLanguageDefinitionId, out var type)) + { + return type; + } + return _library.ResolveModel(model); + } + + /// + /// This method is a shimming layer for HLC specially in DFE. In DFE, we always have `BinaryData` instead of `object` therefore we need to escape the `Any` to always return `BinaryData`. + /// + /// + /// + private CSharpType CreateTypeForDataFactoryElement(InputType inputType) => inputType switch + { + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Unknown } => typeof(BinaryData), + _ => CreateType(inputType) + }; + + internal static Type? ToXMsFormatType(string? format) => format switch + { + XMsFormat.ArmId => typeof(ResourceIdentifier), + XMsFormat.AzureLocation => typeof(AzureLocation), + XMsFormat.DateTime => typeof(DateTimeOffset), + XMsFormat.DateTimeRFC1123 => typeof(DateTimeOffset), + XMsFormat.DateTimeUnix => typeof(DateTimeOffset), + XMsFormat.DurationConstant => typeof(TimeSpan), + XMsFormat.ETag => typeof(ETag), + XMsFormat.ResourceType => typeof(ResourceType), + XMsFormat.Object => typeof(object), + XMsFormat.IPAddress => typeof(IPAddress), + XMsFormat.ContentType => typeof(ContentType), + XMsFormat.RequestMethod => typeof(RequestMethod), + XMsFormat.DataFactoryElementOfString => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfInt => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfDouble => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfBool => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfDateTime => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfDuration => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfUri => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfObject => typeof(DataFactoryElement), + XMsFormat.DataFactoryElementOfListOfString => typeof(DataFactoryElement>), + XMsFormat.DataFactoryElementOfKeyValuePairs => typeof(DataFactoryElement>), + XMsFormat.DataFactoryElementOfKeyObjectValuePairs => typeof(DataFactoryElement>), + _ => null + }; + + public CSharpType CreateType(ITypeSymbol symbol) + { + if (!TryCreateType(symbol, out var type)) + { + throw new InvalidOperationException($"Unable to find a model or framework type that corresponds to {symbol}"); + } + + return type; + } + + private static bool NoTypeValidator(System.Type type) => true; + + public bool TryCreateType(ITypeSymbol symbol, [NotNullWhen(true)] out CSharpType? type) + => TryCreateType(symbol, NoTypeValidator, out type); + + public CSharpType? GetLibraryTypeByName(string name) => _library.FindTypeByName(name); + + public bool TryCreateType(ITypeSymbol symbol, Func validator, [NotNullWhen(true)] out CSharpType? type) + { + type = null; + return symbol switch + { + IArrayTypeSymbol arrayTypeSymbol => TryCreateTypeForIArrayTypeSymbol(arrayTypeSymbol, validator, out type), + INamedTypeSymbol namedTypeSymbol => TryCreateTypeForINamedTypeSymbol(namedTypeSymbol, validator, out type), + + // We can only handle IArrayTypeSymbol of framework type and INamedTypeSymbol for now since CSharpType can't represent other types such as IArrayTypeSymbol of user types + // Instead of throwing an exception, which causes more side effects, we just return false and let the caller handle it. + _ => false + }; + } + + private bool TryCreateTypeForINamedTypeSymbol(INamedTypeSymbol namedTypeSymbol, Func validator, [NotNullWhen(true)] out CSharpType? type) + { + type = null; + if (namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + { + if (!TryCreateType(namedTypeSymbol.TypeArguments[0], validator, out type)) + { + return false; + } + type = type.WithNullable(true); + return true; + } + + Type? existingType = TryGetFrameworkType(namedTypeSymbol); + + if (existingType is not null && validator(existingType)) + { + if (!TryPopulateArguments(namedTypeSymbol.TypeArguments, validator, out var arguments)) + { + return false; + } + type = new CSharpType(existingType, arguments, isNullable: false); + } + else + { + type = _library.FindTypeByName(namedTypeSymbol.Name); + } + + if (type is null) + { + return false; + } + + if (!type.IsValueType && + namedTypeSymbol.NullableAnnotation != NullableAnnotation.NotAnnotated) + { + type = type.WithNullable(true); + } + + return true; + } + + private bool TryCreateTypeForIArrayTypeSymbol(IArrayTypeSymbol symbol, Func validator, [NotNullWhen(true)] out CSharpType? type) + { + type = null; + if (symbol is not IArrayTypeSymbol arrayTypeSymbol) + { + return false; + } + + // For IArrayTypeSymbol, we can only handle it when the element type is a framework type. + var arrayType = TryGetFrameworkType(arrayTypeSymbol); + if (arrayType is not null && validator(arrayType)) + { + type = new CSharpType(arrayType, arrayType.IsValueType && symbol.NullableAnnotation != NullableAnnotation.NotAnnotated); + return true; + } + return false; + } + + private Type? TryGetFrameworkType(ISymbol namedTypeSymbol) + { + var fullMetadataName = GetFullMetadataName(namedTypeSymbol); + var fullyQualifiedName = $"{fullMetadataName}, {namedTypeSymbol.ContainingAssembly?.Name}"; + return Type.GetType(fullMetadataName) ?? Type.GetType(fullyQualifiedName); + } + + // There can be argument type missing + private bool TryPopulateArguments(ImmutableArray typeArguments, Func validator, [NotNullWhen(true)] out IReadOnlyList? arguments) + { + arguments = null; + var result = new List(); + foreach (var typeArgument in typeArguments) + { + if (!TryCreateType(typeArgument, validator, out CSharpType? type)) + { + return false; + } + result.Add(type); + } + arguments = result; + return true; + } + + private string GetFullMetadataName(ISymbol namedTypeSymbol) + { + StringBuilder builder = new StringBuilder(); + + BuildFullMetadataName(builder, namedTypeSymbol); + + return builder.ToString(); + } + + private void BuildFullMetadataName(StringBuilder builder, ISymbol symbol) + { + if (symbol is IArrayTypeSymbol arrayTypeSymbol) + { + BuildFullMetadataName(builder, arrayTypeSymbol.ElementType); + builder.Append("[]"); + return; + } + + if (symbol.ContainingNamespace != null && + !symbol.ContainingNamespace.IsGlobalNamespace) + { + BuildFullMetadataName(builder, symbol.ContainingNamespace); + builder.Append("."); + } + + builder.Append(symbol.MetadataName); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/AspDotNetExtensionWriter.cs b/logger/autorest.csharp/common/Generation/Writers/AspDotNetExtensionWriter.cs new file mode 100644 index 0000000..d21ef81 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/AspDotNetExtensionWriter.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using Azure.Core; +using Azure.Core.Extensions; + +namespace AutoRest.CSharp.Common.Generation.Writers +{ + internal class AspDotNetExtensionWriter + { + private CodeWriter _writer; + + private AspDotNetExtensionTypeProvider This { get; } + + public AspDotNetExtensionWriter(AspDotNetExtensionTypeProvider aspDotNetExtension) + { + _writer = new CodeWriter(); + This = aspDotNetExtension; + } + + public void Write() + { + using (_writer.Namespace(This.Declaration.Namespace)) + { + WriteClassDeclaration(); + using (_writer.Scope()) + { + WriteImplementations(); + } + } + } + + private void WriteClassDeclaration() + { + _writer.WriteXmlDocumentationSummary(This.Description); + _writer.AppendRaw(This.Declaration.Accessibility) + .AppendRaw(" static") + .Append($" partial class {This.Type.Name}"); + } + + private void WriteImplementations() + { + foreach (var (signature, (declarations, values)) in This.ExtesnsionMethods) + { + using (_writer.WriteCommonMethodWithoutValidation(signature, null, false, false)) + { + var builder = signature.Parameters.First(); + var arguments = signature.ReturnType!.Arguments; + var clientType = arguments.First(); + _writer.Append($"return {builder.Name:I}.RegisterClientFactory") + .AppendRaw("<"); + foreach (var argument in arguments) + { + _writer.Append($"{argument},"); + } + _writer.RemoveTrailingComma(); + _writer.AppendRaw(">(("); + foreach (var declaration in declarations) + { + _writer.Append($"{declaration},"); + } + _writer.RemoveTrailingComma(); + _writer.Append($") => new {clientType}("); + foreach (var value in values) + { + _writer.Append($"{value},"); + } + _writer.RemoveTrailingComma(); + _writer.LineRaw("));"); + } + _writer.Line(); + } + + foreach (var signature in This.ExtensionMethodsWithoutCallback) + { + using (_writer.WriteCommonMethodWithoutValidation(signature, null, false, false)) + { + var builder = signature.Parameters.First(); + var otherParameters = signature.Parameters.Skip(1); + _writer.Append($"return {builder.Name:I}.RegisterClientFactory") + .AppendRaw("<"); + foreach (var argument in signature.ReturnType!.Arguments) + { + _writer.Append($"{argument},"); + } + _writer.RemoveTrailingComma(); + _writer.AppendRaw(">("); + foreach (var parameter in otherParameters) + { + _writer.Append($"{parameter.Name:I},"); + } + _writer.RemoveTrailingComma(); + _writer.LineRaw(");"); + } + } + } + + public override string ToString() => _writer.ToString(); + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/CSProjWriter.cs b/logger/autorest.csharp/common/Generation/Writers/CSProjWriter.cs new file mode 100644 index 0000000..d371106 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/CSProjWriter.cs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Xml; + +namespace AutoRest.CSharp.Generation.Writers; + +internal class CSProjWriter +{ + public CSProjWriter() + { + ProjectReferences = new List(); + PackageReferences = new List(); + PrivatePackageReferences = new List(); + CompileIncludes = new List(); + } + + public CSProjProperty? Description { get; init; } + + public CSProjProperty? AssemblyTitle { get; init; } + + public CSProjProperty? Version { get; init; } + + public CSProjProperty? PackageTags { get; init; } + + public CSProjProperty? TargetFrameworks { get; init; } + + public CSProjProperty? TargetFramework { get; init; } + + public CSProjProperty? IncludeOperationsSharedSource { get; init; } + + public CSProjProperty? LangVersion { get; init; } + + public CSProjProperty? GenerateDocumentationFile { get; init; } + + public CSProjProperty? NoWarn { get; init; } + + public CSProjProperty? TreatWarningsAsErrors { get; init; } + + public CSProjProperty? Nullable { get; init; } + + public CSProjProperty? IncludeManagementSharedCode { get; init; } + + public CSProjProperty? IncludeGeneratorSharedCode { get; init; } + + public CSProjProperty? DefineConstants { get; init; } + + public CSProjProperty? RestoreAdditionalProjectSources { get; init; } + + public IList ProjectReferences { get; } + + public IList PackageReferences { get; } + + public IList PrivatePackageReferences { get; } + + public IList CompileIncludes { get; } + + public string Write() + { + var builder = new StringBuilder(); + using var writer = XmlWriter.Create(builder, new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true + }); + writer.WriteStartDocument(); + // write the Project element + writer.WriteStartElement("Project"); + writer.WriteAttributeString("Sdk", "Microsoft.NET.Sdk"); + // write properties + WriteProperties(writer); + + // write the first ItemGroup for compile include + if (CompileIncludes.Count > 0) + { + // this is the only way I know to write a blank line in an xml document using APIs instead of just write raw strings + // feel free to change this if other elegant way is found. + writer.Flush(); + builder.AppendLine(); + writer.WriteStartElement("ItemGroup"); + foreach (var compileInclude in CompileIncludes) + { + WriteCompileInclude(writer, compileInclude); + } + writer.WriteEndElement(); + } + + // write project references + if (ProjectReferences.Count > 0) + { + writer.Flush(); + builder.AppendLine(); + writer.WriteStartElement("ItemGroup"); + foreach (var package in ProjectReferences) + { + WriteProjectReference(writer, package); + } + writer.WriteEndElement(); + } + + // write package references + if (PackageReferences.Count > 0) + { + writer.Flush(); + builder.AppendLine(); + writer.WriteStartElement("ItemGroup"); + foreach (var package in PackageReferences) + { + WritePackageReference(writer, package); + } + writer.WriteEndElement(); + } + + // write private package references + if (PrivatePackageReferences.Count > 0) + { + writer.Flush(); + builder.AppendLine(); + writer.WriteStartElement("ItemGroup"); + foreach (var package in PrivatePackageReferences) + { + WritePackageReference(writer, package, true); + } + writer.WriteEndElement(); + } + + writer.WriteEndDocument(); + writer.Close(); + writer.Flush(); + + // add an empty on the end of file + builder.AppendLine(); + + return builder.ToString(); + } + + private static readonly IEnumerable _properties = typeof(CSProjWriter).GetProperties(BindingFlags.Public | BindingFlags.Instance); + + private void WriteProperties(XmlWriter writer) + { + writer.WriteStartElement("PropertyGroup"); + // this will write those properties in the same order as they are defined in this class + // introduce this method to save the effort of writing every property one by one + foreach (var property in _properties) + { + // only include those CSProjProperty types + if (property.PropertyType != typeof(CSProjProperty)) + continue; + // invoke the WriteElementIfNotNull method on each of them + var value = (CSProjProperty?)property.GetValue(this); + WriteElementIfNotNull(writer, property.Name, value); + } + writer.WriteEndElement(); + } + + private void WriteElementIfNotNull(XmlWriter writer, string name, CSProjProperty? property) + { + if (property == null) + return; + + if (property.Comment != null) + { + writer.WriteComment(property.Comment); + } + + writer.WriteElementString(name, property.Value); + } + + private void WriteCompileInclude(XmlWriter writer, CSProjCompileInclude compileInclude) + { + writer.WriteStartElement("Compile"); + writer.WriteAttributeString("Include", compileInclude.Include); + if (compileInclude.LinkBase != null) + { + writer.WriteAttributeString("LinkBase", compileInclude.LinkBase); + } + writer.WriteEndElement(); + } + + private void WriteProjectReference(XmlWriter writer, CSProjDependencyPackage package) + { + writer.WriteStartElement("ProjectReference"); + writer.WriteAttributeString("Include", package.PackageName); + writer.WriteEndElement(); + } + + private void WritePackageReference(XmlWriter writer, CSProjDependencyPackage package, bool isPrivateAsset = false) + { + writer.WriteStartElement("PackageReference"); + writer.WriteAttributeString("Include", package.PackageName); + if (package.Version != null) + { + writer.WriteAttributeString("Version", package.Version); + } + if (isPrivateAsset) + { + writer.WriteAttributeString("PrivateAssets", "All"); + } + writer.WriteEndElement(); + } + + public record CSProjProperty(string Value, string? Comment) + { + public CSProjProperty(string value) : this(value, null) + { } + + public static implicit operator CSProjProperty(string value) => new(value, null); + public static implicit operator CSProjProperty(bool value) => new(XmlConvert.ToString(value), null); + } + + public record CSProjDependencyPackage(string PackageName, string? Version) + { + public CSProjDependencyPackage(string packageName) : this(packageName, null) { } + } + + public record CSProjCompileInclude(string Include, string? LinkBase) + { + public CSProjCompileInclude(string include) : this(include, null) { } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/ClientOptionsWriter.cs b/logger/autorest.csharp/common/Generation/Writers/ClientOptionsWriter.cs new file mode 100644 index 0000000..577da73 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/ClientOptionsWriter.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class ClientOptionsWriter + { + public static void WriteClientOptions(CodeWriter writer, ClientOptionsTypeProvider clientOptions) + { + using (writer.Namespace(clientOptions.Type.Namespace)) + { + writer.WriteXmlDocumentationSummary(clientOptions.Description); + using (writer.Scope($"{clientOptions.Declaration.Accessibility} partial class {clientOptions.Type.Name}: {Configuration.ApiTypes.ClientOptionsType}")) + { + if (clientOptions.ApiVersions?.Count > 0) + { + writer.Line($"private const ServiceVersion LatestVersion = ServiceVersion.{clientOptions.ApiVersions.Last().Name};"); + writer.Line(); + writer.WriteXmlDocumentationSummary($"The version of the service to use."); + using (writer.Scope($"public enum ServiceVersion")) + { + foreach (var apiVersion in clientOptions.ApiVersions) + { + writer.WriteXmlDocumentationSummary($"{apiVersion.Description}"); + writer.Line($"{apiVersion.Name} = {apiVersion.Value:L},"); + } + } + writer.Line(); + + writer.Line($"internal string Version {{ get; }}"); + writer.Line(); + + writer.WriteXmlDocumentationSummary($"Initializes new instance of {clientOptions.Type.Name}."); + using (writer.Scope($"public {clientOptions.Type.Name}(ServiceVersion version = LatestVersion)")) + { + writer.Append($"Version = version "); + using (writer.Scope($"switch", end: "};")) + { + foreach (var apiVersion in clientOptions.ApiVersions) + { + writer.Line($"ServiceVersion.{apiVersion.Name} => {apiVersion.StringValue:L},"); + } + + writer.Line($"_ => throw new {typeof(NotSupportedException)}()"); + } + } + + writer.Line(); + } + + foreach (var parameter in clientOptions.AdditionalParameters) + { + writer.WriteXmlDocumentationSummary(parameter.Description); + writer.Line($"public {parameter.Type} {parameter.Name.ToCleanName()} {{ get; set; }}"); + writer.Line(); + } + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/ClientWriter.cs b/logger/autorest.csharp/common/Generation/Writers/ClientWriter.cs new file mode 100644 index 0000000..8022931 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/ClientWriter.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Common.Generation.Writers +{ + internal class ClientWriter + { + protected const string ClientDiagnosticsVariable = "clientDiagnostics"; + protected const string PipelineVariable = "pipeline"; + protected const string PipelineProperty = "Pipeline"; + protected const string PipelineField = "_" + PipelineVariable; + protected const string DiagnosticsProperty = "Diagnostics"; + + protected static readonly Reference ClientDiagnosticsField = new Reference("_" + ClientDiagnosticsVariable, typeof(ClientDiagnostics)); + + protected virtual string RestClientAccessibility => "internal"; + + protected virtual string RestClientField => "RestClient"; + + protected static string CreateMethodName(string name, bool async) => async ? $"{name}Async" : name; + + protected void WriteClientFields(CodeWriter writer, RestClient client, bool writePipelineField) + { + if (Configuration.IsBranded) + { + writer.Line($"private readonly {typeof(ClientDiagnostics)} {ClientDiagnosticsField.GetReferenceFormattable()};"); + } + if (writePipelineField) + { + writer.Line($"private readonly {Configuration.ApiTypes.HttpPipelineType} {PipelineField};"); + } + writer.Append($"{RestClientAccessibility} {client.Type} {RestClientField}").LineRaw(" { get; }"); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/CodeWriter.cs b/logger/autorest.csharp/common/Generation/Writers/CodeWriter.cs new file mode 100644 index 0000000..8b8e350 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/CodeWriter.cs @@ -0,0 +1,850 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis.CSharp; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class CodeWriter + { + private const int DefaultLength = 1024; + private static readonly string _newLine = "\n"; + private static readonly string _braceNewLine = "{\n"; + + private readonly HashSet _usingNamespaces = new HashSet(); + + private readonly Stack _scopes; + private string? _currentNamespace; + + private char[] _builder; + private int _length; + private bool _writingXmlDocumentation; + + public CodeWriter() + { + _builder = ArrayPool.Shared.Rent(DefaultLength); + + _scopes = new Stack(); + _scopes.Push(new CodeWriterScope(this, "", false)); + } + + public CodeWriterScope Scope(FormattableString line, string start = "{", string end = "}", bool newLine = true, CodeWriterScopeDeclarations? scopeDeclarations = null) + { + ValidateDeclarations(scopeDeclarations); + CodeWriterScope codeWriterScope = new CodeWriterScope(this, end, newLine); + _scopes.Push(codeWriterScope); + Line(line); + LineRaw(start); + AddDeclarationsToScope(scopeDeclarations); + return codeWriterScope; + } + + public CodeWriterScope Scope() + { + return ScopeRaw(); + } + + private void ValidateDeclarations(CodeWriterScopeDeclarations? scopeDeclarations) + { + if (scopeDeclarations == null) + { + return; + } + + foreach (var declarationName in scopeDeclarations.Names) + { + if (!IsAvailable(declarationName)) + { + throw new InvalidOperationException($"Variable with name '{declarationName}' is declared already."); + } + } + } + + private void AddDeclarationsToScope(CodeWriterScopeDeclarations? scopeDeclarations) + { + if (scopeDeclarations == null) + { + return; + } + + var currentScope = _scopes.Peek(); + + foreach (var declarationName in scopeDeclarations.Names) + { + foreach (var scope in _scopes) + { + scope.AllDefinedIdentifiers.Add(declarationName); + } + + currentScope.Identifiers.Add(declarationName); + } + } + + private CodeWriterScope ScopeRaw(string start = "{", string end = "}", bool newLine = true) + { + LineRaw(start); + CodeWriterScope codeWriterScope = new CodeWriterScope(this, end, newLine); + _scopes.Push(codeWriterScope); + return codeWriterScope; + } + + public CodeWriterScope Namespace(string @namespace) + { + _currentNamespace = @namespace; + Line($"namespace {@namespace}"); + return Scope(); + } + + public CodeWriter Append(FormattableString formattableString) + { + if (formattableString.ArgumentCount == 0) + { + return AppendRaw(formattableString.ToString()); + } + + const string literalFormatString = ":L"; + const string declarationFormatString = ":D"; // :D :) + const string identifierFormatString = ":I"; + const string crefFormatString = ":C"; // wraps content into "see cref" tag, available only in xmlDoc + foreach ((var span, bool isLiteral, int index) in StringExtensions.GetPathParts(formattableString.Format)) + { + if (isLiteral) + { + AppendRaw(span); + continue; + } + + var argument = formattableString.GetArgument(index); + var isDeclaration = span.EndsWith(declarationFormatString); + var isIdentifier = span.EndsWith(identifierFormatString); + var isLiteralFormat = span.EndsWith(literalFormatString); + var isCref = span.EndsWith(crefFormatString); + + if (isCref) + { + if (!_writingXmlDocumentation) + { + throw new InvalidOperationException($"':C' formatter can be used only inside XmlDoc"); + } + + switch (argument) + { + case Type t: + AppendTypeForCRef(new CSharpType(t)); + break; + case CSharpType t: + AppendTypeForCRef(t); + break; + default: + Append($""); + break; + } + + continue; + } + + switch (argument) + { + case IEnumerable fss: + foreach (var fs in fss) + { + Append(fs); + } + break; + case FormattableString fs: + Append(fs); + break; + case Type t: + AppendType(new CSharpType(t), false, false); + break; + case CSharpType t: + AppendType(t, isDeclaration, false); + break; + case CodeWriterDeclaration declaration when isDeclaration: + Declaration(declaration); + break; + case CodeWriterDeclaration declaration: + Append(declaration); + break; + case ValueExpression expression: + expression.Write(this); + break; + case var _ when isLiteralFormat: + Literal(argument); + break; + default: + string? s = argument?.ToString(); + if (s == null) + { + throw new ArgumentNullException(index.ToString()); + } + + if (isDeclaration) + { + Declaration(s); + } + else if (isIdentifier) + { + Identifier(s); + } + else + { + AppendRaw(s); + } + break; + } + } + + return this; + } + + public void UseNamespace(string @namespace) + { + if (_currentNamespace == @namespace) + { + return; + } + + if (_currentNamespace is not null && _currentNamespace.Length > @namespace.Length && _currentNamespace.StartsWith(@namespace) && _currentNamespace[@namespace.Length] == '.') + { + return; + } + + _usingNamespaces.Add(@namespace); + } + + public CodeWriter WriteRawXmlDocumentation(FormattableString? content) + { + // if xml docs is globally turned off by the configuration, write nothing + if (Configuration.DisableXmlDocs) + return this; + + if (content is null) + return this; + + var lines = content.ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var xmlLines = string.Join('\n', lines.Select(l => "/// " + l)); + AppendRaw(xmlLines); + Line(); + return this; + } + + public CodeWriter AppendXmlDocumentation(FormattableString startTag, FormattableString endTag, FormattableString content) + { + // if xml docs is globally turned off by the configuration, write nothing + if (Configuration.DisableXmlDocs) + return this; + + const string xmlDoc = "/// "; + const string xmlDocNewLine = "\n/// "; + + var commentStart = _length; + AppendRaw(CurrentLine.IsEmpty ? xmlDoc : xmlDocNewLine); + + var startTagStart = _length; + Append(startTag); + _writingXmlDocumentation = true; + + var contentStart = _length; + if (content.Format.Length > 0) + { + Append(content); + } + var contentEnd = _length; + + _writingXmlDocumentation = false; + Append(endTag); + + if (contentStart == contentEnd) + { + var startTagSpan = WrittenText.Slice(startTagStart + 1, contentStart - startTagStart - 1); + var endTagSpan = WrittenText.Slice(contentEnd + 2); + + if (startTagSpan.SequenceEqual(endTagSpan)) + { + // Remove empty tags + _length = commentStart; + } + else + { + Line(); + } + + return this; + } + + Line(); + var contentSpan = _builder.AsSpan(contentStart, contentEnd - contentStart); + + var lastLineBreak = contentSpan.LastIndexOf(_newLine); + if (lastLineBreak == -1) + { + // Add spaces and dot to match existing formatting + if (contentEnd > contentStart) + { + if (contentSpan[^1] != ' ') + { + InsertRaw(contentSpan[^1] == '.' ? " " : ". ", contentEnd); + } + else + { + var trimmedContentSpan = contentSpan.TrimEnd(); + if (trimmedContentSpan[^1] != '.') + { + InsertRaw(".", contentStart + trimmedContentSpan.Length); + } + } + + if (contentSpan[0] != ' ') + { + InsertRaw(" ", contentStart); + } + } + return this; + } + + if (lastLineBreak != contentSpan.Length) + { + InsertRaw(xmlDocNewLine, contentEnd); + } + + while (lastLineBreak != -1) + { + InsertRaw(xmlDoc, lastLineBreak + contentStart + 1); + contentSpan = contentSpan.Slice(0, lastLineBreak); + lastLineBreak = contentSpan.LastIndexOf(_newLine); + } + + if (contentSpan.Length > 0) + { + InsertRaw(xmlDocNewLine, contentStart); + } + + return this; + } + + public CodeWriter WriteXmlDocumentationInheritDoc(CSharpType? crefType = null) + { + // TODO Temporary -- maybe we should consolidate this into the CodeWriter.AppendXmlDocumentation method as well + // currently the CodeWriter.AppendXmlDocumentation will remove empty tags, and it does not support we just write a tag like , therefore now we cannot use it here. + if (Configuration.DisableXmlDocs) + return this; + + return crefType == null + ? Line($"/// ") + : Line($"/// "); + } + + protected string GetTemporaryVariable(string s) + { + if (IsAvailable(s)) + { + return s; + } + + for (int i = 0; i < 100; i++) + { + var name = s + i; + if (IsAvailable(name)) + { + return name; + } + } + throw new InvalidOperationException("Can't find suitable variable name."); + } + + private bool IsAvailable(string s) + { + if (_scopes.TryPeek(out var currentScope)) + { + if (currentScope.AllDefinedIdentifiers.Contains(s)) + { + return false; + } + } + + foreach (CodeWriterScope codeWriterScope in _scopes) + { + if (codeWriterScope.Identifiers.Contains(s)) + { + return false; + } + } + + return true; + } + + private void AppendTypeForCRef(CSharpType type) + { + // Because of the limitations of type cref in XmlDoc + // we add "?" nullability operator after `cref` block + var isNullable = type is { IsNullable: true, IsValueType: true }; + var arguments = type.IsGenericType ? type.Arguments : null; + + type = type.WithNullable(false); + if (type.IsGenericType) + { + type = type.GetGenericTypeDefinition(); + } + + AppendRaw($""); + + if (isNullable) + { + AppendRaw("?"); + } + + if (arguments is not null) + { + for (int i = 0; i < arguments.Count; i++) + { + var argument = arguments[i]; + if (argument is { IsFrameworkType: true, FrameworkType.IsGenericParameter: true }) + { + continue; + } + + AppendRaw(" where "); + AppendType(type.Arguments[i], false, false); + AppendRaw(" is"); + if (argument.IsArray) + { + AppendRaw(" an array of type "); + argument = argument.ElementType; + } + else + { + AppendRaw(" of type "); + } + + // If argument type is non-generic, we can provide "see cref" for it + // Otherwise, just write its name + if (argument.IsGenericType) + { + AppendRaw(""); + AppendType(argument, false, true); + AppendRaw(""); + } + else + { + AppendTypeForCRef(argument); + } + + AppendRaw(","); + } + RemoveTrailingComma(); + } + } + + private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameOnly) + { + if (type.IsFrameworkType && type.FrameworkType.IsArray && type.FrameworkType.GetGenericArguments().Any()) + { + AppendType(type.FrameworkType.GetElementType()!, isDeclaration, writeTypeNameOnly); + AppendRaw("[]"); + return; + } + + if (type.TryGetCSharpFriendlyName(out var keywordName)) + { + AppendRaw(keywordName); + if (type.FrameworkType.IsGenericParameter && type.IsNullable) + { + AppendRaw("?"); + } + } + else if (isDeclaration && !type.IsFrameworkType) + { + AppendRaw(type.Implementation.Declaration.Name); + } + else if (writeTypeNameOnly) + { + AppendRaw(type.Name); + } + else if (type.DeclaringType != null) + { + AppendType(type.DeclaringType, isDeclaration, writeTypeNameOnly); + AppendRaw("."); + AppendRaw(type.Name); + } + else + { + UseNamespace(type.Namespace); + + AppendRaw("global::"); + AppendRaw(type.Namespace); + AppendRaw("."); + AppendRaw(type.Name); + } + + if (type.Arguments.Any()) + { + AppendRaw(_writingXmlDocumentation ? "{" : "<"); + foreach (var typeArgument in type.Arguments) + { + AppendType(typeArgument, false, writeTypeNameOnly); + AppendRaw(_writingXmlDocumentation ? "," : ", "); + } + RemoveTrailingComma(); + AppendRaw(_writingXmlDocumentation ? "}" : ">"); + } + + if (!isDeclaration && type is { IsNullable: true, IsValueType: true }) + { + AppendRaw("?"); + } + } + + public CodeWriter Literal(object? o) + { + return AppendRaw(o switch + { + null => "null", + string s => SyntaxFactory.Literal(s).ToString(), + int i => SyntaxFactory.Literal(i).ToString(), + long l => SyntaxFactory.Literal(l).ToString(), + decimal d => SyntaxFactory.Literal(d).ToString(), + double d => SyntaxFactory.Literal(d).ToString(), + float f => SyntaxFactory.Literal(f).ToString(), + char c => SyntaxFactory.Literal(c).ToString(), + sbyte sc => SyntaxFactory.Literal(sc).ToString(), + byte b => SyntaxFactory.Literal(b).ToString(), + bool b => b ? "true" : "false", + BinaryData bd => bd.ToArray().Length == 0 ? "new byte[] { }" : SyntaxFactory.Literal(bd.ToString()).ToString(), + _ => throw new NotImplementedException($"Unknown literal type {o?.GetType().Name ?? "'null'"}") + }); + } + + public CodeWriter Line(FormattableString formattableString) + { + Append(formattableString); + Line(); + + return this; + } + + public CodeWriter Line() + { + LineRaw(string.Empty); + + return this; + } + + public ReadOnlySpan WrittenText => _builder.AsSpan(0, _length); + + private ReadOnlySpan PreviousLine + { + get + { + var writtenText = WrittenText; + + var indexOfNewLine = writtenText.LastIndexOf(_newLine); + if (indexOfNewLine == -1) + { + return Span.Empty; + } + + var writtenTextBeforeLastLine = writtenText.Slice(0, indexOfNewLine); + var indexOfPreviousNewLine = writtenTextBeforeLastLine.LastIndexOf(_newLine); + if (indexOfPreviousNewLine == -1) + { + return writtenText.Slice(0, indexOfNewLine + 1); + } + + return writtenText.Slice(indexOfPreviousNewLine + 1, indexOfNewLine - indexOfPreviousNewLine); + } + } + + private ReadOnlySpan CurrentLine + { + get + { + var writtenText = WrittenText; + + var indexOfNewLine = writtenText.LastIndexOf(_newLine); + if (indexOfNewLine == -1) + { + return writtenText; + } + + return writtenText.Slice(indexOfNewLine + 1); + } + } + + private void EnsureSpace(int space) + { + if (_builder.Length - _length < space) + { + var newBuilder = ArrayPool.Shared.Rent(Math.Max(_builder.Length + space, _builder.Length * 2)); + _builder.AsSpan().CopyTo(newBuilder); + + ArrayPool.Shared.Return(_builder); + _builder = newBuilder; + } + } + + public CodeWriter LineRaw(string str) + { + AppendRaw(str); + + var previousLine = PreviousLine; + + if (CurrentLine.IsEmpty && + (previousLine.SequenceEqual(_newLine) || previousLine.EndsWith(_braceNewLine))) + { + return this; + } + + AppendRaw(_newLine); + + return this; + } + + public CodeWriter AppendRaw(string str) => AppendRaw(str.AsSpan()); + + private CodeWriter AppendRaw(ReadOnlySpan span) => InsertRaw(span, _length); + + private CodeWriter InsertRaw(ReadOnlySpan span, int position, bool skipNewLineCheck = false) + { + Debug.Assert(0 <= position); + Debug.Assert(position <= _length); + + if (!skipNewLineCheck) + { + var newLineSpan = "\r\n".AsSpan(); + var newLineIndex = span.IndexOf(newLineSpan); + while (newLineIndex != -1) + { + InsertRaw(span.Slice(0, newLineIndex), position, skipNewLineCheck: true); + position += newLineIndex; + span = span.Slice(newLineIndex + 1); + newLineIndex = span.IndexOf(newLineSpan); + } + } + + EnsureSpace(span.Length); + if (position < _length) + { + Array.Copy(_builder, position, _builder, span.Length + position, _length - position); + } + + span.CopyTo(_builder.AsSpan(position)); + _length += span.Length; + return this; + } + + public CodeWriter Identifier(string identifier) + { + if (StringExtensions.IsCSharpKeyword(identifier)) + { + AppendRaw("@"); + } + return AppendRaw(identifier); + } + + protected CodeWriter Declaration(string declaration) + { + foreach (var scope in _scopes) + { + scope.AllDefinedIdentifiers.Add(declaration); + } + + _scopes.Peek().Identifiers.Add(declaration); + + return Identifier(declaration); + } + + public virtual CodeWriter Declaration(CodeWriterDeclaration declaration) + { + if (_writingXmlDocumentation) + { + throw new InvalidOperationException("Can't declare variables inside documentation."); + } + + declaration.SetActualName(GetTemporaryVariable(declaration.RequestedName)); + _scopes.Peek().Declarations.Add(declaration); + return Declaration(declaration.ActualName); + } + + public override string ToString() + { + return ToString(true); + } + + public string ToString(bool header) + { + if (_length == 0) + { + return string.Empty; + } + + var builder = new StringBuilder(_length); + IEnumerable namespaces = _usingNamespaces + .OrderByDescending(ns => ns.StartsWith("System")) + .ThenBy(ns => ns, StringComparer.Ordinal); + if (header) + { + builder.Append(Configuration.ApiTypes.LicenseString); + builder.AppendLine("// "); + builder.AppendLine(); + builder.AppendLine("#nullable disable"); + builder.AppendLine(); + + foreach (string ns in namespaces) + { + builder.Append("using ").Append(ns).AppendLine(";"); + } + + if (namespaces.Any()) + { + builder.AppendLine(); + } + } + + // Normalize newlines + var spanLines = _builder.AsSpan(0, _length).EnumerateLines(); + int lineCount = 0; + foreach (var line in spanLines) + { + builder.Append(line.TrimEnd()); + builder.AppendLine(); + lineCount++; + } + // Remove last new line if there are more than 1 + if (lineCount > 1) + { + builder.Remove(builder.Length - Environment.NewLine.Length, Environment.NewLine.Length); + } + return builder.ToString(); + } + + internal class CodeWriterScope : IDisposable + { + private readonly CodeWriter _writer; + private readonly string? _end; + private readonly bool _newLine; + + public HashSet Identifiers { get; } = new(); + + public HashSet AllDefinedIdentifiers { get; } = new(); + + public List Declarations { get; } = new(); + + public CodeWriterScope(CodeWriter writer, string? end, bool newLine) + { + _writer = writer; + _end = end; + _newLine = newLine; + } + + public void Dispose() + { + if (_writer != null) + { + _writer.PopScope(this); + foreach (var declaration in Declarations) + { + declaration.SetActualName(null); + } + + Declarations.Clear(); + + if (_end != null) + { + _writer.TrimNewLines(); + _writer.AppendRaw(_end); + } + + if (_newLine) + { + _writer.Line(); + } + } + } + } + + private void TrimNewLines() + { + while (PreviousLine.SequenceEqual(_newLine) && + CurrentLine.IsEmpty) + { + _length--; + } + } + + private void PopScope(CodeWriterScope expected) + { + var actual = _scopes.Pop(); + Debug.Assert(actual == expected); + } + + private int? FindLastNonWhitespaceCharacterIndex() + { + var text = WrittenText; + for (int i = text.Length - 1; i >= 0; i--) + { + if (char.IsWhiteSpace(text[i])) + { + continue; + } + + return i; + } + + return null; + } + + public void RemoveTrailingCharacter() + { + int? lastCharIndex = FindLastNonWhitespaceCharacterIndex(); + if (lastCharIndex.HasValue) + { + _length = lastCharIndex.Value; + } + } + + public void RemoveTrailingComma() + { + int? lastCharIndex = FindLastNonWhitespaceCharacterIndex(); + if (lastCharIndex.HasValue && WrittenText[lastCharIndex.Value] == ',') + { + _length = lastCharIndex.Value; + } + } + + public CodeWriterScope AmbientScope() + { + var codeWriterScope = new CodeWriterScope(this, null, false); + _scopes.Push(codeWriterScope); + return codeWriterScope; + } + + public virtual void Append(CodeWriterDeclaration declaration) + { + Identifier(declaration.ActualName); + } + + internal void WriteTypeModifiers(TypeSignatureModifiers modifiers) + { + this.AppendRawIf("public ", modifiers.HasFlag(TypeSignatureModifiers.Public)) + .AppendRawIf("internal ", modifiers.HasFlag(TypeSignatureModifiers.Internal)) + .AppendRawIf("private ", modifiers.HasFlag(TypeSignatureModifiers.Private)) + .AppendRawIf("static ", modifiers.HasFlag(TypeSignatureModifiers.Static)) + .AppendRawIf("sealed ", modifiers.HasFlag(TypeSignatureModifiers.Sealed)) + .AppendRawIf("partial ", modifiers.HasFlag(TypeSignatureModifiers.Partial)); // partial must be the last to write otherwise compiler will complain + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/CodeWriterDeclaration.cs b/logger/autorest.csharp/common/Generation/Writers/CodeWriterDeclaration.cs new file mode 100644 index 0000000..c2c5a6d --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/CodeWriterDeclaration.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class CodeWriterDeclaration + { + private string? _actualName; + private string? _debuggerName; + + public CodeWriterDeclaration(string name) + { + RequestedName = name; + } + + public string RequestedName { get; } + + public string ActualName => _actualName ?? _debuggerName ?? throw new InvalidOperationException($"Declaration {RequestedName} is not initialized"); + + internal void SetActualName(string? actualName) + { + if (_actualName != null && actualName != null) + { + throw new InvalidOperationException($"Declaration {_actualName} already initialized, can't initialize it with {actualName} name."); + } + + _actualName = actualName; + } + + internal void SetDebuggerName(string? debuggerName) + { + if (_actualName == null) + { + _debuggerName = debuggerName; + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/CodeWriterExtensions.cs b/logger/autorest.csharp/common/Generation/Writers/CodeWriterExtensions.cs new file mode 100644 index 0000000..efaeba1 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/CodeWriterExtensions.cs @@ -0,0 +1,824 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core.Pipeline; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class CodeWriterExtensions + { + public static CodeWriter AppendIf(this CodeWriter writer, FormattableString formattableString, bool condition) + { + if (condition) + { + writer.Append(formattableString); + } + + return writer; + } + + public static CodeWriter AppendRawIf(this CodeWriter writer, string str, bool condition) + { + if (condition) + { + writer.AppendRaw(str); + } + + return writer; + } + + public static CodeWriter LineIf(this CodeWriter writer, FormattableString formattableString, bool condition) + { + if (condition) + { + writer.Line(formattableString); + } + + return writer; + } + + public static CodeWriter LineRawIf(this CodeWriter writer, string str, bool condition) + { + if (condition) + { + writer.LineRaw(str); + } + + return writer; + } + + public static CodeWriter AppendNullableValue(this CodeWriter writer, CSharpType type) + { + if (type.IsNullable && type.IsValueType) + { + writer.Append($".Value"); + } + + return writer; + } + + + public static CodeWriter WriteField(this CodeWriter writer, FieldDeclaration field, bool declareInCurrentScope = true) + { + if (field.Description != null) + { + writer.Line().WriteXmlDocumentationSummary(field.Description); + } + + var modifiers = field.Modifiers; + + if (field.WriteAsProperty) + { + writer + .AppendRaw(modifiers.HasFlag(FieldModifiers.Public) ? "public " : (modifiers.HasFlag(FieldModifiers.Internal) ? "internal " : "private ")); + } + else + { + writer + .AppendRaw(modifiers.HasFlag(FieldModifiers.Public) ? "public " : (modifiers.HasFlag(FieldModifiers.Internal) ? "internal " : "private ")) + .AppendRawIf("const ", modifiers.HasFlag(FieldModifiers.Const)) + .AppendRawIf("static ", modifiers.HasFlag(FieldModifiers.Static)) + .AppendRawIf("readonly ", modifiers.HasFlag(FieldModifiers.ReadOnly)); + } + + if (declareInCurrentScope) + { + writer.Append($"{field.Type} {field.Declaration:D}"); + } + else + { + writer.Append($"{field.Type} {field.Declaration:I}"); + } + + if (field.WriteAsProperty) + { + writer.AppendRaw(modifiers.HasFlag(FieldModifiers.ReadOnly) ? "{ get; }" : "{ get; set; }"); + } + + if (field.InitializationValue != null && + (modifiers.HasFlag(FieldModifiers.Const) || modifiers.HasFlag(FieldModifiers.Static))) + { + field.InitializationValue.Write(writer.AppendRaw(" = ")); + return writer.Line($";"); + } + + return field.WriteAsProperty ? writer.Line() : writer.Line($";"); + } + + public static CodeWriter WriteFieldDeclarations(this CodeWriter writer, IEnumerable fields) + { + foreach (var field in fields) + { + writer.WriteField(field, declareInCurrentScope: false); + } + + return writer.Line(); + } + + public static IDisposable WriteMethodDeclaration(this CodeWriter writer, MethodSignatureBase methodBase, params string[] disabledWarnings) + { + var outerScope = writer.WriteMethodDeclarationNoScope(methodBase, disabledWarnings); + writer.Line(); + var innerScope = writer.Scope(); + return Disposable.Create(() => + { + innerScope.Dispose(); + outerScope.Dispose(); + }); + } + + private static IDisposable WriteMethodDeclarationNoScope(this CodeWriter writer, MethodSignatureBase methodBase, params string[] disabledWarnings) + { + if (methodBase.Attributes is { } attributes) + { + foreach (var attribute in attributes) + { + if (attribute.Arguments.Any()) + { + writer.Append($"[{attribute.Type}("); + foreach (var argument in attribute.Arguments) + { + argument.Write(writer); + } + writer.RemoveTrailingComma(); + writer.LineRaw(")]"); + } + else + { + writer.Line($"[{attribute.Type}]"); + } + } + } + + foreach (var disabledWarning in disabledWarnings) + { + writer.Line($"#pragma warning disable {disabledWarning}"); + } + + writer + .AppendRawIf("public ", methodBase.Modifiers.HasFlag(Public)) + .AppendRawIf("internal ", methodBase.Modifiers.HasFlag(Internal)) + .AppendRawIf("protected ", methodBase.Modifiers.HasFlag(Protected)) + .AppendRawIf("private ", methodBase.Modifiers.HasFlag(Private)) + .AppendRawIf("static ", methodBase.Modifiers.HasFlag(Static)); + + if (methodBase is MethodSignature method) + { + writer + .AppendRawIf("virtual ", methodBase.Modifiers.HasFlag(Virtual)) + .AppendRawIf("override ", methodBase.Modifiers.HasFlag(Override)) + .AppendRawIf("new ", methodBase.Modifiers.HasFlag(New)) + .AppendRawIf("async ", methodBase.Modifiers.HasFlag(Async)); + + if (method.ReturnType != null) + { + writer.Append($"{method.ReturnType} "); + } + else + { + writer.AppendRaw("void "); + } + + if (method.ExplicitInterface is not null) + { + writer.Append($"{method.ExplicitInterface}."); + } + + writer.Append($"{methodBase.Name}"); + + if (method?.GenericArguments != null) + { + writer.AppendRaw("<"); + foreach (var argument in method.GenericArguments) + { + writer.Append($"{argument:D},"); + } + writer.RemoveTrailingComma(); + writer.AppendRaw(">"); + } + } + else + { + writer.Append($"{methodBase.Name}"); + } + + writer + .AppendRaw("(") + .AppendRawIf("this ", methodBase.Modifiers.HasFlag(Extension)); + + var outerScope = writer.AmbientScope(); + + foreach (var parameter in methodBase.Parameters) + { + writer.WriteParameter(parameter); + } + + writer.RemoveTrailingComma(); + writer.Append($")"); + + if (methodBase is MethodSignature { GenericParameterConstraints: { } constraints }) + { + writer.Line(); + foreach (var constraint in constraints) + { + constraint.Write(writer); + writer.AppendRaw(" "); + } + } + + if (methodBase is ConstructorSignature { Initializer: { } } constructor) + { + var (isBase, arguments) = constructor.Initializer; + + if (!isBase || arguments.Any()) + { + writer.AppendRaw(isBase ? ": base(" : ": this("); + foreach (var argument in arguments) + { + argument.Write(writer); + writer.AppendRaw(", "); + } + writer.RemoveTrailingComma(); + writer.AppendRaw(")"); + } + } + + foreach (var disabledWarning in disabledWarnings) + { + writer.Line(); + writer.Append($"#pragma warning restore {disabledWarning}"); + } + + return outerScope; + } + + public static CodeWriter WriteMethodDocumentation(this CodeWriter writer, MethodSignatureBase methodBase) + { + if (methodBase.IsRawSummaryText) + { + return writer.WriteRawXmlDocumentation(methodBase.Description); + } + + if (methodBase.NonDocumentComment is { } comment) + { + writer.Line($"// {comment}"); + } + + if (methodBase.SummaryText is { } summaryText) + { + writer.WriteXmlDocumentationSummary(summaryText); + } + + return writer.WriteMethodDocumentationSignature(methodBase); + } + + public static CodeWriter WriteMethodDocumentation(this CodeWriter writer, MethodSignatureBase methodBase, FormattableString? summaryText = null) + { + return writer + .WriteXmlDocumentationSummary(summaryText ?? methodBase.SummaryText) + .WriteMethodDocumentationSignature(methodBase); + } + + public static CodeWriter WriteMethodDocumentationSignature(this CodeWriter writer, MethodSignatureBase methodBase) + { + writer.WriteXmlDocumentationParameters(methodBase.Modifiers.HasFlag(Public) ? methodBase.Parameters : methodBase.Parameters.Where(p => p.Description is not null)); + + writer.WriteXmlDocumentationRequiredParametersException(methodBase.Parameters); + writer.WriteXmlDocumentationNonEmptyParametersException(methodBase.Parameters); + if (methodBase is MethodSignature { ReturnDescription: { } } method) + { + writer.WriteXmlDocumentationReturns(method.ReturnDescription); + } + + return writer; + } + + public static void WriteParameter(this CodeWriter writer, Parameter clientParameter) + { + if (clientParameter.Attributes.Any()) + { + writer.AppendRaw("["); + foreach (var attribute in clientParameter.Attributes) + { + writer.Append($"{attribute.Type}, "); + } + writer.RemoveTrailingComma(); + writer.AppendRaw("]"); + } + + writer.AppendRawIf("out ", clientParameter.IsOut); + writer.AppendRawIf("ref ", clientParameter.IsRef); + + writer.Append($"{clientParameter.Type} {clientParameter.Name:D}"); + if (clientParameter.DefaultValue != null) + { + var defaultValue = clientParameter.DefaultValue.Value; + if (defaultValue.IsNewInstanceSentinel && defaultValue.Type.IsValueType || clientParameter.IsApiVersionParameter && clientParameter.Initializer != null) + { + writer.Append($" = default"); + } + else + { + writer.Append($" = {clientParameter.DefaultValue.Value.GetConstantFormattable()}"); + } + } + + writer.AppendRaw(","); + } + + public static CodeWriter WriteParametersValidation(this CodeWriter writer, IEnumerable parameters) + { + foreach (Parameter parameter in parameters) + { + writer.WriteParameterValidation(parameter); + } + + writer.Line(); + return writer; + } + + private static CodeWriter WriteParameterValidation(this CodeWriter writer, Parameter parameter) + { + if (parameter.Validation == ValidationType.None && parameter.Initializer != null) + { + return writer.Line($"{parameter.Name:I} ??= {parameter.Initializer};"); + } + + var validationStatement = Snippets.Argument.ValidateParameter(parameter); + + validationStatement.Write(writer); + + return writer; + } + + public static CodeWriter WriteParameterNullChecks(this CodeWriter writer, IReadOnlyCollection parameters) + { + foreach (Parameter parameter in parameters) + { + writer.WriteVariableAssignmentWithNullCheck(parameter.Name, parameter); + } + + writer.Line(); + return writer; + } + + private static Dictionary requestConditionHeaderNames = new Dictionary { + {RequestConditionHeaders.None, "" }, + {RequestConditionHeaders.IfMatch, "If-Match" }, + {RequestConditionHeaders.IfNoneMatch, "If-None-Match" }, + {RequestConditionHeaders.IfModifiedSince, "If-Modified-Since" }, + {RequestConditionHeaders.IfUnmodifiedSince, "If-Unmodified-Since" } + }; + + private static Dictionary requestConditionFieldNames = new Dictionary { + {RequestConditionHeaders.None, "" }, + {RequestConditionHeaders.IfMatch, "IfMatch" }, + {RequestConditionHeaders.IfNoneMatch, "IfNoneMatch" }, + {RequestConditionHeaders.IfModifiedSince, "IfModifiedSince" }, + {RequestConditionHeaders.IfUnmodifiedSince, "IfUnmodifiedSince" } + }; + + public static CodeWriter WriteRequestConditionParameterChecks(this CodeWriter writer, IReadOnlyCollection parameters, RequestConditionHeaders requestConditionFlag) + { + foreach (Parameter parameter in parameters) + { + if (parameter.Type.Equals(typeof(RequestConditions))) + { + string nullableFlag = (parameter.Type.IsNullable) ? "?" : ""; + foreach (RequestConditionHeaders val in Enum.GetValues(typeof(RequestConditionHeaders)).Cast()) + { + if (val != RequestConditionHeaders.None && !requestConditionFlag.HasFlag(val)) + { + using (writer.Scope($"if ({parameter.Name:I}{nullableFlag}.{requestConditionFieldNames[val]} is not null)")) + { + writer.Line($"throw new {typeof(ArgumentNullException)}(nameof({parameter.Name:I}), \"Service does not support the {requestConditionHeaderNames[val]} header for this operation.\");"); + } + } + } + } + } + return writer; + } + + public static CodeWriter.CodeWriterScope WriteUsingStatement(this CodeWriter writer, string variableName, bool asyncCall, FormattableString asyncMethodName, FormattableString syncMethodName, FormattableString parameters, out CodeWriterDeclaration variable) + { + variable = new CodeWriterDeclaration(variableName); + return writer.Scope($"using (var {variable:D} = {GetMethodCallFormattableString(asyncCall, asyncMethodName, syncMethodName, parameters)})"); + } + + public static CodeWriter WriteMethodCall(this CodeWriter writer, bool asyncCall, FormattableString methodName, FormattableString parameters) + => writer.WriteMethodCall(asyncCall, methodName, methodName, parameters, false); + + public static CodeWriter WriteMethodCall(this CodeWriter writer, bool asyncCall, FormattableString asyncMethodName, FormattableString syncMethodName, FormattableString parameters, bool writeLine = true) + => writer.Append(GetMethodCallFormattableString(asyncCall, asyncMethodName, syncMethodName, parameters)).LineRawIf(";", writeLine); + + public static CodeWriter WriteMethodCall(this CodeWriter writer, MethodSignature method, IEnumerable parameters, bool asyncCall) + { + var parametersFs = parameters.ToArray().Join(", "); + if (asyncCall) + { + return writer.Append($"await {method.WithAsync(true).Name}({parametersFs}).ConfigureAwait(false)"); + } + + return writer.Append($"{method.WithAsync(false).Name}({parametersFs})"); + } + + private static FormattableString GetMethodCallFormattableString(bool asyncCall, FormattableString asyncMethodName, FormattableString syncMethodName, FormattableString parameters) + => asyncCall ? (FormattableString)$"await {asyncMethodName}({parameters}).ConfigureAwait(false)" : $"{syncMethodName}({parameters})"; + + public static void WriteVariableAssignmentWithNullCheck(this CodeWriter writer, string variableName, Parameter parameter) + { + // Temporary check to minimize amount of changes in existing generated code + var assignToSelf = parameter.Name == variableName; + if (parameter.Initializer != null) + { + if (assignToSelf) + { + writer.Line($"{variableName:I} ??= {parameter.Initializer};"); + } + else + { + writer.Line($"{variableName:I} = {parameter.Name:I} ?? {parameter.Initializer};"); + } + } + else if (parameter.Validation != ValidationType.None) + { + // Temporary check to minimize amount of changes in existing generated code + if (assignToSelf) + { + using (writer.Scope($"if ({parameter.Name:I} == null)")) + { + writer.Line($"throw new {typeof(ArgumentNullException)}(nameof({parameter.Name:I}));"); + } + } + else + { + writer.Line($"{variableName:I} = {parameter.Name:I} ?? throw new {typeof(ArgumentNullException)}(nameof({parameter.Name:I}));"); + } + } + else if (!assignToSelf) + { + writer.Line($"{variableName:I} = {parameter.Name:I};"); + } + } + + public static CodeWriter WriteConstant(this CodeWriter writer, Constant constant) => writer.Append(constant.GetConstantFormattable()); + + public static void WriteDeserializationForMethods(this CodeWriter writer, ObjectSerialization serialization, bool async, ValueExpression? variable, FormattableString streamFormattable, CSharpType? type) + { + var streamExpression = new StreamExpression(new FormattableStringToExpression(streamFormattable)); + switch (serialization) + { + case JsonSerialization jsonSerialization: + JsonSerializationMethodsBuilder.BuildDeserializationForMethods(jsonSerialization, async, variable, streamExpression, type is not null && type.Equals(typeof(BinaryData)), null).Write(writer); + break; + case XmlElementSerialization xmlSerialization: + XmlSerializationMethodsBuilder.BuildDeserializationForMethods(xmlSerialization, variable, streamExpression).Write(writer); + break; + default: + throw new NotImplementedException(serialization.ToString()); + } + } + + public static CodeWriter AppendEnumToString(this CodeWriter writer, EnumType enumType) + { + new EnumExpression(enumType, new ValueExpression()).ToSerial().Write(writer); + return writer; + } + + public static CodeWriter AppendEnumFromString(this CodeWriter writer, EnumType enumType, FormattableString value) + { + if (enumType.IsExtensible) + { + writer.Append($"new {enumType.Type}({value})"); + } + else + { + writer.UseNamespace(enumType.Type.Namespace); + writer.Append($"{value}.To{enumType.Declaration.Name}()"); + } + + return writer; + } + + public static CodeWriter WriteReferenceOrConstant(this CodeWriter writer, ReferenceOrConstant value) + => writer.Append(value.GetReferenceOrConstantFormattable()); + + public static CodeWriter WriteInitialization( + this CodeWriter writer, + Action valueCallback, + TypeProvider objectType, + ObjectTypeConstructor constructor, + IEnumerable initializers) + { + var initializersSet = initializers.ToHashSet(); + + // Find longest satisfiable ctor + List selectedCtorInitializers = constructor.Signature.Parameters + .Select(constructor.FindPropertyInitializedByParameter) + .Select(property => initializersSet.SingleOrDefault(i => i.Name == property?.Declaration.Name && Equals(i.Type, property.Declaration.Type))) + .ToList(); + + // Checks if constructor parameters can be satisfied by the provided initializer list + Debug.Assert(!selectedCtorInitializers.Contains(default)); + + // Find properties that would have to be initialized using a foreach loop + var collectionInitializers = initializersSet + .Except(selectedCtorInitializers) + .Where(i => i is { IsReadOnly: true, Type.IsCollection: true }) + .ToArray(); + + // Find properties that would have to be initialized via property initializers + var restOfInitializers = initializersSet + .Except(selectedCtorInitializers) + .Except(collectionInitializers) + .ToArray(); + + var constructorParameters = selectedCtorInitializers + .Select(pi => $"{pi.Value}{GetConversion(writer, pi.ValueType!, pi.Type)}") + .ToArray() + .Join(", "); + + var propertyInitializers = restOfInitializers + .Select(pi => $"{pi.Name} = {pi.Value}{GetConversion(writer, pi.ValueType!, pi.Type)}") + .ToArray() + .Join(",\n "); + + var objectInitializerFormattable = restOfInitializers.Any() + ? $"new {objectType.Type}({constructorParameters}) {{\n{propertyInitializers}\n}}" + : (FormattableString)$"new {objectType.Type}({constructorParameters})"; + + if (collectionInitializers.Any()) + { + var modelVariable = new CodeWriterDeclaration(objectType.Declaration.Name.ToVariableName()); + writer.Line($"{objectType.Type} {modelVariable:D} = {objectInitializerFormattable};"); + + // Writes the: + // foreach (var value in param) + // { + // model.CollectionProperty = value; + // } + foreach (var propertyInitializer in collectionInitializers) + { + var valueVariable = new CodeWriterDeclaration("value"); + using (writer.Scope($"if ({propertyInitializer.Value} != null)")) + { + using (writer.Scope($"foreach (var {valueVariable:D} in {propertyInitializer.Value})")) + { + writer.Append($"{modelVariable:I}.{propertyInitializer.Name}.Add({valueVariable});"); + } + } + } + + valueCallback($"{modelVariable:I}"); + } + else + { + valueCallback(objectInitializerFormattable); + } + + + return writer; + } + + public static CodeWriter WriteConversion(this CodeWriter writer, CSharpType from, CSharpType to) + { + if (CSharpType.RequiresToList(from, to)) + { + writer.UseNamespace(typeof(Enumerable).Namespace!); + return writer.AppendRaw(from.IsNullable ? "?.ToList()" : ".ToList()"); + } + + return writer; + } + + internal static string GetConversion(CodeWriter writer, CSharpType from, CSharpType to) + { + if (CSharpType.RequiresToList(from, to)) + { + writer.UseNamespace(typeof(Enumerable).Namespace!); + return from.IsNullable ? "?.ToList()" : ".ToList()"; + } + + return string.Empty; + } + + public static IDisposable WriteCommonMethodWithoutValidation(this CodeWriter writer, MethodSignature signature, FormattableString? returnDescription, bool isAsync, bool isPublicType) + { + writer.WriteXmlDocumentationSummary(signature.Description); + writer.WriteXmlDocumentationParameters(signature.Parameters); + if (isPublicType) + { + writer.WriteXmlDocumentationNonEmptyParametersException(signature.Parameters); + writer.WriteXmlDocumentationRequiredParametersException(signature.Parameters); + } + + FormattableString? returnDesc = returnDescription ?? signature.ReturnDescription; + if (returnDesc is not null) + writer.WriteXmlDocumentationReturns(returnDesc); + + return writer.WriteMethodDeclaration(signature.WithAsync(isAsync)); + } + + public static IDisposable WriteCommonMethod(this CodeWriter writer, MethodSignature signature, FormattableString? returnDescription, bool isAsync, bool isPublicType, bool skipValidation = false) + { + var scope = WriteCommonMethodWithoutValidation(writer, signature, returnDescription, isAsync, isPublicType); + if (isPublicType && !skipValidation) + writer.WriteParametersValidation(signature.Parameters); + + return scope; + } + + public static CodeWriter WriteEnableHttpRedirectIfNecessary(this CodeWriter writer, RestClientMethod restClientMethod, TypedValueExpression messageVariable) + { + if (restClientMethod.ShouldEnableRedirect) + { + new InvokeStaticMethodStatement(typeof(RedirectPolicy), nameof(RedirectPolicy.SetAllowAutoRedirect), messageVariable, Snippets.True).Write(writer); + } + return writer; + } + + public static void WriteMethod(this CodeWriter writer, Method method) + { + if (method.Body is { } body) + { + using (writer.WriteMethodDeclaration(method.Signature)) + { + body.Write(writer); + } + } + else if (method.BodyExpression is { } expression) + { + using (writer.WriteMethodDeclarationNoScope(method.Signature)) + { + writer.AppendRaw(" => "); + expression.Write(writer); + writer.LineRaw(";"); + } + } + + writer.Line(); + } + + public static void WriteProperty(this CodeWriter writer, PropertyDeclaration property) + { + if (property.Description is not null) + { + writer.Line().WriteXmlDocumentationSummary(property.Description); + } + + if (property.Exceptions is not null) + { + foreach (var (exceptionType, description) in property.Exceptions) + { + writer.WriteXmlDocumentationException(exceptionType, description); + } + } + + var modifiers = property.Modifiers; + writer.AppendRawIf("public ", modifiers.HasFlag(MethodSignatureModifiers.Public)) + .AppendRawIf("protected ", modifiers.HasFlag(MethodSignatureModifiers.Protected)) + .AppendRawIf("internal ", modifiers.HasFlag(MethodSignatureModifiers.Internal)) + .AppendRawIf("private ", modifiers.HasFlag(MethodSignatureModifiers.Private)) + .AppendRawIf("override ", modifiers.HasFlag(MethodSignatureModifiers.Override)) + .AppendRawIf("static ", modifiers.HasFlag(MethodSignatureModifiers.Static)) + .AppendRawIf("virtual ", modifiers.HasFlag(MethodSignatureModifiers.Virtual)); // property does not support other modifiers, here we just ignore them if any + + writer.Append($"{property.PropertyType} "); + if (property is IndexerDeclaration indexer) + { + writer.Append($"this[{indexer.IndexerParameter.Type} {indexer.IndexerParameter.Name}]"); + } + else + { + if (property.ExplicitInterface is not null) + { + writer.Append($"{property.ExplicitInterface}."); + } + writer.Append($"{property.Declaration:I}"); // the declaration order here is quite anonying - we might need to assign the values to those properties in other places before these are written + } + + switch (property.PropertyBody) + { + case ExpressionPropertyBody(var expression): + expression.Write(writer.AppendRaw(" => ")); + writer.AppendRaw(";"); + break; + case AutoPropertyBody(var hasSetter, var setterModifiers, var initialization): + writer.AppendRaw("{ get; "); + if (hasSetter) + { + WritePropertyAccessorModifiers(writer, setterModifiers); + writer.AppendRaw(" set; "); + } + writer.AppendRaw("}"); + if (initialization is not null) + { + initialization.Write(writer.AppendRaw(" = ")); + } + break; + case MethodPropertyBody(var getter, var setter, var setterModifiers): + writer.LineRaw("{"); + // write getter + WriteMethodPropertyAccessor(writer, "get", getter); + // write setter + if (setter is not null) + { + WriteMethodPropertyAccessor(writer, "set", setter, setterModifiers); + } + writer.AppendRaw("}"); + break; + default: + throw new InvalidOperationException($"Unhandled property body type {property.PropertyBody}"); + } + + writer.Line(); + + static void WriteMethodPropertyAccessor(CodeWriter writer, string name, MethodBodyStatement body, MethodSignatureModifiers modifiers = MethodSignatureModifiers.None) + { + WritePropertyAccessorModifiers(writer, modifiers); + writer.LineRaw(name) + .LineRaw("{"); + using (writer.AmbientScope()) + { + body.Write(writer); + } + writer.LineRaw("}"); + } + + static void WritePropertyAccessorModifiers(CodeWriter writer, MethodSignatureModifiers modifiers) + { + writer.AppendRawIf("protected ", modifiers.HasFlag(MethodSignatureModifiers.Protected)) + .AppendRawIf("internal ", modifiers.HasFlag(MethodSignatureModifiers.Internal)) + .AppendRawIf("private ", modifiers.HasFlag(MethodSignatureModifiers.Private)); + } + } + + public static void WriteTypeArguments(this CodeWriter writer, IEnumerable? typeArguments) + { + if (typeArguments is null) + { + return; + } + + writer.AppendRaw("<"); + foreach (var argument in typeArguments) + { + writer.Append($"{argument}, "); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw(">"); + } + + public static void WriteArguments(this CodeWriter writer, IEnumerable arguments, bool useSingleLine = true) + { + if (useSingleLine) + { + writer.AppendRaw("("); + foreach (var argument in arguments) + { + argument.Write(writer); + writer.AppendRaw(", "); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw(")"); + } + else + { + writer.LineRaw("("); + foreach (var argument in arguments) + { + writer.AppendRaw("\t"); + argument.Write(writer); + writer.LineRaw(","); + } + + writer.RemoveTrailingCharacter(); + writer.RemoveTrailingComma(); + writer.AppendRaw(")"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/CodeWriterScopeDeclarations.cs b/logger/autorest.csharp/common/Generation/Writers/CodeWriterScopeDeclarations.cs new file mode 100644 index 0000000..60f16c3 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/CodeWriterScopeDeclarations.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class CodeWriterScopeDeclarations + { + public IReadOnlyList Names { get; } + + public CodeWriterScopeDeclarations(IEnumerable declarations) + { + var names = new List(); + foreach (var declaration in declarations) + { + declaration.SetActualName(declaration.RequestedName); + names.Add(declaration.ActualName); + } + + Names = names; + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/DebuggerCodeWriter.cs b/logger/autorest.csharp/common/Generation/Writers/DebuggerCodeWriter.cs new file mode 100644 index 0000000..16a7d5b --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/DebuggerCodeWriter.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal sealed class DebuggerCodeWriter : CodeWriter, IDisposable + { + private readonly List _declarations; + + public DebuggerCodeWriter() + { + _declarations = new List(); + } + + public override CodeWriter Declaration(CodeWriterDeclaration declaration) + { + declaration.SetDebuggerName(GetTemporaryVariable(declaration.RequestedName)); + _declarations.Add(declaration); + return Declaration(declaration.ActualName); + } + + public void Dispose() + { + foreach (var declaration in _declarations) + { + declaration.SetDebuggerName(null); + } + } + + public override void Append(CodeWriterDeclaration declaration) + { + try + { + Identifier(declaration.ActualName); + } + catch (InvalidOperationException) + { + Identifier(declaration.RequestedName); + } + } + + public override string ToString() => ToString(false); + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/DiagnosticScopeWriter.cs b/logger/autorest.csharp/common/Generation/Writers/DiagnosticScopeWriter.cs new file mode 100644 index 0000000..fa60a2c --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/DiagnosticScopeWriter.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Output.Models.Requests; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class DiagnosticScopeWriter + { + public static IDisposable WriteDiagnosticScope(this CodeWriter writer, Diagnostic diagnostic, Reference clientDiagnostics) + { + var scopeVariable = new CodeWriterDeclaration("scope"); + writer.Line($"using var {scopeVariable:D} = {clientDiagnostics.GetReferenceFormattable()}.{nameof(ClientDiagnostics.CreateScope)}({diagnostic.ScopeName:L});"); + foreach (DiagnosticAttribute diagnosticScopeAttributes in diagnostic.Attributes) + { + writer.Append($"{scopeVariable}.AddAttribute({diagnosticScopeAttributes.Name:L},"); + writer.WriteReferenceOrConstant(diagnosticScopeAttributes.Value); + writer.Line($");"); + } + + writer.Line($"{scopeVariable}.Start();"); + return new DiagnosticScope(writer.Scope($"try"), scopeVariable, writer); + } + + private class DiagnosticScope : IDisposable + { + private readonly CodeWriter.CodeWriterScope _scope; + private readonly CodeWriterDeclaration _scopeVariable; + private readonly CodeWriter _writer; + + public DiagnosticScope(CodeWriter.CodeWriterScope scope, CodeWriterDeclaration scopeVariable, CodeWriter writer) + { + _scope = scope; + _scopeVariable = scopeVariable; + _writer = writer; + } + + public void Dispose() + { + _scope.Dispose(); + using (_writer.Scope($"catch ({typeof(Exception)} e)")) + { + _writer.Line($"{_scopeVariable}.Failed(e);"); + _writer.Line($"throw;"); + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/DocumentationWriterExtensions.cs b/logger/autorest.csharp/common/Generation/Writers/DocumentationWriterExtensions.cs new file mode 100644 index 0000000..e2c53a6 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/DocumentationWriterExtensions.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Output.Models.Shared.ValidationType; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class DocumentationWriterExtensions + { + public static CodeWriter WriteXmlDocumentationSummary(this CodeWriter writer, FormattableString? text) + { + return writer.WriteXmlDocumentation("summary", text); + } + + public static CodeWriter WriteXmlDocumentation(this CodeWriter writer, string tag, FormattableString? text) + { + return writer.WriteDocumentationLines($"<{tag}>", $"", text); + } + + public static CodeWriter WriteXmlDocumentationParameters(this CodeWriter writer, IEnumerable parameters) + { + foreach (var parameter in parameters) + { + writer.WriteXmlDocumentationParameter(parameter); + } + + return writer; + } + + public static CodeWriter WriteXmlDocumentationParameter(this CodeWriter writer, string name, FormattableString? text) + { + return writer.WriteDocumentationLines($"", $"", text); + } + + /// + /// Writes XML documentation for a parameter of a method using a "param" tag. + /// + /// Writer to which code is written to. + /// The definition of the parameter, including name and description. + /// + public static CodeWriter WriteXmlDocumentationParameter(this CodeWriter writer, Parameter parameter) + { + return writer.WriteXmlDocumentationParameter(parameter.Name, parameter.Description); + } + + public static CodeWriter WriteXmlDocumentationException(this CodeWriter writer, CSharpType exception, FormattableString? description) + { + return writer.WriteDocumentationLines($"", $"", description); + } + + public static CodeWriter WriteXmlDocumentationReturns(this CodeWriter writer, FormattableString text) + { + return writer.WriteDocumentationLines($"", $"", text); + } + + public static CodeWriter WriteXmlDocumentationInclude(this CodeWriter writer, string filename, MethodSignature methodSignature, out string memberId) + { + // We use short names of types for external doc reference member id + // This is not good enough for cref, but for now works as member id + // Change to cref-style names if needed + var sb = new StringBuilder(); + sb.Append(methodSignature.Name).Append("("); + foreach (var parameter in methodSignature.Parameters) + { + AppendTypeWithShortNames(parameter.Type, sb); + sb.Append(","); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(")"); + + memberId = sb.ToString(); + return writer.LineRaw($"/// "); + } + + private static void AppendTypeWithShortNames(CSharpType type, StringBuilder sb) + { + sb.Append(type.TryGetCSharpFriendlyName(out var keywordName) ? keywordName : type.Name); + + if (type.Arguments.Any()) + { + sb.Append("{"); + foreach (var typeArgument in type.Arguments) + { + AppendTypeWithShortNames(typeArgument, sb); + sb.Append(","); + } + sb.Remove(sb.Length - 1, 1); + sb.Append("}"); + } + + if (type is { IsNullable: true, IsValueType: true }) + { + sb.Append("?"); + } + } + + public static CodeWriter WriteXmlDocumentationRequiredParametersException(this CodeWriter writer, IEnumerable parameters) + { + return writer.WriteXmlDocumentationParametersExceptions(typeof(ArgumentNullException), parameters.Where(p => p.Validation is AssertNotNull or AssertNotNullOrEmpty).ToArray(), " is null."); + } + + public static CodeWriter WriteXmlDocumentationNonEmptyParametersException(this CodeWriter writer, IEnumerable parameters) + { + return writer.WriteXmlDocumentationParametersExceptions(typeof(ArgumentException), parameters.Where(p => p.Validation == AssertNotNullOrEmpty).ToArray(), " is an empty string, and was expected to be non-empty."); + } + + private static CodeWriter WriteXmlDocumentationParametersExceptions(this CodeWriter writer, Type exceptionType, IReadOnlyCollection parameters, string reason) + { + if (parameters.Count == 0) + { + return writer; + } + + var formatBuilder = new StringBuilder(); + for (var i = 0; i < parameters.Count - 2; ++i) + { + formatBuilder.Append(", "); + } + + if (parameters.Count > 1) + { + formatBuilder.Append(" or "); + } + + formatBuilder.Append(""); + formatBuilder.Append(reason); + + var description = FormattableStringFactory.Create(formatBuilder.ToString(), parameters.Select(p => (object)p.Name).ToArray()); + return writer.WriteXmlDocumentationException(exceptionType, description); + } + + public static CodeWriter WriteDocumentationLines(this CodeWriter writer, FormattableString startTag, FormattableString endTag, FormattableString? text) + => writer.AppendXmlDocumentation(startTag, endTag, text ?? $""); + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/ExpressionTypeProviderWriter.cs b/logger/autorest.csharp/common/Generation/Writers/ExpressionTypeProviderWriter.cs new file mode 100644 index 0000000..bf967fc --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/ExpressionTypeProviderWriter.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class ExpressionTypeProviderWriter + { + private readonly ExpressionTypeProvider _provider; + private readonly CodeWriter _writer; + + public ExpressionTypeProviderWriter(CodeWriter writer, ExpressionTypeProvider provider) + { + _provider = provider; + _writer = writer; + } + + public virtual void Write() + { + foreach (var @using in _provider.Usings) + { + _writer.UseNamespace(@using); + } + + if (_provider.DeclaringTypeProvider == null) + { + using (_writer.Namespace(_provider.Declaration.Namespace)) + { + WriteType(); + } + } + else + { + WriteType(); + } + } + + private void WriteType() + { + if (_provider.IsEnum) + { + WriteEnum(); + } + else + { + WriteClassOrStruct(); + } + } + + private void WriteClassOrStruct() + { + _writer.WriteTypeModifiers(_provider.DeclarationModifiers); + if (_provider.IsStruct) + { + _writer.AppendRaw(" struct "); + } + else + { + _writer.AppendRaw(" class "); + } + _writer.Append($"{_provider.Type:D}") + .AppendRawIf(" : ", _provider.Inherits != null || _provider.Implements.Any()) + .AppendIf($"{_provider.Inherits},", _provider.Inherits != null); + + foreach (var implement in _provider.Implements) + { + _writer.Append($"{implement:D},"); + } + _writer.RemoveTrailingComma(); + + if (_provider.WhereClause is not null) + { + _provider.WhereClause.Write(_writer); + } + + using (_writer.Scope()) + { + WriteFields(); + + WriteConstructors(); + + WriteProperties(); + + WriteMethods(); + + WriteNestedTypes(); + } + } + + private void WriteEnum() + { + _writer.WriteTypeModifiers(_provider.DeclarationModifiers); + _writer.Append($" enum {_provider.Type:D}") + .AppendRawIf(" : ", _provider.Inherits != null) + .AppendIf($"{_provider.Inherits}", _provider.Inherits != null); + + using (_writer.Scope()) + { + foreach (var field in _provider.Fields) + { + _writer.Append($"{field.Declaration:D}"); + if (field.InitializationValue != null) + { + _writer.AppendRaw(" = "); + field.InitializationValue.Write(_writer); + } + _writer.LineRaw(","); + } + _writer.RemoveTrailingComma(); + } + } + + protected virtual void WriteProperties() + { + foreach (var property in _provider.Properties) + { + _writer.WriteProperty(property); + _writer.Line(); + } + } + + protected virtual void WriteFields() + { + foreach (var field in _provider.Fields) + { + _writer.WriteField(field, declareInCurrentScope: true); + } + _writer.Line(); + } + + protected virtual void WriteConstructors() + { + foreach (var ctor in _provider.Constructors) + { + _writer.WriteMethod(ctor); + } + } + + protected virtual void WriteMethods() + { + foreach (var method in _provider.Methods) + { + _writer.WriteMethod(method); + } + + _writer.Line(); + } + + protected virtual void WriteNestedTypes() + { + foreach (var nested in _provider.NestedTypes) + { + var nestedWriter = new ExpressionTypeProviderWriter(_writer, nested); + nestedWriter.Write(); + _writer.Line(); + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/FormattableStringHelpers.cs b/logger/autorest.csharp/common/Generation/Writers/FormattableStringHelpers.cs new file mode 100644 index 0000000..f977e15 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/FormattableStringHelpers.cs @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using static System.Net.Mime.MediaTypeNames; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class FormattableStringHelpers + { + public static FormattableString Empty => $""; + + [return: NotNullIfNotNull(nameof(s))] + public static FormattableString? FromString(string? s) => + s is null ? null : s.Length == 0 ? Empty : $"{s}"; + + public static bool IsNullOrEmpty(this FormattableString? fs) => + fs is null || string.IsNullOrEmpty(fs.Format) && fs.ArgumentCount == 0; + + public static bool IsEmpty(this FormattableString fs) => + string.IsNullOrEmpty(fs.Format) && fs.ArgumentCount == 0; + + public static FormattableString Join(this ICollection fss, string separator, string? lastSeparator = null) + => fss.Count == 1 ? fss.First() : Join(fss, fss.Count, static fs => fs, separator, lastSeparator, null); + + public static FormattableString GetClientTypesFormattable(this IReadOnlyList clients) + => Join(clients, clients.Count, static c => c.Type, ", ", null, 'C'); + + public static FormattableString GetLiteralsFormattable(this IReadOnlyCollection parameters) + => GetLiteralsFormattable(parameters.Select(p => p.Name), parameters.Count); + + public static FormattableString GetLiteralsFormattable(this IReadOnlyCollection references) + => GetLiteralsFormattable(references.Select(p => p.Name), references.Count); + + public static FormattableString GetLiteralsFormattable(this IReadOnlyCollection literals) + => GetLiteralsFormattable(literals, literals.Count); + + public static FormattableString GetLiteralsFormattable(this ICollection literals) + => GetLiteralsFormattable(literals, literals.Count); + + public static FormattableString GetLiteralsFormattable(this IEnumerable literals, int count) + => Join(literals, count, static l => l, ", ", null, 'L'); + + public static FormattableString GetTypesFormattable(this IReadOnlyCollection parameters) + => GetTypesFormattable(parameters, parameters.Count); + + public static FormattableString GetTypesFormattable(this IEnumerable parameters, int count) + => Join(parameters, count, static p => p.Type, ",", null, null); + + public static FormattableString GetIdentifiersFormattable(this IReadOnlyCollection parameters) + => GetIdentifiersFormattable(parameters.Select(p => p.Name), parameters.Count); + + public static FormattableString GetIdentifiersFormattable(this IReadOnlyCollection references) + => GetIdentifiersFormattable(references.Select(p => p.Name), references.Count); + + public static FormattableString GetIdentifiersFormattable(this IReadOnlyCollection identifiers) + => GetIdentifiersFormattable(identifiers, identifiers.Count); + + public static FormattableString GetIdentifiersFormattable(this IEnumerable identifiers, int count) + => Join(identifiers, count, static i => i, ", ", null, 'I'); + + public static FormattableString? GetParameterInitializer(this CSharpType parameterType, Constant? defaultValue) + { + if (parameterType.IsValueType) + { + return null; + } + + if (parameterType.IsCollection && (defaultValue == null || defaultValue.Value.Type.IsCollection)) + { + defaultValue = Constant.NewInstanceOf(parameterType.InitializationType.WithNullable(false)); + } + + if (defaultValue == null) + { + return null; + } + + var constantFormattable = GetConstantFormattable(defaultValue.Value); + var conversion = GetConversionMethod(defaultValue.Value.Type, parameterType); + return conversion == null ? constantFormattable : $"{constantFormattable}{conversion}"; + } + + public static FormattableString GetConversionFormattable(this Parameter parameter, CSharpType toType, string? contentType) + { + if (toType.EqualsIgnoreNullable(Configuration.ApiTypes.RequestContentType)) + { + switch (parameter.Type) + { + case { IsFrameworkType: true }: + return parameter.GetConversionFromFrameworkToRequestContent(contentType); + case { IsFrameworkType: false, Implementation: EnumType enumType }: + if (enumType.IsExtensible) + { + return $"{typeof(BinaryData)}.{nameof(BinaryData.FromObjectAsJson)}({parameter.Name}.{enumType.SerializationMethodName}())"; + } + else + { + return $"{typeof(BinaryData)}.{nameof(BinaryData.FromObjectAsJson)}({(enumType.IsIntValueType ? $"({enumType.ValueType}){parameter.Name}" : $"{parameter.Name}.{enumType.SerializationMethodName}()")})"; + } + case { IsFrameworkType: false, Implementation: ModelTypeProvider }: + { + BodyMediaType? mediaType = contentType == null ? null : ToMediaType(contentType); + if (mediaType == BodyMediaType.Multipart) + { + return $"{parameter.Name:I}.{Configuration.ApiTypes.ToMultipartRequestContentName}()"; + } + break; + } + } + } + + var conversionMethod = GetConversionMethod(parameter.Type, toType); + if (conversionMethod == null) + { + return $"{parameter.Name:I}"; + } + + if (parameter.IsOptionalInSignature) + { + return $"{parameter.Name:I}?{conversionMethod}"; + } + + return $"{parameter.Name:I}{conversionMethod}"; + } + + private static FormattableString GetConversionFromFrameworkToRequestContent(this Parameter parameter, string? contentType) + { + if (parameter.Type.IsReadWriteDictionary) + { + var dictionary = (ValueExpression)parameter; + var expression = RequestContentHelperProvider.Instance.FromDictionary(dictionary); + if (parameter.IsOptionalInSignature) + { + expression = new TernaryConditionalOperator(NotEqual(dictionary, Null), expression, Null); + } + return $"{expression}"; + } + + if (parameter.Type.IsList) + { + var enumerable = (ValueExpression)parameter; + var expression = RequestContentHelperProvider.Instance.FromEnumerable(enumerable); + if (parameter.IsOptionalInSignature) + { + expression = new TernaryConditionalOperator(NotEqual(enumerable, Null), expression, Null); + } + return $"{expression}"; + } + + if (parameter.Type.IsFrameworkType == true && parameter.Type.FrameworkType == typeof(AzureLocation)) + { + return $"{RequestContentHelperProvider.Instance.FromObject(((ValueExpression)parameter).InvokeToString())}"; + } + + BodyMediaType? mediaType = contentType == null ? null : ToMediaType(contentType); + if (parameter.RequestLocation == RequestLocation.Body && mediaType == BodyMediaType.Binary) + { + return $"{parameter.Name:I}"; + } + // TODO: Here we only consider the case when body is string type. We will add support for other types. + if (parameter.RequestLocation == RequestLocation.Body && mediaType == BodyMediaType.Text && parameter.Type.FrameworkType == typeof(string)) + { + return $"{parameter.Name:I}"; + } + + return $"{RequestContentHelperProvider.Instance.FromObject(parameter)}"; + } + + // TODO: This is a temporary solution. We will move this part to some common place. + // This logic is a twist from https://github.com/Azure/autorest/blob/faf5c1168232ba8a1e8fe02fbc28667c00db8c96/packages/libs/codegen/src/media-types.ts#L53 + public static BodyMediaType ToMediaType(string contentType) + { + const string pattern = @"(application|audio|font|example|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))\/([0-9A-Za-z!#$%&'*.^_`|~-]+)\s*(?:\+([0-9A-Za-z!#$%&'*.^_`|~-]+))?\s*(?:;.\s*(\S*))?"; + + var matches = Regex.Matches(contentType, pattern); + if (matches.Count == 0) + { + throw new NotSupportedException($"Content type {contentType} is not supported."); + } + + var type = matches[0].Groups[1].Value; + var subType = matches[0].Groups[2].Value; + var suffix = matches[0].Groups[3].Value; + var parameter = matches[0].Groups[4].Value; + + var typeSubs = contentType.Split('/'); + if (typeSubs.Length != 2) + { + throw new NotSupportedException($"Content type {contentType} is not supported."); + } + + if ((subType == "json" || suffix == "json") && (type == "application" || type == "text") && suffix == "" && parameter == "") + { + return BodyMediaType.Json; + } + + if ((subType == "xml" || suffix == "xml") && (type == "application" || type == "text")) + { + return BodyMediaType.Xml; + } + + if (type == "audio" || type == "image" || type == "video" || subType == "octet-stream" || parameter == "serialization=Avro") + { + return BodyMediaType.Binary; + } + + if (type == "application" && subType == "formEncoded") + { + return BodyMediaType.Form; + } + + if (type == "multipart" && subType == "form-data") + { + return BodyMediaType.Multipart; + } + + if (type == "application") + { + return BodyMediaType.Binary; + } + + if (type == "text") + { + return BodyMediaType.Text; + } + + throw new NotSupportedException($"Content type {contentType} is not supported."); + } + + public static string? GetConversionMethod(CSharpType fromType, CSharpType toType) + => fromType switch + { + { IsFrameworkType: false, Implementation: EnumType { IsExtensible: true } } when toType.EqualsIgnoreNullable(typeof(string)) => ".ToString()", + { IsFrameworkType: false, Implementation: EnumType { IsExtensible: false } } when toType.EqualsIgnoreNullable(typeof(string)) => ".ToSerialString()", + { IsFrameworkType: false, Implementation: EnumType } when toType.EqualsIgnoreNullable(typeof(int)) => ".ToSerialInt32()", + { IsFrameworkType: false, Implementation: EnumType } when toType.EqualsIgnoreNullable(typeof(float)) => ".ToSerialSingle()", + { IsFrameworkType: false, Implementation: ModelTypeProvider } when toType.EqualsIgnoreNullable(Configuration.ApiTypes.RequestContentType) => $".{Configuration.ApiTypes.ToRequestContentName}()", + _ => null + }; + + public static FormattableString GetReferenceOrConstantFormattable(this ReferenceOrConstant value) + => value.IsConstant ? value.Constant.GetConstantFormattable() : value.Reference.GetReferenceFormattable(); + + public static FormattableString GetReferenceFormattable(this Reference reference) + { + var parts = reference.Name.Split(".").ToArray(); + return Join(parts, parts.Length, static s => s, ".", null, 'I'); + } + + public static FormattableString GetConstantFormattable(this Constant constant, bool writeAsString = false) + { + if (constant.Value == null) + { + // Cast helps the overload resolution + return $"({constant.Type}){null:L}"; + } + + if (constant.IsNewInstanceSentinel) + { + return $"new {constant.Type}()"; + } + + if (constant.Value is Constant.Expression expression) + { + return expression.ExpressionValue; + } + + if (constant is { Type: { IsFrameworkType: false }, Value: EnumTypeValue enumTypeValue }) + { + return $"{constant.Type}.{enumTypeValue.Declaration.Name}"; + } + + // we cannot check `constant.Value is string` because it is always string - this is an issue in yaml serialization) + if (constant.Type is { IsFrameworkType: false, Implementation: EnumType enumType }) + { + if (enumType.IsStringValueType) + return $"new {constant.Type}({constant.Value:L})"; + else + return $"new {constant.Type}(({enumType.ValueType}){constant.Value})"; + } + + Type frameworkType = constant.Type.FrameworkType; + if (frameworkType == typeof(DateTimeOffset)) + { + var d = (DateTimeOffset)constant.Value; + d = d.ToUniversalTime(); + return $"new {typeof(DateTimeOffset)}({d.Year:L}, {d.Month:L}, {d.Day:L} ,{d.Hour:L}, {d.Minute:L}, {d.Second:L}, {d.Millisecond:L}, {typeof(TimeSpan)}.{nameof(TimeSpan.Zero)})"; + } + + if (frameworkType == typeof(byte[])) + { + var bytes = (byte[])constant.Value; + var joinedBytes = string.Join(", ", bytes); + return $"new byte[] {{{joinedBytes}}}"; + } + + if (frameworkType == typeof(ResourceType)) + { + return $"{((ResourceType)constant.Value).ToString():L}"; + } + + if (frameworkType == typeof(bool) && writeAsString) + { + return $"\"{constant.Value!.ToString()!.ToLower()}\""; + } + + return $"{constant.Value:L}"; + } + + private static FormattableString Join(IEnumerable source, int count, Func converter, string separator, string? lastSeparator, char? format) + => count switch + { + 0 => Empty, + 1 => FormattableStringFactory.Create(format is not null ? $"{{0:{format}}}" : "{0}", converter(source.First())), + _ => FormattableStringFactory.Create(CreateFormatWithSeparator(separator, lastSeparator, format, count), source.Select(converter).ToArray()) + }; + + private static string CreateFormatWithSeparator(string separator, string? lastSeparator, char? format, int count) + { + const int offset = 48; // (int)'0' is 48 + if (count > 100) + { + var s = string.Join(separator, Enumerable.Range(0, count).Select(i => $"{{{i}}}")); + return lastSeparator is null ? s : s.ReplaceLast(separator, lastSeparator); + } + + Debug.Assert(count > 1); + + lastSeparator ??= separator; + + var placeholderLength = format.HasValue ? 5 : 3; + var length = count < 10 + ? count * placeholderLength + : (count - 10) * (placeholderLength + 1) + 10 * placeholderLength; + + length += separator.Length * (count - 2) + lastSeparator.Length; + + return string.Create(length, (separator, lastSeparator, format, count), static (span, state) => + { + var (separator, lastSeparator, format, count) = state; + for (int i = 0; i < count; i++) + { + span[0] = '{'; + if (i < 10) + { + span[1] = (char)(i + offset); + span = span[2..]; + } + else + { + span[1] = (char)(i / 10 + offset); + span[2] = (char)(i % 10 + offset); + span = span[3..]; + } + + if (format is not null) + { + span[0] = ':'; + span[1] = format.Value; + span = span[2..]; + } + + span[0] = '}'; + span = span[1..]; + + if (i < count - 1) + { + var separatorToUse = i < count - 2 ? separator : lastSeparator; + separatorToUse.CopyTo(span); + span = span[separatorToUse.Length..]; + } + } + + Debug.Assert(span.IsEmpty); + }); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/JsonCodeWriterExtensions.cs b/logger/autorest.csharp/common/Generation/Writers/JsonCodeWriterExtensions.cs new file mode 100644 index 0000000..8c2f013 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/JsonCodeWriterExtensions.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Types; +using Azure; +using Azure.Core; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class JsonCodeWriterExtensions + { + public static FormattableString GetDeserializeValueFormattable(FormattableString element, CSharpType serializationType, SerializationFormat serializationFormat = SerializationFormat.Default, JsonSerializationOptions serializationOptions = JsonSerializationOptions.None) + { + if (serializationType.SerializeAs != null) + { + return $"({serializationType}){GetFrameworkTypeValueFormattable(serializationType.SerializeAs, element, serializationFormat, serializationType)}"; + } + + if (serializationType.IsFrameworkType) + { + var frameworkType = serializationType.FrameworkType; + if (frameworkType == typeof(Nullable<>)) + { + frameworkType = serializationType.Arguments[0].FrameworkType; + } + + return GetFrameworkTypeValueFormattable(frameworkType, element, serializationFormat, serializationType); + } + + return GetDeserializeImplementationFormattable(serializationType.Implementation, element, serializationOptions); + } + + public static FormattableString GetFrameworkTypeValueFormattable(Type frameworkType, FormattableString element, SerializationFormat format, CSharpType? serializationType) + { + bool includeFormat = false; + + if (frameworkType == typeof(ETag) || + frameworkType == typeof(Uri) || + frameworkType == typeof(ResourceIdentifier) || + frameworkType == typeof(ResourceType) || + frameworkType == typeof(ContentType) || + frameworkType == typeof(RequestMethod) || + frameworkType == typeof(AzureLocation)) + { + return $"new {frameworkType}({element}.GetString())"; + } + + if (frameworkType == typeof(IPAddress)) + { + return $"{frameworkType}.Parse({element}.GetString())"; + } + + var methodName = string.Empty; + if (frameworkType == typeof(BinaryData)) + { + switch (format) + { + case SerializationFormat.Bytes_Base64: //intentional fall through + case SerializationFormat.Bytes_Base64Url: + return $"{typeof(BinaryData)}.FromBytes({element}.GetBytesFromBase64(\"{format.ToFormatSpecifier()}\"))"; + default: + return $"{typeof(BinaryData)}.FromString({element}.GetRawText())"; + } + } + + if (frameworkType == typeof(TimeSpan)) + { + if (format is SerializationFormat.Duration_Seconds) + { + return $"{typeof(TimeSpan)}.FromSeconds({element}.GetInt32())"; + } + else if (format is SerializationFormat.Duration_Seconds_Float or SerializationFormat.Duration_Seconds_Double) + { + return $"{typeof(TimeSpan)}.FromSeconds({element}.GetDouble())"; + } + } + + if (frameworkType == typeof(DateTimeOffset)) + { + if (format == SerializationFormat.DateTime_Unix) + { + return $"{typeof(DateTimeOffset)}.FromUnixTimeSeconds({element}.GetInt64())"; + } + } + + if (IsCustomJsonConverterAdded(frameworkType)) + { + return $"{typeof(JsonSerializer)}.{nameof(JsonSerializer.Deserialize)}<{serializationType}>({element}.GetRawText())"; + } + + if (frameworkType == typeof(JsonElement)) + methodName = "Clone"; + if (frameworkType == typeof(object)) + methodName = "GetObject"; + if (frameworkType == typeof(bool)) + methodName = "GetBoolean"; + if (frameworkType == typeof(char)) + methodName = "GetChar"; + if (frameworkType == typeof(short)) + methodName = "GetInt16"; + if (frameworkType == typeof(int)) + methodName = "GetInt32"; + if (frameworkType == typeof(long)) + methodName = "GetInt64"; + if (frameworkType == typeof(float)) + methodName = "GetSingle"; + if (frameworkType == typeof(double)) + methodName = "GetDouble"; + if (frameworkType == typeof(decimal)) + methodName = "GetDecimal"; + if (frameworkType == typeof(string)) + methodName = "GetString"; + if (frameworkType == typeof(Guid)) + methodName = "GetGuid"; + + if (frameworkType == typeof(byte[])) + { + methodName = "GetBytesFromBase64"; + includeFormat = true; + } + + if (frameworkType == typeof(DateTimeOffset)) + { + methodName = "GetDateTimeOffset"; + includeFormat = true; + } + + if (frameworkType == typeof(DateTime)) + { + methodName = "GetDateTime"; + includeFormat = true; + } + + if (frameworkType == typeof(TimeSpan)) + { + methodName = "GetTimeSpan"; + includeFormat = true; + } + + if (includeFormat && format.ToFormatSpecifier() is { } formatString) + { + return $"{element}.{methodName}({formatString:L})"; + } + + return $"{element}.{methodName}()"; + } + + public static FormattableString GetDeserializeImplementationFormattable(TypeProvider implementation, FormattableString element, JsonSerializationOptions options) + { + switch (implementation) + { + case SystemObjectType systemObjectType when IsCustomJsonConverterAdded(systemObjectType.SystemType): + var optionalSerializeOptions = options == JsonSerializationOptions.UseManagedServiceIdentityV3 ? ", serializeOptions" : string.Empty; + return $"{typeof(JsonSerializer)}.{nameof(JsonSerializer.Deserialize)}<{implementation.Type}>({element}.GetRawText(){optionalSerializeOptions})"; + + case Resource { ResourceData: SerializableObjectType { Serialization.Json: { }, IncludeDeserializer: true } resourceDataType } resource: + return $"new {resource.Type}(Client, {resourceDataType.Type}.Deserialize{resourceDataType.Declaration.Name}({element}))"; + + case MgmtObjectType mgmtObjectType when TypeReferenceTypeChooser.HasMatch(mgmtObjectType.InputModel): + return $"{typeof(JsonSerializer)}.{nameof(JsonSerializer.Deserialize)}<{implementation.Type}>({element}.GetRawText())"; + + case SerializableObjectType { Serialization.Json: { }, IncludeDeserializer: true } type: + return $"{type.Type}.Deserialize{type.Declaration.Name}({element})"; + + case EnumType clientEnum: + var value = GetFrameworkTypeValueFormattable(clientEnum.ValueType.FrameworkType, element, SerializationFormat.Default, null); + return clientEnum.IsExtensible + ? $"new {clientEnum.Type}({value})" + : (FormattableString)$"{value}.To{clientEnum.Type:D}()"; + + default: + throw new NotSupportedException($"No deserialization logic exists for {implementation.Declaration.Name}"); + } + } + + private static bool IsCustomJsonConverterAdded(Type type) + { + return type.GetCustomAttributes().Any(a => a.GetType() == typeof(JsonConverterAttribute)); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/LongRunningOperationWriter.cs b/logger/autorest.csharp/common/Generation/Writers/LongRunningOperationWriter.cs new file mode 100644 index 0000000..5f95a3b --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/LongRunningOperationWriter.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Autorest.CSharp.Core; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Requests; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; +using Request = Azure.Core.Request; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal sealed class LongRunningOperationWriter + { + public void Write(CodeWriter writer, LongRunningOperation operation) + { + var pagingResponse = operation.PagingResponse; + + var cs = operation.Type; + var @namespace = cs.Namespace; + using (writer.Namespace(@namespace)) + { + writer.WriteXmlDocumentationSummary($"{operation.Description}"); + var interfaceType = GetInterfaceType(operation); + var baseType = GetBaseType(operation); + var helperType = GetHelperType(operation); + + writer.Append($"{operation.Declaration.Accessibility} partial class {cs.Name}: {baseType}"); + if (interfaceType != null) + { + writer.Append($", {interfaceType}"); + } + + using (writer.Scope()) + { + WriteFields(writer, pagingResponse, helperType); + + writer.Line(); + writer.WriteXmlDocumentationSummary($"Initializes a new instance of {cs.Name} for mocking."); + using (writer.Scope($"protected {cs.Name:D}()")) + { + } + + writer.Line(); + + WriteConstructor(writer, operation, pagingResponse, cs, helperType); + writer.Line(); + + writer + .WriteXmlDocumentationInheritDoc() + .Line($"#pragma warning disable CA1822") + .Line($"public override string Id => throw new NotImplementedException();") + .Line($"#pragma warning restore CA1822") + .Line(); + + WriteValueProperty(writer, operation); + + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override bool HasCompleted => _operation.HasCompleted;"); + writer.Line(); + + if (operation.ResultType != null) + { + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override bool HasValue => _operation.HasValue;"); + writer.Line(); + } + + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override {Configuration.ApiTypes.ResponseType} {Configuration.ApiTypes.GetRawResponseName}() => _operation.RawResponse;"); + writer.Line(); + + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override {Configuration.ApiTypes.ResponseType} UpdateStatus({typeof(CancellationToken)} cancellationToken = default) => _operation.UpdateStatus(cancellationToken);"); + writer.Line(); + + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override {Configuration.ApiTypes.GetValueTaskOfResponse()} UpdateStatusAsync({typeof(CancellationToken)} cancellationToken = default) => _operation.UpdateStatusAsync(cancellationToken);"); + writer.Line(); + + WriteWaitForCompletionVariants(writer, operation); + writer.Line(); + + if (operation.ResultType != null) + { + WriteCreateResult(writer, operation, pagingResponse, operation.ResultType, interfaceType!); + writer.Line(); + WriteCreateResultAsync(writer, operation, pagingResponse, operation.ResultType, interfaceType!); + } + } + } + } + + private CSharpType? GetInterfaceType(LongRunningOperation operation) + { + return operation.ResultType != null ? new CSharpType(typeof(IOperationSource<>), operation.ResultType) : null; + } + + private CSharpType GetNextLinkOperationType(LongRunningOperation operation) + { + return operation.ResultType != null ? new CSharpType(typeof(IOperation<>), operation.ResultType) : typeof(IOperation); + } + + private CSharpType GetBaseType(LongRunningOperation operation) + { + return operation.ResultType != null ? new CSharpType(typeof(Operation<>), operation.ResultType) : new CSharpType(typeof(Operation)); + } + + private CSharpType GetValueTaskType(LongRunningOperation operation) + { + return operation.ResultType != null ? new CSharpType(Configuration.ApiTypes.ResponseOfTType, operation.ResultType) : new CSharpType(Configuration.ApiTypes.ResponseType); + } + + private CSharpType GetHelperType(LongRunningOperation operation) + { + return operation.ResultType != null ? new CSharpType(typeof(OperationInternal<>), operation.ResultType) : new CSharpType(typeof(OperationInternal)); + } + + private void WriteFields(CodeWriter writer, PagingResponseInfo? pagingResponse, CSharpType helperType) + { + writer.Line($"private readonly {helperType} _operation;"); + + if (pagingResponse != null) + { + writer.Line($"private readonly {Configuration.ApiTypes.GetNextPageFuncType()} _nextPageFunc;"); + writer.Line($"private readonly {typeof(ClientDiagnostics)} _clientDiagnostics;"); + writer.Line($"private readonly {Configuration.ApiTypes.HttpPipelineType} _pipeline;"); + } + } + + private void WriteConstructor(CodeWriter writer, LongRunningOperation operation, PagingResponseInfo? pagingResponse, CSharpType lroType, CSharpType helperType) + { + writer.Append($"internal {lroType.Name}({typeof(ClientDiagnostics)} clientDiagnostics, {Configuration.ApiTypes.HttpPipelineType} pipeline, {typeof(Request)} request, {Configuration.ApiTypes.ResponseType} {Configuration.ApiTypes.ResponseParameterName}"); + + if (pagingResponse != null) + { + writer.Append($", {Configuration.ApiTypes.GetNextPageFuncType()} nextPageFunc"); + } + writer.Line($")"); + + using (writer.Scope()) + { + var nextLinkOperationVariable = new CodeWriterDeclaration("nextLinkOperation"); + writer + .Append($"{GetNextLinkOperationType(operation)} {nextLinkOperationVariable:D} = {typeof(NextLinkOperationImplementation)}.{nameof(NextLinkOperationImplementation.Create)}(") + .AppendIf($"this, ", operation.ResultType != null) + .Line($"pipeline, request.Method, request.Uri.ToUri(), {Configuration.ApiTypes.ResponseParameterName}, {typeof(OperationFinalStateVia)}.{operation.FinalStateVia});") + .Line($"_operation = new {helperType}(nextLinkOperation, clientDiagnostics, {Configuration.ApiTypes.ResponseParameterName}, { operation.Diagnostics.ScopeName:L});"); + + if (pagingResponse != null) + { + writer.Line($"_nextPageFunc = nextPageFunc;"); + writer.Line($"_clientDiagnostics = clientDiagnostics;"); + writer.Line($"_pipeline = pipeline;"); + } + } + } + + private void WriteValueProperty(CodeWriter writer, LongRunningOperation operation) + { + if (operation.ResultType != null) + { + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override {operation.ResultType} Value => _operation.Value;"); + writer.Line(); + } + } + + private void WriteWaitForCompletionVariants(CodeWriter writer, LongRunningOperation operation) + { + var valueTaskType = GetValueTaskType(operation); + var waitForCompletionMethodName = operation.ResultType != null ? "WaitForCompletion" : "WaitForCompletionResponse"; + + WriteWaitForCompletionMethods(writer, valueTaskType, waitForCompletionMethodName, false); + WriteWaitForCompletionMethods(writer, valueTaskType, waitForCompletionMethodName, true); + } + + private void WriteWaitForCompletionMethods(CodeWriter writer, CSharpType valueTaskType, string waitForCompletionMethodName, bool async) + { + var waitForCompletionType = async ? new CSharpType(typeof(ValueTask<>), valueTaskType) : valueTaskType; + + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override {waitForCompletionType} {waitForCompletionMethodName}{(async ? "Async" : string.Empty)}({typeof(CancellationToken)} cancellationToken = default) => _operation.{waitForCompletionMethodName}{(async ? "Async" : string.Empty)}(cancellationToken);"); + writer.Line(); + + writer.WriteXmlDocumentationInheritDoc(); + writer.Line($"public override {waitForCompletionType} {waitForCompletionMethodName}{(async ? "Async" : string.Empty)}({typeof(TimeSpan)} pollingInterval, {typeof(CancellationToken)} cancellationToken = default) => _operation.{waitForCompletionMethodName}{(async ? "Async" : string.Empty)}(pollingInterval, cancellationToken);"); + writer.Line(); + } + + private static void WriteCreateResult(CodeWriter writer, LongRunningOperation operation, PagingResponseInfo? pagingResponse, CSharpType resultType, CSharpType interfaceType) + { + var responseVariable = new CodeWriterDeclaration(Configuration.ApiTypes.ResponseParameterName); + using (writer.Scope($"{resultType} {interfaceType}.CreateResult({Configuration.ApiTypes.ResponseType} {responseVariable:D}, {typeof(CancellationToken)} cancellationToken)")) + { + WriteCreateResultBody(writer, operation, responseVariable, pagingResponse, resultType, false); + } + } + + private static void WriteCreateResultAsync(CodeWriter writer, LongRunningOperation operation, PagingResponseInfo? pagingResponse, CSharpType resultType, CSharpType interfaceType) + { + var responseVariable = new CodeWriterDeclaration(Configuration.ApiTypes.ResponseParameterName); + var asyncKeyword = pagingResponse == null && operation.ResultSerialization != null ? "async " : ""; + using (writer.Scope($"{asyncKeyword}{new CSharpType(typeof(ValueTask<>), resultType)} {interfaceType}.CreateResultAsync({Configuration.ApiTypes.ResponseType} {responseVariable:D}, {typeof(CancellationToken)} cancellationToken)")) + { + WriteCreateResultBody(writer, operation, responseVariable, pagingResponse, resultType, true); + } + } + + private static void WriteCreateResultBody(CodeWriter writer, LongRunningOperation operation, CodeWriterDeclaration responseVariable, PagingResponseInfo? pagingResponse, CSharpType resultType, bool async) + { + if (pagingResponse != null) + { + var scopeName = operation.Diagnostics.ScopeName; + var nextLinkName = pagingResponse.NextLinkPropertyName; + var itemName = pagingResponse.ItemPropertyName; + // TODO -- why we have a hard-coded Product.DeserializeProduct here??? + FormattableString returnValue = $"{typeof(GeneratorPageableHelpers)}.{nameof(GeneratorPageableHelpers.CreateAsyncPageable)}({responseVariable}, _nextPageFunc, e => Product.DeserializeProduct(e), _clientDiagnostics, _pipeline, {scopeName:L}, {itemName:L}, {nextLinkName:L}, cancellationToken)"; + WriteCreateResultReturnValue(writer, resultType, returnValue, async); + } + else if (operation.ResultSerialization != null) + { + writer.WriteDeserializationForMethods(operation.ResultSerialization, async, null, $"{responseVariable}.{Configuration.ApiTypes.ContentStreamName}", resultType); + } + else + { + WriteCreateResultReturnValue(writer, resultType, $"{responseVariable}", async); + } + } + + private static void WriteCreateResultReturnValue(CodeWriter writer, CSharpType resultType, FormattableString returnValue, bool async) + { + if (async) + { + writer.Line($"return new {new CSharpType(typeof(ValueTask<>), resultType)}({returnValue});"); + } + else + { + writer.Line($"return {returnValue};"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/ModelFactoryWriter.cs b/logger/autorest.csharp/common/Generation/Writers/ModelFactoryWriter.cs new file mode 100644 index 0000000..fd86a49 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/ModelFactoryWriter.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class ModelFactoryWriter + { + protected CodeWriter _writer; + private ModelFactoryTypeProvider This { get; } + + public ModelFactoryWriter(ModelFactoryTypeProvider modelFactoryProvider) : this(new CodeWriter(), modelFactoryProvider) + { + } + + public ModelFactoryWriter(CodeWriter writer, ModelFactoryTypeProvider modelFactoryProvider) + { + _writer = writer; + This = modelFactoryProvider; + } + + public void Write() + { + using (_writer.Namespace(This.Type.Namespace)) + { + _writer.WriteXmlDocumentationSummary(This.Description); + using (_writer.Scope($"{This.Declaration.Accessibility} static partial class {This.Type:D}")) + { + foreach (var method in This.Methods) + { + _writer.WriteMethodDocumentation(method.Signature); + _writer.WriteMethod(method); + } + } + } + } + + public override string ToString() + { + return _writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/ModelWriter.cs b/logger/autorest.csharp/common/Generation/Writers/ModelWriter.cs new file mode 100644 index 0000000..c8edb8d --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/ModelWriter.cs @@ -0,0 +1,503 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Types; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class ModelWriter + { + public void WriteModel(CodeWriter writer, TypeProvider model) + { + switch (model) + { + case ObjectType objectSchema: + WriteObjectSchema(writer, objectSchema); + break; + case EnumType { IsExtensible: true } e: + WriteExtensibleEnum(writer, e); + break; + case EnumType { IsExtensible: false } e: + WriteEnum(writer, e); + break; + default: + throw new NotImplementedException(); + } + } + + private void WriteObjectSchema(CodeWriter writer, ObjectType schema) + { + using (writer.Namespace(schema.Declaration.Namespace)) + { + List implementsTypes = new List(); + if (schema.Inherits != null) + { + implementsTypes.Add(schema.Inherits); + } + + var description = string.IsNullOrEmpty(schema.Description.ToString()) ? $"The {schema.Declaration.Name}." : schema.Description; + writer.WriteXmlDocumentationSummary(description); + AddClassAttributes(writer, schema); + + if (schema.IsStruct) + { + writer.Append($"{schema.Declaration.Accessibility} readonly partial struct {schema.Declaration.Name}"); + } + else + { + writer.Append($"{schema.Declaration.Accessibility} {GetAbstract(schema)}partial class {schema.Declaration.Name}"); + } + + if (implementsTypes.Any()) + { + writer.AppendRaw(" : "); + foreach (var type in implementsTypes) + { + writer.Append($"{type} ,"); + } + writer.RemoveTrailingComma(); + } + + writer.Line(); + using (writer.Scope()) + { + WritePrivateRawDataField(writer, schema); + + WriteConstructor(writer, schema); + + WriteProperties(writer, schema); + } + } + } + + protected virtual void WriteProperties(CodeWriter writer, ObjectType schema) + { + var rawDataField = (schema as SerializableObjectType)?.RawDataField; + foreach (var property in schema.Properties) + { + if (property == rawDataField) + continue; + + WriteProperty(writer, property, schema); + + if (property.FlattenedProperty != null) + WriteProperty(writer, property.FlattenedProperty, schema); + } + } + + // TODO -- this is workaround because we are declaring fields and properties in different way, and this raw data field and AdditionalProperties property could be converted from each other. + private void WritePrivateRawDataField(CodeWriter writer, ObjectType schema) + { + if ((schema as SerializableObjectType)?.RawDataField is not { } rawDataField) + return; + + if (Configuration.EnableInternalRawData) + { + writer.WriteXmlDocumentationSummary($"{rawDataField.Description}"); + writer.Append($"{rawDataField.Declaration.Accessibility} {rawDataField.Declaration.Type} {rawDataField.Declaration.Name} {{ get; set; }}"); + } + else + { + writer.WriteXmlDocumentationSummary($"{rawDataField.Description}"); + writer.Append($"{rawDataField.Declaration.Accessibility} ") + .AppendRawIf("readonly ", schema.IsStruct) + .Line($"{rawDataField.Declaration.Type} {rawDataField.Declaration.Name};"); + } + + writer.Line(); + } + + private void WriteFieldModifiers(CodeWriter writer, FieldModifiers modifiers) + { + writer.AppendRawIf("public ", modifiers.HasFlag(FieldModifiers.Public)) + .AppendRawIf("internal ", modifiers.HasFlag(FieldModifiers.Internal)) + .AppendRawIf("protected ", modifiers.HasFlag(FieldModifiers.Protected)) + .AppendRawIf("private ", modifiers.HasFlag(FieldModifiers.Private)) + .AppendRawIf("static ", modifiers.HasFlag(FieldModifiers.Static)) + .AppendRawIf("readonly ", modifiers.HasFlag(FieldModifiers.ReadOnly)) + .AppendRawIf("const ", modifiers.HasFlag(FieldModifiers.Const)); + } + + private void WriteProperty(CodeWriter writer, ObjectTypeProperty property, ObjectType objectType) + { + writer.WriteXmlDocumentationSummary(CreatePropertyDescription(property)); + if (!MgmtReferenceType.IsReferenceType(objectType) && Configuration.EnableBicepSerialization && objectType.Declaration.Accessibility == "public" && property.Declaration.Accessibility == "public") + { + writer.Line($"[WirePath(\"{property.GetWirePath()}\")]"); + } + writer.Append($"{property.Declaration.Accessibility} {property.Declaration.Type} {property.Declaration.Name:D}"); + + // write getter + writer.AppendRaw("{"); + if (property.GetterModifiers is { } getterModifiers) + WriteFieldModifiers(writer, getterModifiers); + writer.AppendRaw("get;"); + // writer setter + if (!property.IsReadOnly) + { + if (property.SetterModifiers is { } setterModifiers) + WriteFieldModifiers(writer, setterModifiers); + writer.AppendRaw("set;"); + } + writer.AppendRaw("}"); + if (property.InitializationValue != null) + { + writer.AppendRaw(" = "); + property.InitializationValue.Write(writer); + writer.Line($";"); + } + + writer.Line(); + } + + private void WriteProperty(CodeWriter writer, FlattenedObjectTypeProperty property, ObjectType objectType) + { + writer.WriteXmlDocumentationSummary(CreatePropertyDescription(property)); + if (Configuration.EnableBicepSerialization && objectType.Declaration.Accessibility == "public" && property.Declaration.Accessibility == "public") + { + writer.Line($"[WirePath(\"{property.GetWirePath()}\")]"); + } + using (writer.Scope($"{property.Declaration.Accessibility} {property.Declaration.Type} {property.Declaration.Name:D}")) + { + // write getter + switch (property.IncludeGetterNullCheck) + { + case true: + WriteGetWithNullCheck(writer, property); + break; + case false: + WriteGetWithDefault(writer, property); + break; + default: + WriteGetWithEscape(writer, property); + break; + } + + // only write the setter when it is not readonly + if (!property.IsReadOnly) + { + if (property.IncludeSetterNullCheck) + { + WriteSetWithNullCheck(writer, property); + } + else + { + WriteSetWithSingleParamCtor(writer, property); + } + } + } + + writer.Line(); + } + + private static void WriteSetWithNullCheck(CodeWriter writer, FlattenedObjectTypeProperty property) + { + var underlyingName = property.UnderlyingProperty.Declaration.Name; + var underlyingType = property.UnderlyingProperty.Declaration.Type; + using (writer.Scope($"set")) + { + if (property.IsOverriddenValueType) + { + using (writer.Scope($"if (value.HasValue)")) + { + writer.Line($"if ({underlyingName} is null)"); + writer.Line($"{underlyingName} = new {underlyingType}();"); + writer.Line($"{underlyingName}.{property.ChildPropertyName} = value.Value;"); + } + using (writer.Scope($"else")) + { + writer.Line($"{underlyingName} = null;"); + } + } + else + { + writer.Line($"if ({underlyingName} is null)"); + writer.Line($"{underlyingName} = new {underlyingType}();"); + writer.Line($"{underlyingName}.{property.ChildPropertyName} = value;"); + } + } + } + + private static void WriteSetWithSingleParamCtor(CodeWriter writer, FlattenedObjectTypeProperty property) + { + var underlyingName = property.UnderlyingProperty.Declaration.Name; + var underlyingType = property.UnderlyingProperty.Declaration.Type; + if (property.IsOverriddenValueType) + { + using (writer.Scope($"set")) + { + writer.Line($"{underlyingName} = value.HasValue ? new {underlyingType}(value.Value) : null;"); + } + } + else + { + writer.Line($"set => {underlyingName} = new {underlyingType}(value);"); + } + } + + private static void WriteGetWithNullCheck(CodeWriter writer, FlattenedObjectTypeProperty property) + { + var underlyingName = property.UnderlyingProperty.Declaration.Name; + var underlyingType = property.UnderlyingProperty.Declaration.Type; + using (writer.Scope($"get")) + { + writer.Line($"if ({underlyingName} is null)"); + writer.Line($"{underlyingName} = new {underlyingType}();"); + writer.Line($"return {underlyingName:D}.{property.ChildPropertyName};"); + } + } + + private static void WriteGetWithDefault(CodeWriter writer, FlattenedObjectTypeProperty property) + { + writer.Line($"get => {property.UnderlyingProperty.Declaration.Name:I} is null ? default({property.Declaration.Type}) : {property.UnderlyingProperty.Declaration.Name}.{property.ChildPropertyName};"); + } + + private static void WriteGetWithEscape(CodeWriter writer, FlattenedObjectTypeProperty property) + { + writer.Append($"get => {property.UnderlyingProperty.Declaration.Name}") + .AppendRawIf("?", property.IsUnderlyingPropertyNullable) + .Line($".{property.ChildPropertyName};"); + } + + private FormattableString CreatePropertyDescription(ObjectTypeProperty property, string? overrideName = null) + { + if (!string.IsNullOrWhiteSpace(property.PropertyDescription.ToString())) + { + return $"{property.PropertyDescription}"; + } + return $"{ObjectTypeProperty.CreateDefaultPropertyDescription(overrideName ?? property.Declaration.Name, property.IsReadOnly)}"; + } + + private string GetAbstract(ObjectType schema) + { + // Limit this change to management plane to avoid data plane affected + return schema.Declaration.IsAbstract ? "abstract " : string.Empty; + } + + protected virtual void AddClassAttributes(CodeWriter writer, ObjectType schema) + { + if (schema.Deprecated != null) + { + writer.Line($"[{typeof(ObsoleteAttribute)}(\"{schema.Deprecated}\")]"); + } + } + + private void AddClassAttributes(CodeWriter writer, EnumType enumType) + { + if (enumType.Deprecated != null) + { + writer.Line($"[{typeof(ObsoleteAttribute)}(\"{enumType.Deprecated}\")]"); + } + } + + protected virtual void AddCtorAttribute(CodeWriter writer, ObjectType schema, ObjectTypeConstructor constructor) + { + } + + private void WriteConstructor(CodeWriter writer, ObjectType schema) + { + foreach (var constructor in schema.Constructors) + { + writer.WriteMethodDocumentation(constructor.Signature); + AddCtorAttribute(writer, schema, constructor); + using (writer.WriteMethodDeclaration(constructor.Signature)) + { + writer.WriteParametersValidation(constructor.Signature.Parameters); + + foreach (var initializer in constructor.Initializers) + { + writer.Append($"{initializer.Property.Declaration.Name} = ") + .WriteReferenceOrConstant(initializer.Value) + .WriteConversion(initializer.Value.Type, initializer.Property.Declaration.Type); + + if (initializer.DefaultValue != null && (!initializer.Value.Type.IsValueType || initializer.Value.Type.IsNullable)) + { + writer.Append($"?? ").WriteReferenceOrConstant(initializer.DefaultValue.Value); + } + + writer.Line($";"); + } + } + writer.Line(); + } + } + + public void WriteEnum(CodeWriter writer, EnumType enumType) + { + if (enumType.Declaration.IsUserDefined) + { + return; + } + + using (writer.Namespace(enumType.Declaration.Namespace)) + { + writer.WriteXmlDocumentationSummary($"{enumType.Description}"); + AddClassAttributes(writer, enumType); + + writer.Append($"{enumType.Declaration.Accessibility} enum {enumType.Declaration.Name}") + .AppendIf($" : {enumType.ValueType}", enumType.IsIntValueType && !enumType.ValueType.Equals(typeof(int))); + using (writer.Scope()) + { + foreach (EnumTypeValue value in enumType.Values) + { + writer.WriteXmlDocumentationSummary($"{value.Description}"); + if (enumType.IsIntValueType) + { + writer.Line($"{value.Declaration.Name} = {value.Value.Value:L},"); + } + else + { + writer.Line($"{value.Declaration.Name},"); + } + } + writer.RemoveTrailingComma(); + } + } + } + + public void WriteExtensibleEnum(CodeWriter writer, EnumType enumType) + { + var cs = enumType.Type; + string name = enumType.Declaration.Name; + var valueType = enumType.ValueType; + var isString = valueType.FrameworkType == typeof(string); + + using (writer.Namespace(enumType.Declaration.Namespace)) + { + writer.WriteXmlDocumentationSummary($"{enumType.Description}"); + AddClassAttributes(writer, enumType); + + var implementType = new CSharpType(typeof(IEquatable<>), cs); + using (writer.Scope($"{enumType.Declaration.Accessibility} readonly partial struct {name}: {implementType}")) + { + writer.Line($"private readonly {valueType} _value;"); + writer.Line(); + + writer.WriteXmlDocumentationSummary($"Initializes a new instance of {name:C}."); + + if (isString) + { + writer.WriteXmlDocumentationException(typeof(ArgumentNullException), $" is null."); + } + + using (writer.Scope($"public {name}({valueType} value)")) + { + writer.Append($"_value = value"); + if (isString) + { + writer.Append($"?? throw new {typeof(ArgumentNullException)}(nameof(value))"); + } + writer.Line($";"); + } + writer.Line(); + + foreach (var choice in enumType.Values) + { + var fieldName = GetValueFieldName(name, choice.Declaration.Name, enumType.Values); + writer.Line($"private const {valueType} {fieldName} = {choice.Value.Value:L};"); + } + writer.Line(); + + foreach (var choice in enumType.Values) + { + writer.WriteXmlDocumentationSummary($"{choice.Description}"); + var fieldName = GetValueFieldName(name, choice.Declaration.Name, enumType.Values); + writer.Append($"public static {cs} {choice.Declaration.Name}").AppendRaw("{ get; }").Append($" = new {cs}({fieldName});").Line(); + } + + // write ToSerial method, only write when the underlying type is not a string + if (!enumType.IsStringValueType) + { + writer.Line(); + writer.Line($"internal {valueType} {enumType.SerializationMethodName}() => _value;"); + writer.Line(); + } + + writer.WriteXmlDocumentationSummary($"Determines if two {name:C} values are the same."); + writer.Line($"public static bool operator ==({cs} left, {cs} right) => left.Equals(right);"); + + writer.WriteXmlDocumentationSummary($"Determines if two {name:C} values are not the same."); + writer.Line($"public static bool operator !=({cs} left, {cs} right) => !left.Equals(right);"); + + writer.WriteXmlDocumentationSummary($"Converts a {valueType:C} to a {name:C}."); + writer.Line($"public static implicit operator {cs}({valueType} value) => new {cs}(value);"); + writer.Line(); + + writer.WriteXmlDocumentationInheritDoc(); + WriteEditorBrowsableFalse(writer); + writer.Line($"public override bool Equals({typeof(object)} obj) => obj is {cs} other && Equals(other);"); + + writer.WriteXmlDocumentationInheritDoc(); + writer.Append($"public bool Equals({cs} other) => "); + if (isString) + { + writer.Line($"{valueType}.Equals(_value, other._value, {typeof(StringComparison)}.InvariantCultureIgnoreCase);"); + } + else + { + writer.Line($"{valueType}.Equals(_value, other._value);"); + } + writer.Line(); + + writer.WriteXmlDocumentationInheritDoc(); + WriteEditorBrowsableFalse(writer); + writer.Append($"public override int GetHashCode() => "); + if (isString) + { + writer.Line($"_value != null ? {typeof(StringComparer)}.InvariantCultureIgnoreCase.GetHashCode(_value) : 0;"); + } + else + { + writer.Line($"_value.GetHashCode();"); + } + + writer.WriteXmlDocumentationInheritDoc(); + writer.Append($"public override {typeof(string)} ToString() => "); + + if (isString) + { + writer.Line($"_value;"); + } + else + { + writer.Line($"_value.ToString({typeof(CultureInfo)}.InvariantCulture);"); + } + } + } + } + + private static string GetValueFieldName(string enumName, string enumValue, IList enumValues) + { + if (enumName != $"{enumValue}Value") + { + return $"{enumValue}Value"; + } + + int index = 1; + foreach (var value in enumValues) + { + if (value.Declaration.Name == $"{enumValue}Value{index}") + { + index++; + } + } + return $"{enumValue}Value{index}"; + } + + private static void WriteEditorBrowsableFalse(CodeWriter writer) + { + writer.Line($"[{typeof(EditorBrowsableAttribute)}({typeof(EditorBrowsableState)}.{nameof(EditorBrowsableState.Never)})]"); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/PageableMethodsWriterExtensions.cs b/logger/autorest.csharp/common/Generation/Writers/PageableMethodsWriterExtensions.cs new file mode 100644 index 0000000..bbf1bd5 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/PageableMethodsWriterExtensions.cs @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using Autorest.CSharp.Core; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; + +namespace AutoRest.CSharp.Generation.Writers +{ + // TODO -- this class could be a "PageableMethodsBuilder" in the future + internal static class PageableMethodsWriterExtensions + { + private static readonly CSharpType BinaryDataType = typeof(BinaryData); + + public static CodeWriter WriteLongRunningPageable(this CodeWriter writer, MethodSignature methodSignature, CSharpType? pageItemType, Reference? restClientReference, RestClientMethod createLroRequestMethod, RestClientMethod? createNextPageRequestMethod, Reference clientDiagnosticsReference, Reference pipelineReference, Diagnostic diagnostic, OperationFinalStateVia finalStateVia, string? itemPropertyName, string? nextLinkPropertyName, bool async) + { + using (writer.WriteMethodDeclaration(methodSignature.WithAsync(async))) + { + writer.WriteParametersValidation(methodSignature.Parameters); + using (writer.WriteDiagnosticScope(diagnostic, clientDiagnosticsReference)) + { + var messageVariable = new CodeWriterDeclaration("message"); + var nextPageRequest = GetCreateRequestCall(restClientReference, createNextPageRequestMethod); + var nextPageRequestVariable = nextPageRequest != null ? new CodeWriterDeclaration("NextPageRequest") : null; + var parameters = methodSignature.Parameters.ToList(); + writer.EnsureRequestContextVariable(parameters, null, createNextPageRequestMethod); + + var createPageableParameters = new List + { + $"{KnownParameters.WaitForCompletion.Name}", + $"{messageVariable:I}", + nextPageRequest != null ? $"{nextPageRequestVariable:I}" : (FormattableString)$"null", + GetValueFactory(pageItemType), + clientDiagnosticsReference.GetReferenceFormattable(), + pipelineReference.GetReferenceFormattable(), + $"{typeof(OperationFinalStateVia)}.{finalStateVia}" + }; + + createPageableParameters.AddTrailingPageableParameters(parameters, diagnostic.ScopeName, itemPropertyName, nextLinkPropertyName); + + if (nextPageRequestVariable != null) + { + writer.Line($"{Configuration.ApiTypes.HttpMessageType} {nextPageRequestVariable:D}({KnownParameters.PageSizeHint.Type} {KnownParameters.PageSizeHint.Name}, {KnownParameters.NextLink.Type} {KnownParameters.NextLink.Name}) => {nextPageRequest};"); + } + + writer.Line($"using {Configuration.ApiTypes.HttpMessageType} {messageVariable:D} = {RequestWriterHelpers.CreateRequestMethodName(createLroRequestMethod.Name)}({createLroRequestMethod.Parameters.GetIdentifiersFormattable()});"); + + if (async) + { + writer.Line($"return await {typeof(GeneratorPageableHelpers)}.{nameof(GeneratorPageableHelpers.CreateAsyncPageable)}({createPageableParameters.Join(", ")}).ConfigureAwait(false);"); + } + else + { + writer.Line($"return {typeof(GeneratorPageableHelpers)}.{nameof(GeneratorPageableHelpers.CreatePageable)}({createPageableParameters.Join(", ")});"); + } + } + } + + return writer.Line(); + } + + public static CodeWriter WritePageable(this CodeWriter writer, ConvenienceMethod convenienceMethod, RestClientMethod? createFirstPageRequestMethod, RestClientMethod? createNextPageRequestMethod, Reference clientDiagnosticsReference, Reference pipelineReference, string scopeName, string? itemPropertyName, string? nextLinkPropertyName, bool async) + { + using (writer.WriteMethodDeclaration(convenienceMethod.Signature.WithAsync(async))) + { + writer.WriteParametersValidation(convenienceMethod.Signature.Parameters); + + var firstPageRequest = GetCreateRequestCall(null, createFirstPageRequestMethod, convenienceMethod.ProtocolToConvenienceParameterConverters); + var nextPageRequest = GetCreateRequestCall(null, createNextPageRequestMethod, convenienceMethod.ProtocolToConvenienceParameterConverters); + var parameters = new List(); + + foreach ((Parameter protocolParameter, Parameter? convenienceParameter, _) in convenienceMethod.ProtocolToConvenienceParameterConverters) + { + if (protocolParameter.Type.EqualsIgnoreNullable(Configuration.ApiTypes.RequestContentType) && + convenienceParameter is { Name: var fromName, Type: { IsFrameworkType: false, Implementation: ModelTypeProvider }, IsOptionalInSignature: var isOptional }) + { + writer + .Append($"{protocolParameter.Type} {protocolParameter.Name:D} = {fromName:I}") + .AppendRawIf(".", isOptional) + .Line($".ToRequestContent();"); + } + else if (convenienceParameter is not null) + { + parameters.Add(convenienceParameter); + } + } + + writer.EnsureRequestContextVariable(parameters, createFirstPageRequestMethod, createNextPageRequestMethod); + writer.WritePageableBody(parameters, convenienceMethod.ResponseType, firstPageRequest, nextPageRequest, clientDiagnosticsReference, pipelineReference, scopeName, itemPropertyName, nextLinkPropertyName, async); + } + + return writer.Line(); + } + + public static CodeWriter WritePageable(this CodeWriter writer, MethodSignature methodSignature, CSharpType? pageItemType, Reference? restClientReference, RestClientMethod? createFirstPageRequestMethod, RestClientMethod? createNextPageRequestMethod, Reference clientDiagnosticsReference, Reference pipelineReference, string scopeName, string? itemPropertyName, string? nextLinkPropertyName, bool async) + { + using (writer.WriteMethodDeclaration(methodSignature.WithAsync(async))) + { + writer.WriteParametersValidation(methodSignature.Parameters); + var parameters = methodSignature.Parameters.ToList(); + var firstPageRequest = GetCreateRequestCall(restClientReference, createFirstPageRequestMethod); + var nextPageRequest = GetCreateRequestCall(restClientReference, createNextPageRequestMethod); + + writer.EnsureRequestContextVariable(parameters, createFirstPageRequestMethod, createNextPageRequestMethod); + writer.WritePageableBody(parameters, pageItemType, firstPageRequest, nextPageRequest, clientDiagnosticsReference, pipelineReference, scopeName, itemPropertyName, nextLinkPropertyName, async); + } + + return writer.Line(); + } + + public static CodeWriter WritePageableBody(this CodeWriter writer, IReadOnlyList methodParameters, CSharpType? pageItemType, FormattableString? firstPageRequest, FormattableString? nextPageRequest, Reference clientDiagnosticsReference, Reference pipelineReference, string scopeName, string? itemPropertyName, string? nextLinkPropertyName, bool async) + { + var firstPageRequestVariable = firstPageRequest != null ? new CodeWriterDeclaration("FirstPageRequest") : null; + var nextPageRequestVariable = nextPageRequest != null ? new CodeWriterDeclaration("NextPageRequest") : null; + List createPageableParameters = new() + { + firstPageRequest != null ? $"{firstPageRequestVariable:I}" : (FormattableString)$"null", + nextPageRequest != null ? $"{nextPageRequestVariable:I}" : (FormattableString)$"null", + GetValueFactory(pageItemType), + clientDiagnosticsReference.GetReferenceFormattable(), + pipelineReference.GetReferenceFormattable() + }; + + createPageableParameters.AddTrailingPageableParameters(methodParameters, scopeName, itemPropertyName, nextLinkPropertyName); + + if (firstPageRequestVariable != null) + { + writer.Line($"{Configuration.ApiTypes.HttpMessageType} {firstPageRequestVariable:D}({KnownParameters.PageSizeHint.Type} {KnownParameters.PageSizeHint.Name}) => {firstPageRequest};"); + } + + if (nextPageRequestVariable != null) + { + writer.Line($"{Configuration.ApiTypes.HttpMessageType} {nextPageRequestVariable:D}({KnownParameters.PageSizeHint.Type} {KnownParameters.PageSizeHint.Name}, {KnownParameters.NextLink.Type} {KnownParameters.NextLink.Name}) => {nextPageRequest};"); + } + + return writer.Line($"return {typeof(GeneratorPageableHelpers)}.{(async ? nameof(GeneratorPageableHelpers.CreateAsyncPageable) : nameof(GeneratorPageableHelpers.CreatePageable))}({createPageableParameters.Join(", ")});"); + } + + private static void AddTrailingPageableParameters(this List createPageableParameters, IReadOnlyCollection methodParameters, string scopeName, string? itemPropertyName, string? nextLinkPropertyName) + { + createPageableParameters.Add($"{scopeName:L}"); + createPageableParameters.Add($"{itemPropertyName:L}"); + createPageableParameters.Add($"{nextLinkPropertyName:L}"); + + if (ContainsRequestContext(methodParameters)) + { + createPageableParameters.Add($"{KnownParameters.RequestContext.Name:I}"); + } + else if (methodParameters.Contains(KnownParameters.CancellationTokenParameter)) + { + createPageableParameters.Add($"{KnownParameters.CancellationTokenParameter.Name:I}"); + } + } + + private static void EnsureRequestContextVariable(this CodeWriter writer, List parameters, RestClientMethod? createFirstPageRequestMethod, RestClientMethod? createNextPageRequestMethod) + { + if (ContainsRequestContext(parameters)) + { + return; + } + + if (!ContainsRequestContext(createFirstPageRequestMethod?.Parameters) && !ContainsRequestContext(createNextPageRequestMethod?.Parameters)) + { + return; + } + + var requestContextVariable = new CodeWriterDeclaration(KnownParameters.RequestContext.Name); + if (parameters.Contains(KnownParameters.CancellationTokenParameter)) + { + writer.Line($"{KnownParameters.RequestContext.Type} {requestContextVariable:D} = {KnownParameters.CancellationTokenParameter.Name:I}.{nameof(CancellationToken.CanBeCanceled)} ? new {KnownParameters.RequestContext.Type} {{ {Configuration.ApiTypes.CancellationTokenName} = {KnownParameters.CancellationTokenParameter.Name:I} }} : null;"); + } + else + { + writer.Line($"{KnownParameters.RequestContext.Type} {requestContextVariable:D} = null;"); + } + + parameters.Add(KnownParameters.RequestContext); + + } + + private static bool ContainsRequestContext(IReadOnlyCollection? parameters) => + parameters != null && (parameters.Contains(KnownParameters.RequestContext) || parameters.Contains(KnownParameters.RequestContextRequired)); + + private static FormattableString? GetCreateRequestCall(Reference? restClientReference, RestClientMethod? createRequestMethod, IReadOnlyList? ProtocolToConvenienceParameterConverters = null) + { + if (createRequestMethod == null) + { + return null; + } + + var methodName = RequestWriterHelpers.CreateRequestMethodName(createRequestMethod); + if (restClientReference != null) + { + return $"{restClientReference.Value.GetReferenceFormattable()}.{methodName}({createRequestMethod.Parameters.GetIdentifiersFormattable()})"; + } + + if (ProtocolToConvenienceParameterConverters == null) + { + return $"{methodName}({createRequestMethod.Parameters.GetIdentifiersFormattable()})"; + } + + var parameters = new List(); + foreach (var parameter in createRequestMethod.Parameters) + { + if (parameter == KnownParameters.RequestContext || parameter == KnownParameters.RequestContextRequired || parameter.Name == "nextLink" || parameter.Type.EqualsIgnoreNullable(Configuration.ApiTypes.RequestContentType)) + { + parameters.Add($"{parameter.Name}"); + continue; + } + + var convenienceParameter = ProtocolToConvenienceParameterConverters.FirstOrDefault(convert => convert.Convenience?.Name == parameter.Name)?.Convenience; + if (convenienceParameter == null) + { + throw new InvalidOperationException($"Cannot find corresponding convenience parameter for create request method parameter {parameter.Name}."); + } + + parameters.Add(convenienceParameter.GetConversionFormattable(parameter.Type, null)); + } + return $"{methodName}({parameters.Join(" ,")})"; + } + + private static FormattableString GetValueFactory(CSharpType? pageItemType) + { + if (pageItemType is null) + { + throw new NotSupportedException("Type of the element of the page must be specified"); + } + + if (pageItemType.Equals(BinaryDataType)) + { + // When `JsonElement` provides access to its UTF8 buffer, change this code to create `BinaryData` from it. + // See also GeneratorPageableHelpers.ParseResponseForBinaryData + return $"e => {BinaryDataType}.{nameof(BinaryData.FromString)}(e.{nameof(JsonElement.GetRawText)}())"; + } + + if (pageItemType is { IsFrameworkType: false, Implementation: SerializableObjectType { Serialization.Json: { } } type }) + { + // TODO -- we no longer need this once we remove the UseModelReaderWriter flag + if (Configuration.UseModelReaderWriter) + return $"e => {type.Type}.Deserialize{type.Declaration.Name}(e)"; + else + return $"{type.Type}.Deserialize{type.Declaration.Name}"; + } + + var deserializeImplementation = JsonCodeWriterExtensions.GetDeserializeValueFormattable($"e", pageItemType); + return $"e => {deserializeImplementation}"; + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/PropertyInitializer.cs b/logger/autorest.csharp/common/Generation/Writers/PropertyInitializer.cs new file mode 100644 index 0000000..cf2b326 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/PropertyInitializer.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal readonly struct PropertyInitializer + { + public PropertyInitializer(string name, CSharpType type, bool isReadOnly, FormattableString value, CSharpType? valueType = null) + { + Name = name; + Type = type; + IsReadOnly = isReadOnly; + Value = value; + ValueType = valueType ?? type; + } + + public string Name { get; } + public CSharpType Type { get; } + public FormattableString Value { get; } + public CSharpType ValueType { get; } + public bool IsReadOnly { get; } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/RequestWriterHelpers.cs b/logger/autorest.csharp/common/Generation/Writers/RequestWriterHelpers.cs new file mode 100644 index 0000000..483e1e8 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/RequestWriterHelpers.cs @@ -0,0 +1,579 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using Request = Azure.Core.Request; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class RequestWriterHelpers + { + public static void WriteRequestAndUriCreation(CodeWriter writer, RestClientMethod clientMethod, string methodAccessibility, ClientFields fields, string? responseClassifierType, bool writeSDKUserAgent, IReadOnlyList? clientParameters = null) + { + WriteUriCreation(writer, clientMethod, methodAccessibility, fields, clientParameters); + WriteRequestCreation(writer, clientMethod, fields, responseClassifierType, writeSDKUserAgent, clientParameters); + } + + public static void WriteRequestCreation(CodeWriter writer, RestClientMethod clientMethod, ClientFields fields, string? responseClassifierType, bool writeSDKUserAgent, IReadOnlyList? clientParameters = null) + { + using var methodScope = writer.AmbientScope(); + var parameters = clientMethod.Parameters; + + var methodName = CreateRequestMethodName(clientMethod.Name); + writer.Append($"internal {Configuration.ApiTypes.HttpMessageType} {methodName}("); + foreach (Parameter clientParameter in parameters) + { + writer.Append($"{clientParameter.Type} {clientParameter.Name:D},"); + } + writer.RemoveTrailingComma(); + writer.Line($")"); + using (writer.Scope()) + { + var message = new CodeWriterDeclaration("message"); + var request = new CodeWriterDeclaration("request"); + var uri = new CodeWriterDeclaration("uri"); + + if (Configuration.IsBranded) + { + if (clientMethod.Parameters.Contains(KnownParameters.RequestContext)) + { + writer.Append($"var {message:D} = {Configuration.ApiTypes.GetHttpPipelineCreateMessageFormat(true)}"); + if (responseClassifierType != default) + { + writer.Append($", {responseClassifierType}"); + } + writer.Line($");"); + } + else + { + writer.Line($"var {message:D} = {Configuration.ApiTypes.GetHttpPipelineCreateMessageFormat(false)});"); + } + } + else + { + writer.Line($"var {message:D} = _pipeline.CreateMessage();"); + writer.Line($"{message}.ResponseClassifier = {responseClassifierType};"); + } + + writer.Line($"var {request:D} = {message}.{Configuration.ApiTypes.HttpMessageRequestName};"); + + var method = clientMethod.Request.HttpMethod; + if (!clientMethod.BufferResponse) + { + writer.Line($"{message}.BufferResponse = false;"); + } + writer.Line(Configuration.ApiTypes.GetSetMethodString(request.ActualName, method.Method)); + + writer.Line($"var {uri:D} = new {Configuration.ApiTypes.RequestUriType}();"); + foreach (var segment in clientMethod.Request.PathSegments) + { + var value = GetFieldReference(fields, segment.Value); + if (value.Type.IsFrameworkType && value.Type.FrameworkType == typeof(Uri)) + { + writer.Append($"{uri}.Reset("); + WriteConstantOrParameter(writer, value, enumAsString: !segment.IsRaw); + writer.Line($");"); + } + else if (!value.IsConstant && value.Reference.Name == "nextLink") + { + WritePathSegment(writer, uri, segment, value, "AppendRawNextLink"); + } + else + { + WritePathSegment(writer, uri, segment, value); + } + } + + //TODO: Duplicate code between query and header parameter processing logic + foreach (var queryParameter in clientMethod.Request.Query) + { + WriteQueryParameter(writer, uri, queryParameter, fields, clientParameters); + } + + writer.Line(Configuration.ApiTypes.GetSetUriString(request.ActualName, uri.ActualName)); + + WriteHeaders(writer, clientMethod, request, content: false, fields); + + switch (clientMethod.Request.Body) + { + case RequestContentRequestBody body: + WriteHeaders(writer, clientMethod, request, content: true, fields); + writer.Line(Configuration.ApiTypes.GetSetContentString(request.ActualName, body.Parameter.Name)); + break; + case SchemaRequestBody body: + using (WriteValueNullCheck(writer, body.Value)) + { + WriteHeaders(writer, clientMethod, request, content: true, fields); + WriteSerializeContent(writer, request, body.Serialization, GetConstantOrParameter(body.Value, ignoreNullability: true, convertBinaryDataToArray: false)); + } + + break; + case BinaryRequestBody binaryBody: + using (WriteValueNullCheck(writer, binaryBody.Value)) + { + WriteHeaders(writer, clientMethod, request, content: true, fields); + writer.Append($"{request}.Content = {Configuration.ApiTypes.RequestContentType}.{Configuration.ApiTypes.RequestContentCreateName}("); + WriteConstantOrParameter(writer, binaryBody.Value); + writer.Line($");"); + } + break; + case TextRequestBody textBody: + using (WriteValueNullCheck(writer, textBody.Value)) + { + WriteHeaders(writer, clientMethod, request, content: true, fields); + writer.Append($"{request}.Content = new {typeof(StringRequestContent)}("); + WriteConstantOrParameter(writer, textBody.Value); + writer.Line($");"); + } + break; + case MultipartRequestBody multipartRequestBody: + WriteHeaders(writer, clientMethod, request, content: true, fields); + + var multipartContent = new CodeWriterDeclaration("content"); + writer.Line($"var {multipartContent:D} = new {typeof(MultipartFormDataContent)}();"); + + foreach (var bodyParameter in multipartRequestBody.RequestBodyParts) + { + switch (bodyParameter.Content) + { + case BinaryRequestBody binaryBody: + using (WriteValueNullCheck(writer, binaryBody.Value)) + { + writer.Append($"{multipartContent}.Add({Configuration.ApiTypes.RequestContentType}.{Configuration.ApiTypes.RequestContentCreateName}("); + WriteConstantOrParameter(writer, binaryBody.Value); + writer.Line($"), {bodyParameter.Name:L}, null);"); + } + break; + case TextRequestBody textBody: + using (WriteValueNullCheck(writer, textBody.Value)) + { + writer.Append($"{multipartContent}.Add(new {typeof(StringRequestContent)}("); + WriteConstantOrParameter(writer, textBody.Value); + writer.Line($"), {bodyParameter.Name:L}, null);"); + } + break; + case BinaryCollectionRequestBody collectionBody: + var collectionItemVariable = new CodeWriterDeclaration("value"); + using (writer.Scope($"foreach (var {collectionItemVariable:D} in {collectionBody.Value.Reference.Name})")) + { + writer.Append($"{multipartContent}.Add({Configuration.ApiTypes.RequestContentType}.{Configuration.ApiTypes.RequestContentCreateName}({collectionItemVariable}), {bodyParameter.Name:L}, null);"); + } + break; + default: + throw new NotImplementedException(bodyParameter.Content?.GetType().FullName); + } + } + writer.Line($"{multipartContent}.ApplyToRequest({request});"); + break; + case FlattenedSchemaRequestBody flattenedSchemaRequestBody: + WriteHeaders(writer, clientMethod, request, content: true, fields); + + var initializers = new List(); + foreach (var initializer in flattenedSchemaRequestBody.Initializers) + { + initializers.Add(new PropertyInitializer(initializer.Property.Declaration.Name, initializer.Property.Declaration.Type, initializer.Property.IsReadOnly, initializer.Value.GetReferenceOrConstantFormattable(), initializer.Value.Type)); + } + var modelVariable = new CodeWriterDeclaration("model"); + writer.WriteInitialization( + v => writer.Line($"var {modelVariable:D} = {v};"), + flattenedSchemaRequestBody.ObjectType, + flattenedSchemaRequestBody.ObjectType.InitializationConstructor, + initializers); + + WriteSerializeContent(writer, request, flattenedSchemaRequestBody.Serialization, $"{modelVariable:I}"); + break; + case UrlEncodedBody urlEncodedRequestBody: + var urlContent = new CodeWriterDeclaration("content"); + + WriteHeaders(writer, clientMethod, request, content: true, fields); + writer.Line($"var {urlContent:D} = new {typeof(FormUrlEncodedContent)}();"); + + foreach (var (name, value) in urlEncodedRequestBody.Values) + { + using (WriteValueNullCheck(writer, value)) + { + writer.Append($"{urlContent}.Add({name:L},"); + WriteConstantOrParameterAsString(writer, value); + writer.Line($");"); + } + } + writer.Line($"{request}.Content = {urlContent};"); + break; + case null: + break; + default: + throw new NotImplementedException(clientMethod.Request.Body?.GetType().FullName); + } + + if (writeSDKUserAgent) + { + writer.Line($"_userAgent.Apply({message});"); + } + + // we need to apply the RequestOptions in non-branded case as a last step + if (!Configuration.IsBranded && clientMethod.Parameters.Contains(KnownParameters.RequestContext)) + { + writer.Line($"{message}.Apply({KnownParameters.RequestContext.Name:I});"); + } + + writer.Line($"return {message};"); + } + writer.Line(); + } + + private static void WriteUriCreation(CodeWriter writer, RestClientMethod clientMethod, string methodAccessibility, ClientFields? fields, IReadOnlyList? clientParameters = null) + { + using var methodScope = writer.AmbientScope(); + var methodName = CreateRequestUriMethodName($"{clientMethod.Name}"); + writer.Append($"{methodAccessibility} {typeof(RequestUriBuilder)} {methodName}("); + foreach (Parameter clientParameter in clientMethod.Parameters) + { + writer.Append($"{clientParameter.Type} {clientParameter.Name:D},"); + } + writer.RemoveTrailingComma(); + writer.Line($")"); + using (writer.Scope()) + { + var uri = new CodeWriterDeclaration("uri"); + writer.Line($"var {uri:D} = new RawRequestUriBuilder();"); + foreach (var segment in clientMethod.Request.PathSegments) + { + var value = GetFieldReference(fields, segment.Value); + if (value.Type.IsFrameworkType && value.Type.FrameworkType == typeof(Uri)) + { + writer.Append($"{uri}.Reset("); + WriteConstantOrParameter(writer, value, enumAsString: !segment.IsRaw); + writer.Line($");"); + } + else if (!value.IsConstant && value.Reference.Name == "nextLink") + { + WritePathSegment(writer, uri, segment, value, "AppendRawNextLink"); + } + else + { + WritePathSegment(writer, uri, segment, value); + } + } + + //TODO: Duplicate code between query and header parameter processing logic + foreach (var queryParameter in clientMethod.Request.Query) + { + WriteQueryParameter(writer, uri, queryParameter, fields, clientParameters); + } + writer.Line($"return {uri};"); + } + writer.Line(); + } + + private static ReferenceOrConstant GetFieldReference(ClientFields? fields, ReferenceOrConstant value) => + fields != null && !value.IsConstant ? fields.GetFieldByParameter(value.Reference.Name, value.Reference.Type) ?? value : value; + + private static ReferenceOrConstant GetReferenceForQueryParameter(ClientFields? fields, QueryParameter parameter) + { + var value = parameter.Value; + if (fields is null || + value.IsConstant is true || + (value.Reference.Name == "apiVersion" && parameter.IsApiVersion is false))// strictly check api-version parameter name + { + return parameter.Value; + } + + return fields.GetFieldByParameter(value.Reference.Name, value.Reference.Type) ?? value; + } + + public static void WriteHeaders(CodeWriter writer, RestClientMethod clientMethod, CodeWriterDeclaration request, bool content, ClientFields fields) + { + foreach (var header in clientMethod.Request.Headers) + { + if (header.IsContentHeader == content) + { + Configuration.ApiTypes.WriteHeaderMethod(writer, request, header, fields); + } + } + } + + public static string CreateRequestMethodName(RestClientMethod method) => CreateRequestMethodName(method.Name); + public static string CreateRequestMethodName(string name) => $"Create{name}Request"; + public static string CreateRequestUriMethodName(string name) => $"Create{name}RequestUri"; + + private static void WriteSerializeContent(CodeWriter writer, CodeWriterDeclaration request, ObjectSerialization bodySerialization, FormattableString value) + { + GetRequestContentForSerialization(request, bodySerialization, value).Write(writer); + } + + private static MethodBodyStatement GetRequestContentForSerialization(CodeWriterDeclaration request, ObjectSerialization serialization, FormattableString value) + { + var valueExpression = new FormattableStringToExpression(value); + var requestExpression = new VariableReference(typeof(Request), request); + + return serialization switch + { + JsonSerialization jsonSerialization => new[] + { + Extensible.RestOperations.DeclareContentWithUtf8JsonWriter(out var utf8JsonContent, out var writer), + JsonSerializationMethodsBuilder.SerializeExpression(writer, jsonSerialization, new TypedValueExpression(jsonSerialization.Type, valueExpression), ModelReaderWriterOptionsExpression.Wire), + Assign(requestExpression.Property(nameof(Request.Content)), utf8JsonContent) + }, + + XmlElementSerialization xmlSerialization => new[] + { + Extensible.RestOperations.DeclareContentWithXmlWriter(out var utf8JsonContent, out var writer), + XmlSerializationMethodsBuilder.SerializeExpression(writer, xmlSerialization, valueExpression), + Assign(requestExpression.Property(nameof(Request.Content)), utf8JsonContent) + }, + + _ => throw new NotImplementedException() + }; + } + + internal static void WriteHeader(CodeWriter writer, CodeWriterDeclaration request, RequestHeader header, ClientFields fields) + { + string? delimiter = header.Delimiter; + string method = delimiter != null + ? nameof(RequestHeaderExtensions.AddDelimited) + : nameof(RequestHeaderExtensions.Add); + + var value = GetFieldReference(fields, header.Value); + using (WriteValueNullCheck(writer, value)) + { + if (value.Type.Equals(typeof(MatchConditions)) || value.Type.Equals(typeof(RequestConditions))) + { + writer.Append($"{request}.Headers.{method}("); + } + else + { + writer.Append($"{request}.Headers.{method}({header.Name:L}, "); + } + + if (value.Type.Equals(typeof(ContentType))) + { + WriteConstantOrParameterAsString(writer, value); + } + else + { + WriteConstantOrParameter(writer, value, enumAsString: true); + } + + if (delimiter != null) + { + writer.Append($", {delimiter:L}"); + } + WriteSerializationFormat(writer, header.Format); + writer.Line($");"); + } + } + + internal static void WriteHeaderSystem(CodeWriter writer, CodeWriterDeclaration request, RequestHeader header, ClientFields fields) + { + var value = GetFieldReference(fields, header.Value); + using (WriteValueNullCheck(writer, value)) + { + writer.Append($"{request}.Headers.Set({header.Name:L}, "); + WriteConstantOrParameter(writer, value, enumAsString: true); + var formatSpecifier = header.Format.ToFormatSpecifier(); + if (formatSpecifier != null) + { + writer.Append($".ToString({formatSpecifier:L})"); + } + writer.Line($");"); + } + } + + private static void WritePathSegment(CodeWriter writer, CodeWriterDeclaration uri, PathSegment segment, ReferenceOrConstant value, string? methodName = null) + { + methodName ??= segment.IsRaw ? "AppendRaw" : "AppendPath"; + writer.Append($"{uri}.{methodName}("); + WriteConstantOrParameter(writer, value, enumAsString: !segment.IsRaw || value.Type is { IsFrameworkType: false, Implementation: EnumType }); + if (!Configuration.IsBranded) + { + if (value.Type.IsFrameworkType && value.Type.FrameworkType != typeof(string)) + { + writer.Append($".ToString("); + WriteSerializationFormat(writer, segment.Format); + writer.AppendRaw(")"); + } + } + else + { + WriteSerializationFormat(writer, segment.Format); + } + writer.Line($", {segment.Escape:L});"); + } + + private static void WriteConstantOrParameterAsString(CodeWriter writer, ReferenceOrConstant constantOrReference) + { + WriteConstantOrParameter(writer, constantOrReference, enumAsString: true); + if (constantOrReference.Type.IsFrameworkType && constantOrReference.Type.FrameworkType != typeof(string)) + { + writer.Append($".ToString()"); + } + } + + private static void WriteConstantOrParameter(CodeWriter writer, ReferenceOrConstant constantOrReference, bool ignoreNullability = false, bool enumAsString = false) + { + writer.Append(GetConstantOrParameter(constantOrReference, ignoreNullability)); + + if (enumAsString && + !constantOrReference.Type.IsFrameworkType && + constantOrReference.Type.Implementation is EnumType enumType) + { + writer.AppendEnumToString(enumType); + } + } + + private static FormattableString GetConstantOrParameter(ReferenceOrConstant constantOrReference, bool ignoreNullability = false, bool convertBinaryDataToArray = true) + { + if (constantOrReference.IsConstant) + { + return constantOrReference.Constant.GetConstantFormattable(!Configuration.IsBranded); + } + + if (!ignoreNullability && constantOrReference.Type.IsNullable && constantOrReference.Type.IsValueType) + { + return $"{constantOrReference.Reference.Name:I}.Value"; + } + + if (constantOrReference.Type.Equals(typeof(BinaryData)) && convertBinaryDataToArray) + { + return $"{constantOrReference.Reference.Name:I}.ToArray()"; + } + + return $"{constantOrReference.Reference.Name:I}"; + } + + private static void WriteSerializationFormat(CodeWriter writer, SerializationFormat format) + { + var formatSpecifier = format.ToFormatSpecifier(); + if (formatSpecifier != null) + { + if (Configuration.IsBranded) + { + writer.Append($", {formatSpecifier:L}"); + } + else + { + writer.Append($"{formatSpecifier:L}"); + } + } + } + + private static void WriteQueryParameter(CodeWriter writer, CodeWriterDeclaration uri, QueryParameter queryParameter, ClientFields? fields, IReadOnlyList? parameters) + { + string? delimiter = queryParameter.Delimiter; + bool explode = queryParameter.Explode; + string method = delimiter != null && !explode + ? nameof(RequestUriBuilderExtensions.AppendQueryDelimited) + : nameof(RequestUriBuilderExtensions.AppendQuery); + + var value = GetReferenceForQueryParameter(fields, queryParameter); + var parameter = parameters != null && queryParameter.Name == "api-version" ? parameters.FirstOrDefault(p => p.Name == "apiVersion") : null; + using (parameter != null && parameter.IsOptionalInSignature ? null : WriteValueNullCheck(writer, value, checkUndefinedCollection: true)) + { + if (explode) + { + var paramVariable = new CodeWriterDeclaration("param"); + writer.Append($"foreach(var {paramVariable:D} in "); + WriteConstantOrParameter(writer, value, enumAsString: true); + writer.Line($")"); + using (writer.Scope()) + { + writer.Append($"{uri}.{method}({queryParameter.Name:L}, "); + WriteConstantOrParameter(writer, new Reference(paramVariable.ActualName, value.Type.IsGenericType ? value.Type.Arguments[0] : value.Type), enumAsString: true); + WriteSerializationFormat(writer, queryParameter.SerializationFormat); + writer.Line($", {queryParameter.Escape:L});"); + } + } + else + { + writer.Append($"{uri}.{method}({queryParameter.Name:L}, "); + WriteConstantOrParameter(writer, value, enumAsString: true); + if (delimiter != null) + { + writer.Append($", {delimiter:L}"); + } + WriteSerializationFormat(writer, queryParameter.SerializationFormat); + writer.Line($", {queryParameter.Escape:L});"); + } + } + } + + private static CodeWriter.CodeWriterScope? WriteValueNullCheck(CodeWriter writer, ReferenceOrConstant value, bool checkUndefinedCollection = false) + { + if (value.IsConstant) + return default; + + var type = value.Type; + string valueStr = GetValueExpression(writer, value); + ValueExpression valueExpression = new FormattableStringToExpression($"{valueStr}"); + CodeWriterDeclaration changeTrackingList = new CodeWriterDeclaration("changeTrackingList"); + if (checkUndefinedCollection && type.IsCollection) + { + writer.Append($"if ("); + + valueExpression.Write(writer); + + writer.Append($" != null && !("); + valueExpression.Write(writer); + writer.Append($" is {ChangeTrackingListProvider.Instance.Type.MakeGenericType(type.Arguments)} {changeTrackingList:D} && {changeTrackingList}.IsUndefined)"); + + return writer.LineRaw(")").Scope(); + } + else if (type.IsNullable) + { + writer.Append($"if ("); + + valueExpression.Write(writer); + + return writer.Line($" != null)").Scope(); + } + + return default; + } + + private static string GetValueExpression(CodeWriter writer, ReferenceOrConstant value) + { + // turn "object.Property" into "object?.Property" + StringBuilder builder = new StringBuilder(); + var parts = value.Reference.Name.Split("."); + bool first = true; + foreach (var part in parts) + { + if (first) + { + first = false; + } + else + { + builder.Append("?."); + } + + if (StringExtensions.IsCSharpKeyword(part)) + { + builder.Append("@"); + } + builder.Append(part); + } + return builder.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/ResponseWriterHelpers.cs b/logger/autorest.csharp/common/Generation/Writers/ResponseWriterHelpers.cs new file mode 100644 index 0000000..bfcec84 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/ResponseWriterHelpers.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal static class ResponseWriterHelpers + { + public static void WriteRawResponseToGeneric(CodeWriter writer, RestClientMethod operation, Output.Models.Responses.Response response, bool async, string? messageVariableName, FormattableString responseVariable) + { + var returnType = operation.ReturnType; + var headersModelType = operation.HeaderModel?.Type; + + ReturnKind kind; + + if (returnType != null && headersModelType != null) + { + kind = ReturnKind.HeadersAndValue; + } + else if (headersModelType != null) + { + kind = ReturnKind.Headers; + } + else if (returnType != null) + { + kind = ReturnKind.Value; + } + else + { + kind = ReturnKind.Response; + } + + var responseBody = response.ResponseBody; + ReferenceOrConstant value = default; + + var valueVariable = new CodeWriterDeclaration("value"); + switch (responseBody) + { + case ObjectResponseBody objectResponseBody: + writer.Line($"{responseBody.Type} {valueVariable:D} = default;"); + writer.WriteDeserializationForMethods(objectResponseBody.Serialization, async, new VariableReference(responseBody.Type, valueVariable), $"{responseVariable}.{Configuration.ApiTypes.ContentStreamName}", objectResponseBody.Type); + value = new Reference(valueVariable.ActualName, responseBody.Type); + break; + case StreamResponseBody _: + writer.Line($"var {valueVariable:D} = {messageVariableName}.ExtractResponseContent();"); + value = new Reference(valueVariable.ActualName, responseBody.Type); + break; + case ConstantResponseBody body: + writer.Append($"{returnType} {valueVariable:D} = "); + writer.WriteReferenceOrConstant(body.Value); + writer.Line($";"); + value = new Reference(valueVariable.ActualName, responseBody.Type); + break; + case StringResponseBody _: + var streamReaderVariable = new CodeWriterDeclaration("streamReader"); + writer.Line($"{typeof(StreamReader)} {streamReaderVariable:D} = new {typeof(StreamReader)}({responseVariable}.{Configuration.ApiTypes.ContentStreamName});"); + writer.Append($"{returnType} {valueVariable:D} = "); + if (async) + { + writer.Line($"await {streamReaderVariable}.ReadToEndAsync().ConfigureAwait(false);"); + } + else + { + writer.Line($"{streamReaderVariable}.ReadToEnd();"); + } + value = new Reference(valueVariable.ActualName, responseBody.Type); + break; + default: + { + if (returnType != null) + { + value = Constant.Default(returnType.WithNullable(true)); + } + + break; + } + } + + switch (kind) + { + case ReturnKind.Response: + writer.Append($"return {responseVariable};"); + break; + case ReturnKind.Headers: + writer.Append($"return {typeof(ResponseWithHeaders)}.FromValue(headers, {responseVariable});"); + break; + case ReturnKind.HeadersAndValue: + writer.Append($"return {typeof(ResponseWithHeaders)}.FromValue"); + if (!Equals(responseBody?.Type, operation.ReturnType)) + { + writer.Append($"<{operation.ReturnType}, {headersModelType}>"); + } + writer.Append($"("); + writer.WriteReferenceOrConstant(value); + writer.Append($", headers, {responseVariable});"); + break; + case ReturnKind.Value: + writer.Append($"return {Configuration.ApiTypes.ResponseType}.FromValue"); + if (!Equals(responseBody?.Type, operation.ReturnType)) + { + writer.Append($"<{operation.ReturnType}>"); + } + writer.Append($"("); + writer.WriteReferenceOrConstant(value); + writer.Append($", {responseVariable});"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static void WriteStatusCodeSwitch(CodeWriter writer, string messageVariableName, RestClientMethod operation, bool async, FieldDeclaration? clientDiagnosticsField) + { + FormattableString responseVariable = $"{messageVariableName}.Response"; + + var returnType = operation.ReturnType; + var headersModelType = operation.HeaderModel?.Type; + + if (headersModelType != null) + { + writer.Line($"var headers = new {headersModelType}({responseVariable});"); + } + + using (writer.Scope($"switch ({responseVariable}.Status)")) + { + foreach (var response in operation.Responses) + { + var responseBody = response.ResponseBody; + var statusCodes = response.StatusCodes; + + foreach (var statusCode in statusCodes) + { + if (statusCode.Code != null) + { + writer.Line($"case {statusCode.Code}:"); + } + else + { + writer.Line($"case int s when s >= {statusCode.Family * 100:L} && s < {statusCode.Family * 100 + 100:L}:"); + } + } + + using (responseBody != null ? writer.Scope() : default) + { + WriteRawResponseToGeneric(writer, operation, response, async, messageVariableName, responseVariable); + } + } + + writer + .Line($"default:") + .Line($"throw new {Configuration.ApiTypes.RequestFailedExceptionType}({responseVariable});"); + } + } + + private enum ReturnKind + { + Response, + Headers, + HeadersAndValue, + Value + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/RestClientWriter.cs b/logger/autorest.csharp/common/Generation/Writers/RestClientWriter.cs new file mode 100644 index 0000000..681303e --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/RestClientWriter.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Responses; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class RestClientWriter + { + internal delegate void WriteFuncBody(CodeWriter writer, CodeWriterDeclaration messageVariable, RestClientMethod operation, string pipelineName, bool async); + internal delegate void WriteStatusCodeImplementation(CodeWriter writer, CodeWriterDeclaration messageVariable, RestClientMethod operation, bool async, FieldDeclaration fieldDeclaration); + + public void WriteClient(CodeWriter writer, DataPlaneRestClient restClient) + { + using (writer.Namespace(restClient.Type.Namespace)) + { + using (writer.Scope($"{restClient.Declaration.Accessibility} partial class {restClient.Type:D}", scopeDeclarations: restClient.Fields.ScopeDeclarations)) + { + var responseClassifierTypes = new List(); + writer.WriteFieldDeclarations(restClient.Fields); + WriteClientCtor(writer, restClient); + foreach (var method in restClient.Methods) + { + RequestWriterHelpers.WriteRequestCreation(writer, method, restClient.Fields, null, false, restClient.Parameters); + WriteOperation(writer, method, restClient.Fields.PipelineField.Name, true, WriteFuncBodyWithSend, null, MethodSignatureModifiers.Public, WriteStatusCodeSwitch, restClient.Fields.GetFieldByParameter(KnownParameters.ClientDiagnostics)); + WriteOperation(writer, method, restClient.Fields.PipelineField.Name, false, WriteFuncBodyWithSend, null, MethodSignatureModifiers.Public, WriteStatusCodeSwitch, restClient.Fields.GetFieldByParameter(KnownParameters.ClientDiagnostics)); + var protocolMethod = restClient.ProtocolMethods.FirstOrDefault(m => m.RequestMethod.Operation.Equals(method.Operation)); + if (protocolMethod != null) + { + DpgClientWriter.WriteProtocolMethods(writer, restClient.Fields, protocolMethod); + responseClassifierTypes.Add(protocolMethod.RequestMethod.ResponseClassifierType); + } + } + + DpgClientWriter.WriteResponseClassifierMethod(writer, responseClassifierTypes); + } + } + } + + private static void WriteClientCtor(CodeWriter writer, DataPlaneRestClient restClient) + { + var constructor = restClient.Constructor; + writer.WriteMethodDocumentation(constructor); + using (writer.WriteMethodDeclaration(constructor)) + { + foreach (Parameter clientParameter in constructor.Parameters) + { + var field = restClient.Fields.GetFieldByParameter(clientParameter); + if (field != null) + { + writer.WriteVariableAssignmentWithNullCheck($"{field.Name}", clientParameter); + } + } + } + writer.Line(); + } + + public static void WriteOperation(CodeWriter writer, RestClientMethod operation, string pipelineName, bool async, WriteFuncBody writeFuncBody, string? methodName = null, MethodSignatureModifiers modifiers = MethodSignatureModifiers.Public, WriteStatusCodeImplementation? writeStatusCodeImplementation = null, FieldDeclaration? fieldDeclaration = null) + { + using var methodScope = writer.AmbientScope(); + + CSharpType? bodyType = operation.ReturnType; + CSharpType? headerModelType = operation.HeaderModel?.Type; + CSharpType returnType = bodyType switch + { + null when headerModelType != null => new CSharpType(typeof(ResponseWithHeaders<>), headerModelType), + { } when headerModelType == null => new CSharpType(Configuration.ApiTypes.ResponseOfTType, bodyType), + { } => new CSharpType(typeof(ResponseWithHeaders<,>), bodyType, headerModelType), + _ => new CSharpType(Configuration.ApiTypes.ResponseType), + }; + + var parameters = operation.Parameters.Where(p => p.Name != KnownParameters.RequestContext.Name).Append(KnownParameters.CancellationTokenParameter).ToArray(); + var method = new MethodSignature($"{methodName ?? operation.Name}", FormattableStringHelpers.FromString(operation.Summary), FormattableStringHelpers.FromString(operation.Description), modifiers, returnType, null, parameters).WithAsync(async); + + writer.WriteXmlDocumentationSummary(method.SummaryText) + .WriteXmlDocumentationParameters(method.Parameters) + .WriteXmlDocumentationRequiredParametersException(method.Parameters) + .WriteXmlDocumentation("remarks", method.DescriptionText); + + if (method.ReturnDescription != null) + { + writer.WriteXmlDocumentationReturns(method.ReturnDescription); + } + using (writer.WriteMethodDeclaration(method)) + { + writer.WriteParameterNullChecks(parameters); + var messageVariable = new CodeWriterDeclaration("message"); + writeFuncBody(writer, messageVariable, operation, pipelineName, async); + + if (writeStatusCodeImplementation != null && fieldDeclaration != null) + { + writeStatusCodeImplementation(writer, messageVariable, operation, async, fieldDeclaration); + } + } + writer.Line(); + } + + private void WriteFuncBodyWithSend(CodeWriter writer, CodeWriterDeclaration messageVariable, RestClientMethod operation, string pipelineName, bool async) + { + var requestMethodName = RequestWriterHelpers.CreateRequestMethodName(operation.Name); + + writer + .Line($"using var {messageVariable:D} = {requestMethodName}({operation.Parameters.GetIdentifiersFormattable()});") + .WriteEnableHttpRedirectIfNecessary(operation, new VariableReference(Configuration.ApiTypes.HttpMessageType, messageVariable)) + .WriteMethodCall(async, $"{pipelineName}.SendAsync", $"{pipelineName}.Send", $"{messageVariable}, {KnownParameters.CancellationTokenParameter.Name}"); + } + + private void WriteStatusCodeSwitch(CodeWriter writer, CodeWriterDeclaration messageVariable, RestClientMethod operation, bool async, FieldDeclaration clientDiagnosticsField) + { + ResponseWriterHelpers.WriteStatusCodeSwitch(writer, $"{messageVariable.ActualName}", operation, async, clientDiagnosticsField); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/SerializationWriter.cs b/logger/autorest.csharp/common/Generation/Writers/SerializationWriter.cs new file mode 100644 index 0000000..5736ba8 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/SerializationWriter.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Linq; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Common.Output.Builders; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class SerializationWriter + { + public void WriteSerialization(CodeWriter writer, TypeProvider schema) + { + switch (schema) + { + // The corresponding ResoruceData does not exist for PartialResource, so we do not need to generate serialization methods for it. + case PartialResource: + break; + case Resource resource: + WriteResourceJsonSerialization(writer, resource); + break; + case SerializableObjectType obj: + if (obj.IncludeSerializer || obj.IncludeDeserializer) + { + WriteObjectSerialization(writer, obj); + } + break; + case EnumType { IsExtensible: false } sealedChoiceSchema: + WriteEnumSerialization(writer, sealedChoiceSchema); + break; + } + } + + private void WriteResourceJsonSerialization(CodeWriter writer, Resource resource) + { + var declaration = resource.Declaration; + + using (writer.Namespace(declaration.Namespace)) + { + var resourceDataType = resource.ResourceData.Type; + writer.Append($"{declaration.Accessibility} partial class {declaration.Name} : IJsonModel<{resourceDataType}>"); + + using (writer.Scope()) + { + foreach (var method in JsonSerializationMethodsBuilder.BuildResourceJsonSerializationMethods(resource)) + { + writer.WriteMethod(method); + } + } + } + } + + private void WriteObjectSerialization(CodeWriter writer, SerializableObjectType model) + { + var declaration = model.Declaration; + var serialization = model.Serialization; + + if (!serialization.HasSerializations) + { + return; + } + using (writer.Namespace(declaration.Namespace)) + { + var converter = serialization.Json?.JsonConverter; + if (converter is not null) + { + // this is an inner class therefore we directly write its name here instead of writing its type + writer.Append($"[{typeof(JsonConverter)}(typeof({converter.Declaration.Name}))]"); + } + // write the serialization proxy attribute if the model has one + if (serialization.PersistableModelProxyType is { } proxyType) + { + writer.Append($"[{typeof(PersistableModelProxyAttribute)}(typeof({proxyType}))]"); + } + + writer.Append($"{declaration.Accessibility} partial {(model.IsStruct ? "struct" : "class")} {declaration.Name}") + .AppendRawIf(" : ", serialization.Interfaces?.Any() ?? false); + + if (serialization.Interfaces is not null) + { + foreach (var i in serialization.Interfaces) + { + writer.Append($"{i}, "); + } + } + + writer.RemoveTrailingComma(); + + writer.Line(); + using (writer.Scope()) + { + foreach (var method in model.Methods) + { + writer.WriteMethodDocumentation(method.Signature); + writer.WriteMethod(method); + } + + if (converter is not null) + { + WriteCustomJsonConverter(writer, converter); + } + } + } + } + + private static void WriteCustomJsonConverter(CodeWriter writer, JsonConverterProvider converter) + { + writer.Append($"{converter.Declaration.Accessibility} partial class {converter.Declaration.Name} : {converter.Inherits!}"); + using (writer.Scope()) + { + foreach (var method in converter.Methods) + { + writer.WriteMethod(method); + } + } + } + + public static void WriteEnumSerialization(CodeWriter writer, EnumType enumType) + { + using (writer.Namespace(enumType.Declaration.Namespace)) + { + string declaredTypeName = enumType.Declaration.Name; + + var isString = enumType.ValueType.FrameworkType == typeof(string); + + using (writer.Scope($"internal static partial class {declaredTypeName}Extensions")) + { + if (!enumType.IsIntValueType) + { + WriteEnumSerializationMethod(writer, enumType, declaredTypeName); + } + + WriteEnumDeserializationMethod(writer, enumType, declaredTypeName, isString); + } + } + } + + private static void WriteEnumSerializationMethod(CodeWriter writer, EnumType enumType, string declaredTypeName) + { + using (writer.Scope($"public static {enumType.ValueType} {enumType.SerializationMethodName}(this {declaredTypeName} value) => value switch", end: "};")) + { + foreach (EnumTypeValue value in enumType.Values) + { + writer.Line($"{declaredTypeName}.{value.Declaration.Name} => {value.Value.Value:L},"); + } + + writer.Line($"_ => throw new {typeof(ArgumentOutOfRangeException)}(nameof(value), value, \"Unknown {declaredTypeName} value.\")"); + } + writer.Line(); + } + + private static void WriteEnumDeserializationMethod(CodeWriter writer, EnumType schema, string declaredTypeName, bool isString) + { + using (writer.Scope($"public static {declaredTypeName} To{declaredTypeName}(this {schema.ValueType} value)")) + { + if (isString) + { + foreach (EnumTypeValue value in schema.Values) + { + if (value.Value.Value is string strValue && strValue.All(char.IsAscii)) + { + writer.Append($"if ({typeof(StringComparer)}.{nameof(StringComparer.OrdinalIgnoreCase)}.{nameof(StringComparer.Equals)}(value, {strValue:L}))"); + } + else + { + writer.Append($"if ({schema.ValueType}.Equals(value, {value.Value.Value:L}"); + writer.Append($", {typeof(StringComparison)}.InvariantCultureIgnoreCase))"); + } + writer.Line($" return {declaredTypeName}.{value.Declaration.Name};"); + } + } + else// int, and float + { + foreach (EnumTypeValue value in schema.Values) + { + writer.Line($"if (value == {value.Value.Value:L}) return {declaredTypeName}.{value.Declaration.Name};"); + } + } + + writer.Line($"throw new {typeof(ArgumentOutOfRangeException)}(nameof(value), value, \"Unknown {declaredTypeName} value.\");"); + } + writer.Line(); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/WirePathWriter.cs b/logger/autorest.csharp/common/Generation/Writers/WirePathWriter.cs new file mode 100644 index 0000000..a012db8 --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/WirePathWriter.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Input; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class WirePathWriter + { + private CodeWriter _writer; + + public WirePathWriter() + { + _writer = new CodeWriter(); + } + + public void Write() + { + using (_writer.Namespace(Configuration.Namespace)) + { + _writer.Line($"[{typeof(AttributeUsageAttribute)}({typeof(AttributeTargets)}.{nameof(AttributeTargets.Property)})]"); + using (_writer.Scope($"internal class WirePathAttribute : {typeof(Attribute)}")) + { + _writer.Line($"private string _wirePath;"); + _writer.Line(); + using (_writer.Scope($"public WirePathAttribute(string wirePath)")) + { + _writer.Line($"_wirePath = wirePath;"); + } + _writer.Line(); + using (_writer.Scope($"public override string ToString()")) + { + _writer.Line($"return _wirePath;"); + } + } + } + } + + public override string ToString() + { + return _writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Generation/Writers/XmlDocWriter.cs b/logger/autorest.csharp/common/Generation/Writers/XmlDocWriter.cs new file mode 100644 index 0000000..b38358b --- /dev/null +++ b/logger/autorest.csharp/common/Generation/Writers/XmlDocWriter.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace AutoRest.CSharp.Generation.Writers +{ + /// + /// Writer to compose the content of .Net external XML doc which can be included + /// throught "// <include>" tag. + /// For details, see: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d36-include + /// + internal class XmlDocWriter + { + private readonly XElement _membersElement; + + public XmlDocWriter(string filename) + { + _membersElement = new XElement("members"); + Document = new XDocument( + new XElement("doc", _membersElement) + ); + Filename = filename; + } + + public string Filename { get; } + + public IEnumerable Members => _membersElement.Elements("member"); + + public XDocument Document { get; } + + private XElement? _lastMember = null; + + public XmlDocWriter AddMember(string docRef) + { + _lastMember = new XElement("member", new XAttribute("name", docRef)); + _membersElement.Add(_lastMember); + + return this; + } + + public XmlDocWriter AddExamples(IEnumerable<(string ExampleInformation, string TestMethodName)> examples) + { + if (_lastMember != null && examples.Any()) + { + var exampleElement = new XElement("example"); + foreach (var example in examples) + { + exampleElement.Add( + Environment.NewLine, + example.ExampleInformation, + Environment.NewLine, + new XElement("code", example.TestMethodName)); + } + + _lastMember.Add(exampleElement); + } + + return this; + } + + public override string ToString() + { + var writer = new XmlStringWriter(); + XmlWriterSettings settings = new XmlWriterSettings { OmitXmlDeclaration = false, Indent = true }; + using (XmlWriter xw = XmlWriter.Create(writer, settings)) + { + Document.Save(xw); + } + + return writer.ToString(); + } + + private class XmlStringWriter : StringWriter + { + public override Encoding Encoding => Encoding.UTF8; + } + } +} diff --git a/logger/autorest.csharp/common/Input/ApiTypes.cs b/logger/autorest.csharp/common/Input/ApiTypes.cs new file mode 100644 index 0000000..74e24d3 --- /dev/null +++ b/logger/autorest.csharp/common/Input/ApiTypes.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Output.Expressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Input +{ + internal abstract class ApiTypes + { + public abstract Type ResponseType { get; } + public abstract Type ResponseOfTType { get; } + + public string FromResponseName => "FromResponse"; + public abstract string ResponseParameterName { get; } + public abstract string ContentStreamName { get; } + public abstract string StatusName { get; } + + public abstract string GetRawResponseName { get; } + + public Type GetResponseOfT() => ResponseOfTType.MakeGenericType(typeof(T)); + public Type GetTaskOfResponse(Type? valueType = default) => + valueType is null ? typeof(Task<>).MakeGenericType(ResponseType) : typeof(Task<>).MakeGenericType(ResponseOfTType.MakeGenericType(valueType)); + public Type GetValueTaskOfResponse(Type? valueType = default) => + valueType is null ? typeof(ValueTask<>).MakeGenericType(ResponseType) : typeof(ValueTask<>).MakeGenericType(ResponseOfTType.MakeGenericType(valueType)); + + public abstract Type HttpPipelineType { get; } + public abstract CSharpType PipelineExtensionsType { get; } + public abstract string HttpPipelineCreateMessageName { get; } + public FormattableString GetHttpPipelineCreateMessageFormat(bool withContext) + { + FormattableString context = withContext ? (FormattableString)$"{KnownParameters.RequestContext.Name:I}" : $""; + return $"_pipeline.{Configuration.ApiTypes.HttpPipelineCreateMessageName}({context}"; + } + + public abstract FormattableString CredentialDescription { get; } + + public abstract Type HttpMessageType { get; } + public abstract string HttpMessageResponseName { get; } + public string HttpMessageResponseStatusName => nameof(PipelineResponse.Status); + + public Type GetNextPageFuncType() => typeof(Func<,,>).MakeGenericType(typeof(int?), typeof(string), HttpMessageType); + + public abstract Type ClientOptionsType { get; } + + public abstract Type RequestContextType { get; } + public abstract string RequestContextName { get; } + public abstract string RequestContextDescription { get; } + public string CancellationTokenName = nameof(RequestOptions.CancellationToken); + + public abstract Type HttpPipelineBuilderType { get; } + public abstract Type BearerAuthenticationPolicyType { get; } + public abstract Type KeyCredentialPolicyType { get; } + public abstract Type KeyCredentialType { get; } + public FormattableString GetHttpPipelineBearerString(string pipelineField, string optionsVariable, string credentialVariable, CodeWriterDeclaration? scopesParam) + => $"{pipelineField} = {HttpPipelineBuilderType}.Build({optionsVariable}, new {BearerAuthenticationPolicyType}({credentialVariable}, {scopesParam}));"; + public FormattableString GetHttpPipelineKeyCredentialString(string pipelineField, string optionsVariable, string credentialVariable, string keyName) + => $"{pipelineField} = {HttpPipelineBuilderType}.Build({optionsVariable}, new {KeyCredentialPolicyType}({credentialVariable}, \"{keyName}\"));"; + public abstract FormattableString GetHttpPipelineClassifierString(string pipelineField, string optionsVariable, FormattableString perCallPolicies, FormattableString perRetryPolicies, FormattableString beforeTransportPolicies); + + public abstract Type HttpPipelinePolicyType { get; } + public abstract string HttpMessageRequestName { get; } + + public abstract FormattableString GetSetMethodString(string requestName, string method); + public abstract FormattableString GetSetUriString(string requestName, string uriName); + + public abstract Action WriteHeaderMethod { get; } + + public abstract FormattableString GetSetContentString(string requestName, string contentName); + + public abstract CSharpType RequestUriType { get; } + public abstract Type RequestContentType { get; } + public abstract string ToRequestContentName { get; } + public abstract string MultipartRequestContentTypeName { get; } + public abstract string ToMultipartRequestContentName { get; } + public abstract string RequestContentCreateName { get; } + + public abstract Type IXmlSerializableType { get; } + + public abstract Type RequestFailedExceptionType { get; } + + public abstract string ResponseClassifierIsErrorResponseName { get; } + + public abstract string EndPointSampleValue { get; } + + public abstract string JsonElementVariableName { get; } + public abstract Type ResponseClassifierType { get; } + public abstract Type StatusCodeClassifierType { get; } + + public abstract ValueExpression GetCreateFromStreamSampleExpression(ValueExpression freeFormObjectExpression); + + public abstract ValueExpression GetKeySampleExpression(string clientName); + + public abstract ExtensibleSnippets ExtensibleSnippets { get; } + + public abstract string LicenseString { get; } + } +} diff --git a/logger/autorest.csharp/common/Input/AzureApiTypes.cs b/logger/autorest.csharp/common/Input/AzureApiTypes.cs new file mode 100644 index 0000000..5d5ffce --- /dev/null +++ b/logger/autorest.csharp/common/Input/AzureApiTypes.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions; +using AutoRest.CSharp.Common.Output.Expressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Types.HelperTypeProviders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Common.Input +{ + internal class AzureApiTypes : ApiTypes + { + public override Type ResponseType => typeof(Response); + public override Type ResponseOfTType => typeof(Response<>); + public override string ResponseParameterName => "response"; + public override string ContentStreamName => nameof(Response.ContentStream); + public override string StatusName => nameof(Response.Status); + public override string GetRawResponseName => nameof(Response.GetRawResponse); + + public override Type HttpPipelineType => typeof(HttpPipeline); + public override CSharpType PipelineExtensionsType => typeof(HttpPipelineExtensions); + public override string HttpPipelineCreateMessageName => nameof(HttpPipeline.CreateMessage); + + public override Type HttpMessageType => typeof(HttpMessage); + public override string HttpMessageResponseName => nameof(HttpMessage.Response); + + public override Type ClientOptionsType => typeof(ClientOptions); + + public override Type RequestContextType => typeof(RequestContext); + + public override string RequestContextName => "context"; + public override string RequestContextDescription => "The request context, which can override default behaviors of the client pipeline on a per-call basis."; + + public override Type BearerAuthenticationPolicyType => typeof(BearerTokenAuthenticationPolicy); + public override Type KeyCredentialType => typeof(AzureKeyCredential); + public override Type HttpPipelineBuilderType => typeof(HttpPipelineBuilder); + public override Type KeyCredentialPolicyType => typeof(AzureKeyCredentialPolicy); + public override FormattableString GetHttpPipelineClassifierString(string pipelineField, string optionsVariable, FormattableString perCallPolicies, FormattableString perRetryPolicies, FormattableString beforeTransportPolicies) + => $"{pipelineField:I} = {typeof(HttpPipelineBuilder)}.{nameof(HttpPipelineBuilder.Build)}({optionsVariable:I}, {perCallPolicies}, {perRetryPolicies}, new {Configuration.ApiTypes.ResponseClassifierType}());"; + + public override FormattableString CredentialDescription => $"A credential used to authenticate to an Azure Service."; + + public override Type HttpPipelinePolicyType => typeof(HttpPipelinePolicy); + public override string HttpMessageRequestName => nameof(HttpMessage.Request); + + public override FormattableString GetSetMethodString(string requestName, string method) + => $"{requestName}.Method = {typeof(RequestMethod)}.{RequestMethod.Parse(method).ToRequestMethodName()};"; + public override FormattableString GetSetUriString(string requestName, string uriName) + => $"{requestName}.Uri = {uriName};"; + + public override Action WriteHeaderMethod => RequestWriterHelpers.WriteHeader; + + public override FormattableString GetSetContentString(string requestName, string contentName) + => $"{requestName}.Content = {contentName};"; + + public override CSharpType RequestUriType => typeof(RawRequestUriBuilder); + public override Type RequestContentType => typeof(RequestContent); + public override string ToRequestContentName => "ToRequestContent"; + public override string MultipartRequestContentTypeName => "MultipartFormDataRequestContent"; + public override string ToMultipartRequestContentName => "ToMultipartRequestContent"; + public override string RequestContentCreateName => nameof(RequestContent.Create); + + public override Type IXmlSerializableType => typeof(IXmlSerializable); + + public override Type RequestFailedExceptionType => typeof(RequestFailedException); + + public override Type ResponseClassifierType => typeof(ResponseClassifier); + public override Type StatusCodeClassifierType => typeof(StatusCodeClassifier); + + public override ValueExpression GetCreateFromStreamSampleExpression(ValueExpression freeFormObjectExpression) + => new InvokeStaticMethodExpression(Configuration.ApiTypes.RequestContentType, Configuration.ApiTypes.RequestContentCreateName, new[]{freeFormObjectExpression}); + + public override string EndPointSampleValue => ""; + + public override string JsonElementVariableName => "result"; + + public override ValueExpression GetKeySampleExpression(string clientName) + => new StringLiteralExpression("", false); + + public override ExtensibleSnippets ExtensibleSnippets { get; } = new AzureExtensibleSnippets(); + + public override string LicenseString => Configuration.CustomHeader ?? """ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +"""; + + public override string ResponseClassifierIsErrorResponseName => nameof(ResponseClassifier.IsErrorResponse); + } +} diff --git a/logger/autorest.csharp/common/Input/CodeModelConverter.cs b/logger/autorest.csharp/common/Input/CodeModelConverter.cs new file mode 100644 index 0000000..cf16a4d --- /dev/null +++ b/logger/autorest.csharp/common/Input/CodeModelConverter.cs @@ -0,0 +1,936 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Utilities; +using Azure.Core.Expressions.DataFactory; + +namespace AutoRest.CSharp.Common.Input +{ + internal class CodeModelConverter + { + private readonly CodeModel _codeModel; + private readonly SchemaUsageProvider _schemaUsageProvider; + private readonly Dictionary> _operationsCache; + private readonly Dictionary> _parametersCache; + private readonly Dictionary _enumsCache; + private readonly Dictionary _enumsNamingCache; + private readonly Dictionary _modelsCache; + private readonly Dictionary Properties, IReadOnlyList CompositSchemas)> _modelPropertiesCache; + private readonly Dictionary DerivedModels, Dictionary DiscriminatedSubtypes)> _derivedModelsCache; + private readonly Dictionary _propertiesCache; + + public CodeModelConverter(CodeModel codeModel, SchemaUsageProvider schemaUsageProvider) + { + _codeModel = codeModel; + _schemaUsageProvider = schemaUsageProvider; + _enumsCache = new Dictionary(); + _enumsNamingCache = new Dictionary(); + _operationsCache = new Dictionary>(); + _parametersCache = new Dictionary>(); + _modelsCache = new Dictionary(); + _modelPropertiesCache = new Dictionary Properties, IReadOnlyList CompositSchemas)>(); + _derivedModelsCache = new Dictionary DerivedModels, Dictionary DiscriminatedSubtypes)>(); + _propertiesCache = new Dictionary(); + } + + public InputNamespace CreateNamespace() => CreateNamespace(null, null); + + public (InputNamespace Namespace, IReadOnlyDictionary ServiceRequestToInputOperation, IReadOnlyDictionary InputOperationToOperation) CreateNamespaceWithMaps() + { + var serviceRequestToInputOperation = new Dictionary(); + var inputOperationToOperation = new Dictionary(); + var inputNamespace = CreateNamespace(serviceRequestToInputOperation, inputOperationToOperation); + return (inputNamespace, serviceRequestToInputOperation, inputOperationToOperation); + } + + private InputNamespace CreateNamespace(Dictionary? serviceRequestToInputOperation, Dictionary? inputOperationToOperation) + { + CreateEnums(); + var models = CreateModels(); + var clients = CreateClients(_codeModel.OperationGroups, serviceRequestToInputOperation, inputOperationToOperation); + + return new(Name: _codeModel.Language.Default.Name, + Clients: clients, + Models: models, + Enums: _enumsCache.Values.ToArray(), + ApiVersions: GetApiVersions(), + Auth: GetAuth(_codeModel.Security.Schemes.OfType())); + } + + private IReadOnlyList CreateClients(IEnumerable operationGroups, Dictionary? serviceRequestToInputOperation, Dictionary? inputOperationToOperation) + => operationGroups.Select(operationGroup => CreateClient(operationGroup, serviceRequestToInputOperation, inputOperationToOperation)).ToList(); + + private InputClient CreateClient(OperationGroup operationGroup, Dictionary? serviceRequestToInputOperation, Dictionary? inputOperationToOperation) + => new( + Name: operationGroup.Language.Default.Name, + Description: operationGroup.Language.Default.Description, + Operations: CreateOperations(operationGroup.Operations, serviceRequestToInputOperation, inputOperationToOperation), + Parameters: Array.Empty(), + Parent: null) + { + Key = operationGroup.Key, + }; + + private IReadOnlyList CreateOperations(ICollection operations, Dictionary? serviceRequestToInputOperation, Dictionary? inputOperationToOperation) + { + var serviceRequests = new List(); + var inputOperations = new List(); + foreach (var operation in operations) + { + foreach (var serviceRequest in operation.Requests) + { + if (serviceRequest.Protocol.Http is not HttpRequest httpRequest) + { + continue; + } + + serviceRequests.Add(serviceRequest); + _operationsCache.CreateAndCacheResult(serviceRequest, () => CreateOperation(serviceRequest, operation, httpRequest)); + + if (operation.RequestMediaTypes != null && !Configuration.Generation1ConvenienceClient) + { + break; + } + } + } + + foreach (var serviceRequest in serviceRequests) + { + var inputOperation = _operationsCache[serviceRequest](); + inputOperations.Add(inputOperation); + if (serviceRequestToInputOperation is not null) + { + serviceRequestToInputOperation[serviceRequest] = inputOperation; + } + } + + if (serviceRequestToInputOperation is not null && inputOperationToOperation is not null) + { + foreach (var operation in operations) + { + foreach (var serviceRequest in operation.Requests) + { + if (serviceRequestToInputOperation.TryGetValue(serviceRequest, out var inputOperation)) + { + inputOperationToOperation[inputOperation] = operation; + } + } + } + } + + return inputOperations; + } + + private InputOperation CreateOperation(ServiceRequest serviceRequest, Operation operation, HttpRequest httpRequest) + { + var parameters = CreateOperationParameters(operation.Parameters.Concat(serviceRequest.Parameters).ToList()); + var operationId = operation.OperationId; + var inputOperation = new InputOperation( + name: operation.Language.Default.Name, + // Keep the behavior for non-mgmt scenarios + resourceName: Configuration.AzureArm ? GetResoureName(operationId) : null, + summary: operation.Language.Default.Summary, + deprecated: operation.Deprecated?.Reason, + description: operation.Language.Default.Description, + accessibility: operation.Accessibility, + parameters: parameters, + responses: operation.Responses.Select(CreateOperationResponse).ToList(), + httpMethod: httpRequest.Method.ToCoreRequestMethod(), + requestBodyMediaType: GetBodyFormat((httpRequest as HttpWithBodyRequest)?.KnownMediaType), + uri: httpRequest.Uri, + path: httpRequest.Path, + externalDocsUrl: operation.ExternalDocs?.Url, + requestMediaTypes: operation.RequestMediaTypes?.Keys.ToList(), + bufferResponse: operation.Extensions?.BufferResponse ?? true, + longRunning: CreateLongRunning(operation), + paging: CreateOperationPaging(serviceRequest, operation), + generateProtocolMethod: true, + generateConvenienceMethod: false, + crossLanguageDefinitionId: string.Empty, // in typespec input, this is to determine whether an operation has been renamed. We have separated configuration for that in swagger input, therefore we leave it empty here + keepClientDefaultValue: operationId is null ? false : Configuration.MethodsToKeepClientDefaultValue.Contains(operationId), + examples: CreateOperationExamplesFromTestModeler(operation)) + { + SpecName = operation.Language.Default.SerializedName ?? operation.Language.Default.Name + }; + return inputOperation; + } + + private static string? GetResoureName(string? operationId) + { + if (operationId is null || operationId.IndexOf("_") == -1) + { + return null; + } + return operationId.Split('_')[0]; + } + + private IReadOnlyList? CreateOperationExamplesFromTestModeler(Operation operation) + { + if (!Configuration.AzureArm) + return null; + + var result = new List(); + var exampleOperation = _codeModel.TestModel?.MockTest.ExampleGroups?.FirstOrDefault(g => g.Operation == operation); + if (exampleOperation is null) + { + return result; + } + foreach (var example in exampleOperation.Examples) + { + var parameters = example.AllParameters + .Select(p => new InputParameterExample(_parametersCache[p.Parameter](), CreateExampleValue(p.ExampleValue))) + .ToList(); + result.Add(new InputOperationExample(example.Name, null, example.OriginalFile!, parameters)); + } + return result; + } + + private InputExampleValue CreateExampleValue(ExampleValue exampleValue) + { + var inputType = CreateType(exampleValue.Schema, exampleValue.Schema.Extensions?.Format, false); + if (exampleValue.RawValue != null) + { + // test modeler has a bug that all the primitive types are deserialized as strings, here we should convert them according to its real type + return new InputExampleRawValue(inputType, ConvertRawValue(inputType, exampleValue.RawValue)); + } + if (exampleValue.Elements != null && exampleValue.Elements.Any()) + { + return new InputExampleListValue(inputType, exampleValue.Elements.Select(CreateExampleValue).ToList()); + } + if (exampleValue.Properties is null) + { + return InputExampleValue.Null(inputType); + } + else + { + return new InputExampleObjectValue(inputType, exampleValue.Properties.ToDictionary(p => p.Key, p => CreateExampleValue(p.Value))); + } + } + + private static object? ConvertRawValue(InputType inputType, object? originalValue) + { + if (originalValue == null) + { + return null; + } + + if (originalValue is string strValue) + { + var effectiveInputType = inputType switch + { + InputEnumType enumType => enumType.ValueType, + InputDurationType durationType => durationType.WireType, + InputDateTimeType dateTimeType => dateTimeType.WireType, + _ => inputType + }; + switch (effectiveInputType) + { + case InputPrimitiveType { Kind: InputPrimitiveTypeKind.Int32 or InputPrimitiveTypeKind.UInt32 }: + return int.TryParse(strValue, out var intValue) ? intValue : default(int); + case InputPrimitiveType { Kind: InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.UInt64 or InputPrimitiveTypeKind.Integer }: + return long.TryParse(strValue, out var longValue) ? longValue : default(long); + case InputPrimitiveType { Kind: InputPrimitiveTypeKind.Float32 or InputPrimitiveTypeKind.Float64 or InputPrimitiveTypeKind.Float }: + return double.TryParse(strValue, out var doubleValue) ? doubleValue : default(double); + case InputPrimitiveType { Kind: InputPrimitiveTypeKind.Decimal or InputPrimitiveTypeKind.Decimal128 }: + return decimal.TryParse(strValue, out var decimalValue) ? decimalValue : default(decimal); + case InputPrimitiveType { Kind: InputPrimitiveTypeKind.Boolean }: + return bool.TryParse(strValue, out var boolValue) ? boolValue : default(bool); + default: + return strValue; + } + } + + return originalValue; + } + + private List CreateOperationParameters(IReadOnlyCollection requestParameters) + { + foreach (var parameter in requestParameters) + { + _parametersCache.CreateAndCacheResult(parameter, () => CreateOperationParameter(parameter)); + } + + return requestParameters.Select(rp => _parametersCache[rp]()).ToList(); + } + + private InputParameter CreateOperationParameter(RequestParameter input) => new( + Name: input.Language.Default.Name, + NameInRequest: input.Language.Default.SerializedName ?? input.Language.Default.Name, + Description: input.Language.Default.Description, + Type: GetOrCreateType(input), + Location: GetRequestLocation(input), + DefaultValue: GetDefaultValue(input), + IsRequired: input.IsRequired, + GroupedBy: input.GroupedBy is { } groupedBy ? _parametersCache[groupedBy]() : null, + Kind: GetOperationParameterKind(input), + IsApiVersion: input.IsApiVersion, + IsResourceParameter: Convert.ToBoolean(input.Extensions.GetValue("x-ms-resource-identifier")), + IsContentType: input.Origin == "modelerfour:synthesized/content-type", + IsEndpoint: input.Origin == "modelerfour:synthesized/host", + ArraySerializationDelimiter: GetArraySerializationDelimiter(input), + Explode: input.Protocol.Http is HttpParameter { Explode: true }, + SkipUrlEncoding: input.Extensions?.SkipEncoding ?? false, + HeaderCollectionPrefix: input.Extensions?.HeaderCollectionPrefix, + FlattenedBodyProperty: input is VirtualParameter vp and ({ Schema: not ConstantSchema } or { Required: not true }) + ? GetOrCreateProperty(vp.TargetProperty) + : null + ); + + private OperationResponse CreateOperationResponse(ServiceResponse response) => new( + StatusCodes: response.HttpResponse.IntStatusCodes.ToList(), + BodyType: GetResponseBodyType(response), + BodyMediaType: GetBodyFormat(response.HttpResponse.KnownMediaType), + Headers: GetResponseHeaders(response.HttpResponse.Headers), + IsErrorResponse: false, + ContentTypes: Array.Empty() + ); + + private OperationResponseHeader CreateResponseHeader(HttpResponseHeader header) => new( + Name: header.CSharpName(), + NameInResponse: header.Extensions?.HeaderCollectionPrefix ?? header.Header, + Description: header.Language.Default.Description, + Type: GetOrCreateType(header.Schema, header.Extensions?.Format, true) + ); + + private OperationLongRunning? CreateLongRunning(Operation operation) + { + if (!operation.IsLongRunning) + { + return null; + } + + return new OperationLongRunning( + FinalStateVia: operation.LongRunningFinalStateVia, + FinalResponse: CreateOperationResponse(operation.LongRunningFinalResponse), + ResultPath: null + ); + } + + private OperationPaging? CreateOperationPaging(ServiceRequest serviceRequest, Operation operation) + { + var paging = operation.Language.Default.Paging; + if (paging == null) + { + return null; + } + + var nextLinkServiceRequest = paging.NextLinkOperation?.Requests.Single(); + if (nextLinkServiceRequest != null && nextLinkServiceRequest != serviceRequest && _operationsCache.TryGetValue(nextLinkServiceRequest, out var nextLinkOperationRef)) + { + return new OperationPaging(NextLinkName: paging.NextLinkName, ItemName: paging.ItemName, nextLinkOperationRef(), false); + } + + return new OperationPaging(NextLinkName: paging.NextLinkName, ItemName: paging.ItemName, null, nextLinkServiceRequest == serviceRequest); + } + + private void CreateEnums() + { + foreach (var choiceSchema in _codeModel.Schemas.Choices) + { + var enumType = CreateEnumType(choiceSchema, choiceSchema.ChoiceType, choiceSchema.Choices, true); + _enumsCache[choiceSchema] = enumType; + _enumsNamingCache[choiceSchema.Language.Default.Name] = enumType; + } + + foreach (var sealedChoiceSchema in _codeModel.Schemas.SealedChoices) + { + var enumType = CreateEnumType(sealedChoiceSchema, sealedChoiceSchema.ChoiceType, sealedChoiceSchema.Choices, false); + _enumsCache[sealedChoiceSchema] = enumType; + _enumsNamingCache[sealedChoiceSchema.Language.Default.Name] = enumType; + } + } + + private IReadOnlyList CreateModels() + { + var schemas = _codeModel.Schemas.Objects.Concat(_codeModel.Schemas.Groups).ToList(); + + foreach (var schema in schemas) + { + GetOrCreateModel(schema); + } + + foreach (var (schema, (properties, compositionSchemas)) in _modelPropertiesCache) + { + properties.AddRange(schema.Properties.Select(GetOrCreateProperty)); + properties.AddRange(compositionSchemas.SelectMany(EnumerateBase).SelectMany(x => x.Properties).Select(GetOrCreateProperty)); + } + + foreach (var schema in schemas) + { + var derived = schema.Children?.Immediate.OfType().Select(s => _modelsCache[s]); + if (derived != null) + { + _derivedModelsCache[schema].DerivedModels.AddRange(derived); + } + if (schema.Discriminator != null && schema.Children != null) + { + foreach (var child in schema.Children.Immediate.OfType()) + { + if (child.DiscriminatorValue != null) + { + // TODO -- change it to the throw exception version when https://github.com/Azure/azure-rest-api-specs/issues/29320 and https://github.com/Azure/azure-rest-api-specs/issues/29321 are resolved + //_derivedModelsCache[schema].DiscriminatedSubtypes.Add(child.DiscriminatorValue, GetOrCreateModel(child)); + _derivedModelsCache[schema].DiscriminatedSubtypes[child.DiscriminatorValue] = GetOrCreateModel(child); + } + } + } + } + + return schemas.Select(s => _modelsCache[s]).ToList(); + } + + private static IEnumerable EnumerateBase(ObjectSchema? schema) + { + while (schema != null) + { + yield return schema; + schema = GetBaseModelSchema(schema); + } + } + + private InputModelType GetOrCreateModel(ObjectSchema schema) + { + if (_modelsCache.TryGetValue(schema, out var model)) + { + return model; + } + + var usage = _schemaUsageProvider.GetUsage(schema); + var properties = new List(); + var derived = new List(); + var discriminatedSubtypes = new Dictionary(); + var baseModelSchema = GetBaseModelSchema(schema); + var baseModel = baseModelSchema is not null ? GetOrCreateModel(baseModelSchema) : null; + IReadOnlyList compositeSchemas = schema.Parents?.Immediate?.OfType().Where(s => s != baseModelSchema).ToArray() ?? Array.Empty(); + var dictionarySchema = schema.Parents?.Immediate?.OfType().FirstOrDefault(); + + model = new InputModelType( + Name: schema.Language.Default.Name, + CrossLanguageDefinitionId: GetCrossLanguageDefinitionId(schema), + Access: schema.Extensions?.Accessibility ?? (usage.HasFlag(SchemaTypeUsage.Model) ? "public" : "internal"), + Deprecation: schema.Deprecated?.Reason, + Description: schema.CreateDescription(), + Usage: (schema.Extensions != null && schema.Extensions.Formats.Contains("multipart/form-data") ? InputModelTypeUsage.MultipartFormData : InputModelTypeUsage.None) + | GetUsage(usage), + Properties: properties, + BaseModel: baseModel, + DerivedModels: derived, + DiscriminatorValue: schema.DiscriminatorValue, + DiscriminatorProperty: schema.Discriminator?.Property is { } discriminatorProperty ? GetOrCreateProperty(discriminatorProperty) : null, + DiscriminatedSubtypes: discriminatedSubtypes, + AdditionalProperties: dictionarySchema is not null ? GetOrCreateType(dictionarySchema.ElementType, false) : null) + { + Serialization = GetSerialization(schema), + UseSystemTextJsonConverter = usage.HasFlag(SchemaTypeUsage.Converter), + SpecName = schema.Language.Default.SerializedName ?? schema.Language.Default.Name + }; + + _modelsCache[schema] = model; + _modelPropertiesCache[schema] = (properties, compositeSchemas); + _derivedModelsCache[schema] = (derived, discriminatedSubtypes); + + return model; + } + + private static InputModelTypeUsage GetUsage(SchemaTypeUsage usage) + { + var result = InputModelTypeUsage.None; + if (usage.HasFlag(SchemaTypeUsage.Input)) + { + result |= InputModelTypeUsage.Input; + } + if (usage.HasFlag(SchemaTypeUsage.Output)) + { + result |= InputModelTypeUsage.Output; + } + return result; + } + + private static ObjectSchema? GetBaseModelSchema(ObjectSchema schema) + => schema.Parents?.Immediate is { } parents + ? parents.OfType().FirstOrDefault(s => s.Discriminator is not null) ?? parents.OfType().FirstOrDefault() + : null; + + private InputModelProperty GetOrCreateProperty(Property property) + { + if (_propertiesCache.TryGetValue(property, out var inputProperty)) + { + return inputProperty; + } + inputProperty = new( + Name: property.Language.Default.Name, + SerializedName: property.SerializedName, + Description: property.Language.Default.Description, + Type: GetOrCreateType(property), + ConstantValue: property.Schema is ConstantSchema constantSchema ? CreateConstant(constantSchema, constantSchema.Extensions?.Format, property.IsNullable) : null, + IsRequired: property.IsRequired, + IsReadOnly: property.IsReadOnly, + IsDiscriminator: property.IsDiscriminator ?? false) + { + FlattenedNames = property.FlattenedNames.ToList(), + }; + _propertiesCache.Add(property, inputProperty); + + return inputProperty; + } + + private static InputOperationParameterKind GetOperationParameterKind(RequestParameter input) => input switch + { + { Implementation: ImplementationLocation.Client } => InputOperationParameterKind.Client, + + // only required constant parameter can be Constant kind which will be optimized to disappear from method signature + { Schema: ConstantSchema, IsRequired: true } => InputOperationParameterKind.Constant, + + // Grouped and flattened parameters shouldn't be added to methods + { IsFlattened: true } => InputOperationParameterKind.Flattened, + { GroupedBy: not null } => InputOperationParameterKind.Grouped, + _ => InputOperationParameterKind.Method + }; + + private static InputTypeSerialization GetSerialization(Schema schema) + { + var formats = schema is ObjectSchema objectSchema ? objectSchema.SerializationFormats : new List { KnownMediaType.Json, KnownMediaType.Xml }; + if (Configuration.SkipSerializationFormatXml) + { + formats.Remove(KnownMediaType.Xml); + } + + if (schema.Extensions != null) + { + foreach (var format in schema.Extensions.Formats) + { + formats.Add(Enum.Parse(format, true)); + } + } + + var xmlSerialization = schema.Serialization?.Xml; + var jsonFormat = formats.Contains(KnownMediaType.Json); + var xmlFormat = formats.Contains(KnownMediaType.Xml) + ? new InputTypeXmlSerialization(xmlSerialization?.Name, xmlSerialization?.Attribute == true, xmlSerialization?.Text == true, xmlSerialization?.Wrapped == true) + : null; + + return new InputTypeSerialization(jsonFormat, xmlFormat); + } + + private static string? GetArraySerializationDelimiter(RequestParameter input) => input.In switch + { + HttpParameterIn.Query or HttpParameterIn.Header => (input.Protocol.Http as HttpParameter)?.Style switch + { + SerializationStyle.PipeDelimited => "|", + SerializationStyle.TabDelimited => "\t", + SerializationStyle.SpaceDelimited => " ", + null or SerializationStyle.Form or SerializationStyle.Simple => input.Schema switch + { + ArraySchema or ConstantSchema { ValueType: ArraySchema } => ",", + _ => null + }, + _ => throw new ArgumentOutOfRangeException() + }, + _ => null + }; + + private static BodyMediaType GetBodyFormat(KnownMediaType? knownMediaType) => knownMediaType switch + { + KnownMediaType.Binary => BodyMediaType.Binary, + KnownMediaType.Form => BodyMediaType.Form, + KnownMediaType.Json => BodyMediaType.Json, + KnownMediaType.Multipart => BodyMediaType.Multipart, + KnownMediaType.Text => BodyMediaType.Text, + KnownMediaType.Xml => BodyMediaType.Xml, + _ => BodyMediaType.None + }; + + private InputType? GetResponseBodyType(ServiceResponse response) => response switch + { + { HttpResponse.KnownMediaType: KnownMediaType.Text } => InputPrimitiveType.String, + BinaryResponse => InputPrimitiveType.Stream, + SchemaResponse schemaResponse => GetOrCreateType(schemaResponse.Schema, schemaResponse.IsNullable), + _ => null + }; + + private IReadOnlyList GetResponseHeaders(ICollection? headers) + { + if (headers == null) + { + return Array.Empty(); + } + return headers.Select(CreateResponseHeader).ToList(); + } + + private InputType GetOrCreateType(RequestParameter requestParameter) + { + var schema = requestParameter is { Schema: ConstantSchema constantSchema } + ? constantSchema.ValueType + : requestParameter.Schema; + + return GetOrCreateType(schema, requestParameter.Extensions?.Format, Configuration.AzureArm ? requestParameter.IsNullable : requestParameter.IsNullable || !requestParameter.IsRequired); + } + + private InputType GetOrCreateType(Property property) + { + var name = property.Schema.Name; + object? elementType = null; + if ((property.Schema is AnyObjectSchema || property.Schema is StringSchema) && true == property.Extensions?.TryGetValue("x-ms-format-element-type", out elementType)) + { + return ToDataFactoryElementType(name, property.Extensions?.Format, property.IsNullable, (Schema)elementType!) ?? GetOrCreateType(property.Schema, property.Schema.Extensions?.Format, property.IsNullable); + } + else + { + return GetOrCreateType(property.Schema, GetPropertyFormat(property), property.IsNullable); + } + } + + private string? GetPropertyFormat(Property property) + { + if (!string.IsNullOrEmpty(property.Extensions?.Format)) + { + return property.Extensions.Format; + } + return property.Schema.Extensions?.Format; + } + + private InputType GetOrCreateType(Schema schema, bool isNullable) + => GetOrCreateType(schema, schema.Extensions?.Format, isNullable); + + private InputType GetOrCreateType(Schema schema, string? format, bool isNullable) + { + if (schema is ObjectSchema objectSchema) + { + var resolvedType = _modelsCache[objectSchema]; + return resolvedType.WithNullable(isNullable); + } + + if (_enumsCache.TryGetValue(schema, out var enumType)) + { + var resolvedType = enumType; + return resolvedType.WithNullable(isNullable); + } + + var type = CreateType(schema, format, isNullable); + if (type.Serialization == InputTypeSerialization.Default) + { + return type with + { + Serialization = GetSerialization(schema), + }; + } + + return type; + } + + private InputType CreateType(Schema schema, string? format, bool isNullable) => schema switch + { + // Respect schema.Type firstly since we might have applied the type change based on rename-mapping + { Type: AllSchemaTypes.ArmId } => InputPrimitiveType.ResourceIdentifier.WithNullable(isNullable), + { Type: AllSchemaTypes.Boolean } => InputPrimitiveType.Boolean.WithNullable(isNullable), + { Type: AllSchemaTypes.Binary } => InputPrimitiveType.Stream.WithNullable(isNullable), + ByteArraySchema { Type: AllSchemaTypes.ByteArray, Format: ByteArraySchemaFormat.Base64url } => InputPrimitiveType.Base64Url.WithNullable(isNullable), + ByteArraySchema { Type: AllSchemaTypes.ByteArray, Format: ByteArraySchemaFormat.Byte } => InputPrimitiveType.Base64.WithNullable(isNullable), + { Type: AllSchemaTypes.Credential } => InputPrimitiveType.String.WithNullable(isNullable), + { Type: AllSchemaTypes.Date } => InputPrimitiveType.PlainDate.WithNullable(isNullable), + DateTimeSchema { Type: AllSchemaTypes.DateTime, Format: DateTimeSchemaFormat.DateTime } => new InputDateTimeType(DateTimeKnownEncoding.Rfc3339, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + DateTimeSchema { Type: AllSchemaTypes.DateTime, Format: DateTimeSchemaFormat.DateTimeRfc1123 } => new InputDateTimeType(DateTimeKnownEncoding.Rfc7231, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + { Type: AllSchemaTypes.DateTime } => new InputDateTimeType(DateTimeKnownEncoding.Rfc3339, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + DurationSchema when format == XMsFormat.DurationConstant => new InputDurationType(DurationKnownEncoding.Constant, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + { Type: AllSchemaTypes.Duration } => new InputDurationType(DurationKnownEncoding.Iso8601, "duration", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + NumberSchema { Type: AllSchemaTypes.Number, Precision: 32 } => InputPrimitiveType.Float32.WithNullable(isNullable), + NumberSchema { Type: AllSchemaTypes.Number, Precision: 128 } => InputPrimitiveType.Decimal128.WithNullable(isNullable), + { Type: AllSchemaTypes.Number } => InputPrimitiveType.Float64.WithNullable(isNullable), + NumberSchema { Type: AllSchemaTypes.Integer, Precision: 64 } => InputPrimitiveType.Int64.WithNullable(isNullable), + { Type: AllSchemaTypes.Integer } => InputPrimitiveType.Int32.WithNullable(isNullable), + { Type: AllSchemaTypes.Time } => InputPrimitiveType.PlainTime.WithNullable(isNullable), + { Type: AllSchemaTypes.Unixtime } => new InputDateTimeType(DateTimeKnownEncoding.UnixTimestamp, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + { Type: AllSchemaTypes.Uri } => InputPrimitiveType.Url.WithNullable(isNullable), + { Type: AllSchemaTypes.Uuid } => InputPrimitiveType.Uuid.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.ArmId => InputPrimitiveType.ResourceIdentifier.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.AzureLocation => InputPrimitiveType.AzureLocation.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.ContentType => InputPrimitiveType.ContentType.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.DateTime => new InputDateTimeType(DateTimeKnownEncoding.Rfc3339, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.DateTimeRFC1123 => new InputDateTimeType(DateTimeKnownEncoding.Rfc7231, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.DateTimeUnix => new InputDateTimeType(DateTimeKnownEncoding.UnixTimestamp, "dateTime", string.Empty, InputPrimitiveType.Int64).WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.DurationConstant => new InputDurationType(DurationKnownEncoding.Constant, "dateTime", string.Empty, InputPrimitiveType.String).WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.ETag => InputPrimitiveType.ETag.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.IPAddress => InputPrimitiveType.IPAddress.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.ResourceType => InputPrimitiveType.ResourceType.WithNullable(isNullable), +#pragma warning disable CS0618 // Type or member is obsolete + { Type: AllSchemaTypes.String } when format == XMsFormat.RequestMethod => InputPrimitiveType.RequestMethod.WithNullable(isNullable), + { Type: AllSchemaTypes.String } when format == XMsFormat.Object => InputPrimitiveType.Object.WithNullable(isNullable), +#pragma warning restore CS0618 // Type or member is obsolete + { Type: AllSchemaTypes.String } => ToDataFactoryElementType(schema.Name, format, isNullable) ?? InputPrimitiveType.String.WithNullable(isNullable), + { Type: AllSchemaTypes.Char } => InputPrimitiveType.Char.WithNullable(isNullable), + { Type: AllSchemaTypes.Any } => InputPrimitiveType.Unknown.WithNullable(isNullable), + { Type: AllSchemaTypes.AnyObject } => ToDataFactoryElementType(schema.Name, format, isNullable) ?? InputPrimitiveType.Unknown.WithNullable(isNullable), + + ConstantSchema constantSchema => CreateConstant(constantSchema, format, isNullable).Type, + ChoiceSchema choiceSchema => GetInputTypeForChoiceSchema(choiceSchema), + SealedChoiceSchema choiceSchema => GetInputTypeForChoiceSchema(choiceSchema), + ArraySchema array => new InputListType(array.Name, array.Extensions?.IsEmbeddingsVector == true ? "Azure.Core.EmbeddingVector" : "TypeSpec.Array", GetOrCreateType(array.ElementType, array.NullableItems ?? false)).WithNullable(isNullable), + DictionarySchema dictionary => new InputDictionaryType(dictionary.Name, InputPrimitiveType.String, GetOrCreateType(dictionary.ElementType, dictionary.NullableItems ?? false)).WithNullable(isNullable), + ObjectSchema objectSchema => CreateTypeForObjectSchema(objectSchema), + + _ => throw new InvalidCastException($"Unknown schema type {schema.GetType()}") + }; + + // For ExampleValue.Schema, the schema is ChoiceSchema or SealedChoiceSchema, but the ChoiceSchema is not the same instance as the one in CodeModel.ChoiceSchemas or CodeModel.SealedChoiceSchemas + private InputType GetInputTypeForChoiceSchema(SealedChoiceSchema schema) + { + if (_enumsCache.TryGetValue(schema, out var result)) + { + return result; + } + return _enumsNamingCache[schema.Language.Default.Name]; + } + + private InputType GetInputTypeForChoiceSchema(ChoiceSchema schema) + { + if (_enumsCache.TryGetValue(schema, out var result)) + { + return result; + } + return _enumsNamingCache[schema.Language.Default.Name]; + } + + private InputType CreateTypeForObjectSchema(ObjectSchema objectSchema) + { + if (objectSchema.Language.Default.Name.StartsWith("DataFactoryElement-")) + { + return CreateDataFactoryElementInputType(false, InputPrimitiveType.String); + } + return _modelsCache[objectSchema]; + } + + private InputType? ToDataFactoryElementType(string name, string? format, bool isNullable, Schema? elementType = null) + { + var type = typeof(DataFactoryElement<>); + return format switch + { + XMsFormat.DataFactoryElementOfObject => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.Unknown), + XMsFormat.DataFactoryElementOfString => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.String), + XMsFormat.DataFactoryElementOfInt => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.Int32), + XMsFormat.DataFactoryElementOfDouble => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.Float64), + XMsFormat.DataFactoryElementOfBool => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.Boolean), + XMsFormat.DataFactoryElementOfListOfT => CreateDataFactoryElementInputType(isNullable, new InputListType(name, "TypeSpec.Array", GetOrCreateType(elementType!, false))), + XMsFormat.DataFactoryElementOfListOfString => CreateDataFactoryElementInputType(isNullable, new InputListType(name, "TypeSpec.Array", InputPrimitiveType.String)), + XMsFormat.DataFactoryElementOfKeyValuePairs => CreateDataFactoryElementInputType(isNullable, new InputDictionaryType(name, InputPrimitiveType.String, InputPrimitiveType.String)), + XMsFormat.DataFactoryElementOfDateTime => CreateDataFactoryElementInputType(isNullable, new InputDateTimeType(DateTimeKnownEncoding.Rfc3339, "dateTime", string.Empty, InputPrimitiveType.String)), + XMsFormat.DataFactoryElementOfDuration => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.PlainTime), + XMsFormat.DataFactoryElementOfUri => CreateDataFactoryElementInputType(isNullable, InputPrimitiveType.Url), + XMsFormat.DataFactoryElementOfKeyObjectValuePairs => CreateDataFactoryElementInputType(isNullable, new InputDictionaryType(name, InputPrimitiveType.String, InputPrimitiveType.Unknown)), + _ => null + }; + } + + private static InputType CreateDataFactoryElementInputType(bool isNullable, InputType argumentType) + => new InputModelType("DataFactoryElement", "Azure.Core.Resources.DataFactoryElement", null, null, null, InputModelTypeUsage.None, Array.Empty(), null, Array.Empty(), null, null, new Dictionary(), null, new List { argumentType }).WithNullable(isNullable); + + private InputConstant CreateConstant(ConstantSchema constantSchema, string? format, bool isNullable) + { + var rawValue = constantSchema.Value.Value; + var valueType = CreateType(constantSchema.ValueType, format, isNullable); + if (isNullable && rawValue == null) + { + return new InputConstant(null, valueType); + } + + // normalize the value, because the "value" coming from the code model is always a string + var kind = valueType.GetImplementType() switch + { + InputPrimitiveType primitiveType => primitiveType.Kind, + InputEnumType enumType => enumType.ValueType.Kind, + _ => throw new InvalidCastException($"Unknown value type {valueType.GetType()} for literal types") + }; + + object normalizedValue = kind switch + { + InputPrimitiveTypeKind.Boolean => bool.Parse(rawValue.ToString()!), + InputPrimitiveTypeKind.Int32 => int.Parse(rawValue.ToString()!), + InputPrimitiveTypeKind.Int64 => long.Parse(rawValue.ToString()!), + InputPrimitiveTypeKind.Float32 => float.Parse(rawValue.ToString()!), + InputPrimitiveTypeKind.Float64 => double.Parse(rawValue.ToString()!), + _ => rawValue + }; + + return new InputConstant(normalizedValue, valueType); + } + + private InputEnumType CreateEnumType(Schema schema, PrimitiveSchema choiceType, IEnumerable choices, bool isExtensible) + { + var usage = _schemaUsageProvider.GetUsage(schema); + var valueType = (InputPrimitiveType)CreateType(choiceType, schema.Extensions?.Format, false); + var inputEnumType = new InputEnumType( + Name: schema.Name, + CrossLanguageDefinitionId: GetCrossLanguageDefinitionId(schema), + Accessibility: schema.Extensions?.Accessibility ?? (usage.HasFlag(SchemaTypeUsage.Model) ? "public" : "internal"), + Deprecated: schema.Deprecated?.Reason, + Description: schema.CreateDescription(), + Usage: GetUsage(usage), + ValueType: valueType, + Values: choices.Select(c => CreateEnumValue(c, valueType)).ToList(), + IsExtensible: isExtensible + ) + { + Serialization = GetSerialization(schema) + }; + return inputEnumType; + } + + private static string GetCrossLanguageDefinitionId(Schema schema) + { + var ns = schema.Extensions?.Namespace; + var name = schema.Language.Default.Name; + if (ns == null) + { + return name; + } + + return $"{ns}.{name}"; + } + + private static InputEnumTypeValue CreateEnumValue(ChoiceValue choiceValue, InputPrimitiveType valueType) + { + // in code model, the value of choices are defined to be strings. We need to convert them back to their real values + var value = ConvertRawValue(valueType, choiceValue.Value); + return new( + Name: choiceValue.Language.Default.Name, + Description: choiceValue.Language.Default.Description, + Value: value!); + } + + private static RequestLocation GetRequestLocation(RequestParameter requestParameter) => requestParameter.In switch + { + HttpParameterIn.Uri => RequestLocation.Uri, + HttpParameterIn.Path => RequestLocation.Path, + HttpParameterIn.Query => RequestLocation.Query, + HttpParameterIn.Header => RequestLocation.Header, + HttpParameterIn.Body => RequestLocation.Body, + _ => RequestLocation.None + }; + + private InputConstant? GetDefaultValue(RequestParameter parameter) + { + if (parameter.ClientDefaultValue != null) + { + if (parameter.Origin == "modelerfour:synthesized/host" && (string)parameter.ClientDefaultValue == "") + { + return null; + } + + return new InputConstant(Value: parameter.ClientDefaultValue, Type: GetOrCreateType(parameter.Schema, parameter.IsNullable)); + } + + if (parameter is { Schema: ConstantSchema constantSchema } && (!Configuration.AzureArm || parameter.IsRequired)) + { + return new InputConstant(Value: constantSchema.Value.Value, Type: GetOrCreateType(constantSchema.ValueType, constantSchema.Value.Value == null)); + } + + return null; + } + + private IReadOnlyList GetApiVersions() + => _codeModel.OperationGroups + .SelectMany(g => g.Operations.SelectMany(o => o.ApiVersions)) + .Select(v => v.Version) + .Distinct() + .OrderBy(v => v) + .ToList(); + + private static InputAuth GetAuth(IEnumerable securitySchemes) + { + InputApiKeyAuth? apiKey = null; + InputOAuth2Auth? oAuth2 = null; + + foreach (var scheme in securitySchemes.Distinct(SecuritySchemesComparer.Instance)) + { + switch (scheme) + { + case AzureKeySecurityScheme: + throw new NotSupportedException($"{typeof(AzureKeySecurityScheme)} is not supported. Use {typeof(KeySecurityScheme)} instead"); + case AADTokenSecurityScheme: + throw new NotSupportedException($"{typeof(AADTokenSecurityScheme)} is not supported. Use {typeof(OAuth2SecurityScheme)} instead"); + case KeySecurityScheme when apiKey is not null: + // Tolerate second KeySecurityScheme to support TranslatorText: https://github.com/Azure/azure-rest-api-specs/blob/3196a62202976da192d6da86f44b02246ca2aa97/specification/cognitiveservices/data-plane/TranslatorText/stable/v3.0/TranslatorText.json#L14 + // See https://github.com/Azure/autorest.csharp/issues/2637 + //throw new NotSupportedException($"Only one {typeof(KeySecurityScheme)} is supported. Remove excess"); + break; + case OAuth2SecurityScheme when oAuth2 is not null: + throw new NotSupportedException($"Only one {typeof(OAuth2SecurityScheme)} is not supported. Remove excess"); + case KeySecurityScheme apiKeyScheme: + apiKey = new InputApiKeyAuth(apiKeyScheme.Name); + break; + case OAuth2SecurityScheme oAuth2Scheme: + oAuth2 = new InputOAuth2Auth(oAuth2Scheme.Scopes.ToList()); + break; + } + } + + return new InputAuth(apiKey, oAuth2); + } + + private class SecuritySchemesComparer : IEqualityComparer + { + public static IEqualityComparer Instance { get; } = new SecuritySchemesComparer(); + + private SecuritySchemesComparer() { } + + public bool Equals(SecurityScheme? x, SecurityScheme? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null)) + { + return false; + } + + if (ReferenceEquals(y, null)) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return (x, y) switch + { + (KeySecurityScheme apiKeyX, KeySecurityScheme apiKeyY) + => apiKeyX.Type == apiKeyY.Type && apiKeyX.Name == apiKeyY.Name, + (OAuth2SecurityScheme oAuth2X, OAuth2SecurityScheme oAuth2Y) + => oAuth2X.Type == oAuth2Y.Type && oAuth2X.Scopes.SequenceEqual(oAuth2Y.Scopes, StringComparer.Ordinal), + _ => x.Type == y.Type + }; + } + + public int GetHashCode(SecurityScheme obj) => + obj switch + { + AzureKeySecurityScheme azure => HashCode.Combine(azure.Type, azure.HeaderName), + AADTokenSecurityScheme aad => GetAADTokenSecurityHashCode(aad), + _ => HashCode.Combine(obj.Type) + }; + + private static int GetAADTokenSecurityHashCode(AADTokenSecurityScheme aad) + { + var hashCode = new HashCode(); + hashCode.Add(aad.Type); + foreach (var value in aad.Scopes) + { + hashCode.Add(value); + } + return hashCode.ToHashCode(); + } + } + + } +} diff --git a/logger/autorest.csharp/common/Input/CodeModelPartials.cs b/logger/autorest.csharp/common/Input/CodeModelPartials.cs new file mode 100644 index 0000000..2a70944 --- /dev/null +++ b/logger/autorest.csharp/common/Input/CodeModelPartials.cs @@ -0,0 +1,769 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using YamlDotNet.Serialization; + +#pragma warning disable SA1649 +#pragma warning disable SA1402 +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. +// ReSharper disable once CheckNamespace +namespace AutoRest.CSharp.Input +{ + [DebuggerDisplay("Languages(Name: {Default.Name})")] + internal partial class Languages { } + + internal partial class Operation + { + // For some reason, booleans in dictionaries are deserialized as string instead of bool. + public bool IsLongRunning => Convert.ToBoolean(Extensions.GetValue("x-ms-long-running-operation") ?? "false"); + + public bool KeepClientDefaultValue => Configuration.MethodsToKeepClientDefaultValue.Contains(OperationId); + public OperationFinalStateVia LongRunningFinalStateVia + { + get + { + var finalStateVia = Extensions.GetValue>("x-ms-long-running-operation-options")?.GetValue("final-state-via"); + return finalStateVia switch + { + "azure-async-operation" => OperationFinalStateVia.AzureAsyncOperation, + "location" => OperationFinalStateVia.Location, + "original-uri" => OperationFinalStateVia.OriginalUri, + null => OperationFinalStateVia.Location, + _ => throw new ArgumentException($"Unknown final-state-via value: {finalStateVia}"), + }; + } + } + + public string? Accessibility => Extensions.GetValue("x-accessibility"); + + public ServiceResponse LongRunningInitialResponse + { + get + { + Debug.Assert(IsLongRunning); + + foreach (var operationResponse in Responses) + { + if (operationResponse.Protocol.Http is HttpResponse operationHttpResponse && + !operationHttpResponse.StatusCodes.Contains(StatusCodes._200) && + !operationHttpResponse.StatusCodes.Contains(StatusCodes._204)) + { + return operationResponse; + } + } + + return Responses.First(); + } + } + public ServiceResponse LongRunningFinalResponse + { + get + { + Debug.Assert(IsLongRunning); + + foreach (var operationResponse in Responses) + { + if (operationResponse.Protocol.Http is HttpResponse operationHttpResponse && + (operationHttpResponse.StatusCodes.Contains(StatusCodes._200) || + operationHttpResponse.StatusCodes.Contains(StatusCodes._204))) + { + return operationResponse; + } + } + + return Responses.First(); + } + } + + + public ServiceResponse? GetResponseByCode(StatusCodes code) + { + return Responses.FirstOrDefault(response => response.Protocol.Http is HttpResponse httpResponse && + httpResponse.StatusCodes.Any(c => c == code)); + + } + public ServiceResponse? GetSuccessfulQueryResponse() + { + return GetResponseByCode(StatusCodes._200); + } + } + + internal partial class RecordOfStringAndAny + { + private static char[] _formatSplitChar = new[] { ',', ' ' }; + + public string? Accessibility => TryGetValue("x-accessibility", out object? value) ? value?.ToString() : null; + public string? Namespace => TryGetValue("x-namespace", out object? value) ? value?.ToString() : null; + public string? Usage => TryGetValue("x-csharp-usage", out object? value) ? value?.ToString() : null; + + public bool IsEmbeddingsVector => TryGetValue("x-ms-embedding-vector", out var value) && Convert.ToBoolean(value); + + public string[] Formats + { + get + { + if (TryGetValue("x-csharp-formats", out object? value) && value?.ToString() is string s) + { + return s.Split(_formatSplitChar, StringSplitOptions.RemoveEmptyEntries); + } + + return Array.Empty(); + } + } + + public string? HeaderCollectionPrefix => TryGetValue("x-ms-header-collection-prefix", out object? value) ? value?.ToString() : null; + + public bool? BufferResponse => TryGetValue("x-csharp-buffer-response", out object? value) && value != null ? (bool?)Convert.ToBoolean(value) : null; + + public bool SkipEncoding => TryGetValue("x-ms-skip-url-encoding", out var value) && Convert.ToBoolean(value); + + public string? Format + { + get + { + if (format == null) + format = TryGetValue("x-ms-format", out object? value) ? value?.ToString() : string.Empty; + return format; + } + set + { + format = value; + } + } + private string? format; + + public bool SkipInitCtor + { + get + { + if (!skipInitCtor.HasValue) + skipInitCtor = TryGetValue("x-ms-skip-init-ctor", out var value) && Convert.ToBoolean(value); + return skipInitCtor.Value; + } + set + { + skipInitCtor = value; + } + } + private bool? skipInitCtor; + } + + internal partial class RecordOfStringAndRequest : System.Collections.Generic.Dictionary + { + } + + internal static class XMsFormat + { + public const string ArmId = "arm-id"; + public const string AzureLocation = "azure-location"; + public const string DateTime = "date-time"; + public const string DateTimeRFC1123 = "date-time-rfc1123"; + public const string DateTimeUnix = "date-time-unix"; + public const string DurationConstant = "duration-constant"; + public const string ETag = "etag"; + public const string ResourceType = "resource-type"; + public const string Object = "object"; + public const string IPAddress = "ip-address"; + public const string ContentType = "content-type"; + public const string RequestMethod = "request-method"; + public const string DataFactoryElementOfObject = "dfe-object"; + public const string DataFactoryElementOfString = "dfe-string"; + public const string DataFactoryElementOfInt = "dfe-int"; + public const string DataFactoryElementOfDouble = "dfe-float"; + public const string DataFactoryElementOfBool = "dfe-bool"; + public const string DataFactoryElementOfListOfT = "dfe-list-generic"; + public const string DataFactoryElementOfListOfString = "dfe-list-string"; + public const string DataFactoryElementOfKeyValuePairs = "dfe-key-value-pairs"; + public const string DataFactoryElementOfKeyObjectValuePairs = "dfe-key-object-value-pairs"; + public const string DataFactoryElementOfDateTime = "dfe-date-time"; + public const string DataFactoryElementOfDuration = "dfe-duration"; + public const string DataFactoryElementOfUri = "dfe-uri"; + } + + internal partial class ServiceResponse + { + public HttpResponse HttpResponse => Protocol.Http as HttpResponse ?? throw new InvalidOperationException($"Expected an HTTP response"); + public Schema? ResponseSchema => (this as SchemaResponse)?.Schema; + } + + internal partial class RequestParameter + { + public bool IsResourceParameter => Convert.ToBoolean(Extensions.GetValue("x-ms-resource-identifier")); + + public HttpParameterIn In => Protocol.Http is HttpParameter httpParameter ? httpParameter.In : HttpParameterIn.None; + public bool IsFlattened => Flattened ?? false; + + public bool IsApiVersion => Origin == "modelerfour:synthesized/api-version"; + } + + internal partial class HttpResponse + { + public int[] IntStatusCodes => StatusCodes.Select(ToStatusCode).ToArray(); + + private static int ToStatusCode(StatusCodes arg) => int.Parse(arg.ToString().Trim('_')); + } + + internal partial class Value + { + public Value() + { + Extensions = new RecordOfStringAndAny(); + } + + public bool IsNullable => Nullable ?? false; + public bool IsRequired => Required ?? false; + } + + internal partial class SchemaResponse + { + public bool IsNullable => Nullable ?? false; + } + + internal partial class Property + { + public bool IsReadOnly => ReadOnly ?? false; + } + + internal partial class ObjectSchema + { + public ObjectSchema() + { + Parents = new Relations(); + Children = new Relations(); + } + + public bool IsUnknownDiscriminatorModel { get; init; } + } + + // redefined manually to inherit from ObjectSchema + internal partial class GroupSchema : ObjectSchema + { + } + + internal partial class Schema + { + public string? XmlName => Serialization?.Xml?.Name; + public string Name => Language.Default.Name; + } + + internal partial class HTTPSecurityScheme : Dictionary + { + + } + + internal partial class Paging + { + [YamlMember(Alias = "group")] + public string? Group { get; set; } + + [YamlMember(Alias = "itemName")] + public string? ItemName { get; set; } + + [YamlMember(Alias = "member")] + public string? Member { get; set; } + + [YamlMember(Alias = "nextLinkName")] + public string? NextLinkName { get; set; } + + [YamlMember(Alias = "nextLinkOperation")] + public Operation? NextLinkOperation { get; set; } + } + + /// language metadata specific to schema instances + internal partial class Language + { + /// namespace of the implementation of this item + [YamlMember(Alias = "namespace")] + public string? Namespace { get; set; } + + /// if this is a child of a polymorphic class, this will have the value of the discriminator. + [YamlMember(Alias = "discriminatorValue")] + public string? DiscriminatorValue { get; set; } + + [YamlMember(Alias = "uid")] + public string? Uid { get; set; } + + [YamlMember(Alias = "internal")] + public bool? Internal { get; set; } + + [YamlMember(Alias = "serializedName")] + public string? SerializedName { get; set; } + + [YamlMember(Alias = "paging")] + public Paging? Paging { get; set; } + + [YamlMember(Alias = "header")] + public string? Header { get; set; } + + [YamlMember(Alias = "summary")] + public string? Summary { get; set; } + } + + internal partial class OperationGroup + { + public override string ToString() + { + return $"OperationGroup(Key: {Key})"; + } + } + + internal partial class CodeModel + { + [YamlDotNet.Serialization.YamlMember(Alias = "testModel")] + public TestModel? TestModel { get; set; } + + public IEnumerable AllSchemas + { + get + { + foreach (var schema in Schemas.Choices) + yield return schema; + foreach (var schema in Schemas.SealedChoices) + yield return schema; + foreach (var schema in Schemas.Objects) + yield return schema; + foreach (var schema in Schemas.Groups) + yield return schema; + } + } + } + + internal partial class VariableScope + { + [YamlMember(Alias = "variables")] + public Dictionary Variables { get; set; } = new(); + + [YamlMember(Alias = "requiredVariables")] + public ICollection RequiredVariables { get; set; } = Array.Empty(); + + [YamlMember(Alias = "secretVariables")] + public ICollection SecretVariables { get; set; } = Array.Empty(); + } + + internal partial class Variable + { + [YamlMember(Alias = @"type")] + public string? Type { get; set; } + [YamlMember(Alias = @"value")] + public object? Value { get; set; } + } + + internal enum TestDefinitionScope + { + [System.Runtime.Serialization.EnumMember(Value = @"ResourceGroup")] + ResourceGroup, + } + + internal partial class ScenarioDefinition : VariableScope + { + [YamlMember(Alias = "scope")] + public TestDefinitionScope? Scope { get; set; } + + [YamlMember(Alias = "prepareSteps")] + public ICollection PrepareSteps { get; set; } = Array.Empty(); + + [YamlMember(Alias = "scenarios")] + public ICollection Scenarios { get; set; } = Array.Empty(); + + [YamlMember(Alias = "cleanUpSteps")] + public ICollection CleanUpSteps { get; set; } = Array.Empty(); + + [YamlMember(Alias = "_filePath")] + public string FilePath { get; set; } + + [YamlMember(Alias = "_swaggerFilePaths")] + public ICollection SwaggerFilePaths { get; set; } = Array.Empty(); + } + + internal partial class TestDefinitionModel : ScenarioDefinition + { + [YamlMember(Alias = "useArmTemplate")] + public bool UseArmTemplate { get; set; } + + [YamlMember(Alias = "requiredVariablesDefault")] + public Dictionary RequiredVariablesDefault; + }; + + internal partial class JsonPatchOp + { + [YamlMember(Alias = "add")] + public string? Add { get; set; } + + [YamlMember(Alias = "remove")] + public string? Remove { get; set; } + + [YamlMember(Alias = "replace")] + public string? Replace { get; set; } + + [YamlMember(Alias = "copy")] + public string? Copy { get; set; } + + [YamlMember(Alias = "move")] + public string? Move { get; set; } + + [YamlMember(Alias = "test")] + public string? Test { get; set; } + + [YamlMember(Alias = "value")] + public object? Value { get; set; } + + [YamlMember(Alias = "oldValue")] + public object? OldValue { get; set; } + + [YamlMember(Alias = "from")] + public string? From { get; set; } + }; + + internal enum TestStepType + { + [System.Runtime.Serialization.EnumMember(Value = @"restCall")] + RestCall, + + [System.Runtime.Serialization.EnumMember(Value = @"armTemplateDeployment")] + ArmTemplateDeployment, + + [System.Runtime.Serialization.EnumMember(Value = @"rawCall")] + RawCall, + } + + internal enum VariableType + { + [System.Runtime.Serialization.EnumMember(Value = @"string")] + String, + + [System.Runtime.Serialization.EnumMember(Value = @"secureString")] + SecureString, + } + + internal partial class StepBase : VariableScope + { + [YamlMember(Alias = "description")] + public string? Description { get; set; } + + [YamlMember(Alias = "step")] + public string? Step { get; set; } + + [YamlMember(Alias = "outputVariables")] + public Dictionary OutputVariables { get; set; } = new Dictionary(); + + [YamlMember(Alias = "isPrepareStep")] + public bool? IsPrepareStep { get; set; } + + [YamlMember(Alias = "isCleanUpStep")] + public bool? IsCleanUpStep { get; set; } + } + + internal partial class OutputVariable + { + [YamlMember(Alias = "type")] + public VariableType Type { get; set; } + + [YamlMember(Alias = "fromResponse")] + public string FromResponse { get; set; } + } + + // left this class here for reference. The properties in this class have been populated into TestStep since we could not find a way to introduce polymorphism during yaml deserialization + internal partial class RestCallStep : TestStepBase + { + // for TestStepRestCall (type==restCall) + [YamlMember(Alias = "operationId")] + public string? OperationId { get; set; } + + [YamlMember(Alias = "operation")] + public Operation? Operation { get; set; } + + [YamlMember(Alias = "exampleName")] + public string? ExampleName { get; set; } + + [YamlMember(Alias = "exampleFile")] + public string? ExampleFile { get; set; } + + [YamlMember(Alias = "resourceName")] + public string? ResourceName { get; set; } + + [YamlMember(Alias = "exampleFilePath")] + public string? ExampleFilePath { get; set; } + + [YamlMember(Alias = "requestParameters")] + public RecordOfStringAndAny? RequestParameters { get; set; } + + [YamlMember(Alias = "expectedResponse")] + public object? ExpectedResponse { get; set; } + + [YamlMember(Alias = "statusCode")] + public int? StatusCode { get; set; } + + [YamlMember(Alias = "resourceUpdate")] + public ICollection ResourceUpdate { get; set; } = Array.Empty(); + + [YamlMember(Alias = "requestUpdate")] + public ICollection RequestUpdate { get; set; } = Array.Empty(); + + [YamlMember(Alias = "responseUpdate")] + public ICollection ResponseUpdate { get; set; } = Array.Empty(); + + [YamlMember(Alias = "exampleModel")] + public ExampleModel? ExampleModel { get; set; } + + [YamlMember(Alias = "outputVariablesModel")] + public Dictionary> OutputVariablesModel { get; set; } = new(); + } + + internal partial class OutputVariableModel + { + [YamlMember(Alias = "index")] + public int? Index { get; set; } + + [YamlMember(Alias = "key")] + public string? Key { get; set; } + + [YamlMember(Alias = "languages")] + public Languages Language { get; set; } = new Languages(); + } + + // left this class here for reference. The properties in this class have been populated into TestStep since we could not find a way to introduce polymorphism during yaml deserialization + internal partial class ArmTemplateStep : TestStepBase + { + // for TestStepArmTemplateDeployment (type==armTemplateDeployment) + [YamlMember(Alias = "armTemplate")] + public string? ArmTemplate { get; set; } + } + + // left this class here for reference. The properties in this class have been populated into TestStep since we could not find a way to introduce polymorphism during yaml deserialization + internal partial class RawCallStep : TestStepBase + { + [YamlMember(Alias = "method")] + public HttpMethod Method { get; set; } + + [YamlMember(Alias = "rawUrl")] + public string? RawUrl { get; set; } + + [YamlMember(Alias = "requestHeaders")] + public Dictionary RequestHeaders { get; set; } = new(); + + [YamlMember(Alias = "requestBody")] + public string? RequestBody { get; set; } + + [YamlMember(Alias = "statusCode")] + public int? StatusCode { get; set; } + + [YamlMember(Alias = "expectedResponse")] + public string? ExpectedResponse { get; set; } + } + + internal partial class TestStepBase : StepBase + { + [YamlMember(Alias = "type")] + public TestStepType Type { get; set; } + } + + internal partial class TestStep : StepBase + { + [YamlMember(Alias = "type")] + public TestStepType Type { get; set; } + + #region RestCallStep (type==restCall) + [YamlMember(Alias = "operationId")] + public string? OperationId { get; set; } + + [YamlMember(Alias = "operation")] + public Operation? Operation { get; set; } + + [YamlMember(Alias = "exampleName")] + public string? ExampleName { get; set; } + + [YamlMember(Alias = "exampleFile")] + public string? ExampleFile { get; set; } + + [YamlMember(Alias = "resourceName")] + public string? ResourceName { get; set; } + + [YamlMember(Alias = "exampleFilePath")] + public string? ExampleFilePath { get; set; } + + [YamlMember(Alias = "requestParameters")] + public RecordOfStringAndAny? RequestParameters { get; set; } + + [YamlMember(Alias = "resourceUpdate")] + public ICollection ResourceUpdate { get; set; } = Array.Empty(); + + [YamlMember(Alias = "requestUpdate")] + public ICollection RequestUpdate { get; set; } = Array.Empty(); + + [YamlMember(Alias = "responseUpdate")] + public ICollection ResponseUpdate { get; set; } = Array.Empty(); + + [YamlMember(Alias = "exampleModel")] + public ExampleModel? ExampleModel { get; set; } + + [YamlMember(Alias = "outputVariablesModel")] + public Dictionary> OutputVariablesModel { get; set; } = new(); + #endregion + + #region ArmTemplateStep + [YamlMember(Alias = "armTemplate")] + public string? ArmTemplate { get; set; } + #endregion + + #region RawCallStep + [YamlMember(Alias = "method")] + public HttpMethod Method { get; set; } + + [YamlMember(Alias = "rawUrl")] + public string? RawUrl { get; set; } + + [YamlMember(Alias = "requestHeaders")] + public Dictionary RequestHeaders { get; set; } = new(); + + [YamlMember(Alias = "requestBody")] + public string? RequestBody { get; set; } + #endregion + + #region Both RawCallStep and RestCallStep + + [YamlMember(Alias = "statusCode")] + public int? StatusCode { get; set; } + + [YamlMember(Alias = "expectedResponse")] + public object? ExpectedResponse { get; set; } + #endregion + } + + internal partial class Scenario : VariableScope + { + [YamlMember(Alias = "scenario")] + public string ScenarioName { get; set; } + + [YamlMember(Alias = "shareScope")] + public bool ShareScope { get; set; } + + [YamlMember(Alias = "description")] + public string Description { get; set; } + + [YamlMember(Alias = "steps")] + public ICollection Steps { get; set; } = Array.Empty(); + + [YamlMember(Alias = "_scenarioDef")] + public ScenarioDefinition? ScenarioDefinition { get; set; } + + [YamlMember(Alias = "requiredVariablesDefault")] + public Dictionary RequiredVaraiblesDefault { get; set; } = new(); + }; + + + internal partial class TestModel + { + [YamlMember(Alias = "mockTest")] + public MockTestDefinitionModel MockTest { get; set; } + + [YamlMember(Alias = "scenarioTests")] + public ICollection ScenarioTests { get; set; } + } + + internal partial class MockTestDefinitionModel + { + [YamlMember(Alias = "exampleGroups")] + public ICollection ExampleGroups { get; set; } + } + + internal partial class ExampleGroup + { + [YamlMember(Alias = "operationId")] + public string OperationId { get; set; } + + [YamlMember(Alias = "examples")] + public ICollection Examples { get; set; } + + [YamlMember(Alias = "operationGroup")] + public OperationGroup OperationGroup { get; set; } + + [YamlMember(Alias = "operation")] + public Operation Operation { get; set; } + } + + internal partial class ExampleModel + { + [YamlMember(Alias = "name")] + public string Name { get; set; } /** Key in x-ms-examples */ + + [YamlMember(Alias = "operationGroup")] + public OperationGroup OperationGroup { get; set; } + + [YamlMember(Alias = "operation")] + public Operation Operation { get; set; } + + [YamlMember(Alias = "originalFile")] + public string? OriginalFile { get; set; } + + [YamlMember(Alias = "clientParameters")] + public ICollection ClientParameters { get; set; } + + [YamlMember(Alias = "methodParameters")] + public ICollection MethodParameters { get; set; } + + [YamlMember(Alias = "responses")] + public Dictionary Responses { get; set; } // statusCode-->ExampleResponse + + public IEnumerable AllParameters => this.ClientParameters.Concat(this.MethodParameters); + } + + internal partial class ExampleResponse + { + [YamlMember(Alias = "body")] + public ExampleValue? Body { get; set; } + + [YamlMember(Alias = "headers")] + public object? Headers { get; set; } + } + + internal partial class ExampleParameter + { + /** Ref to the Parameter of operations in codeModel */ + [YamlMember(Alias = "parameter")] + public RequestParameter Parameter { get; set; } + + /** Can be object, list, primitive data, ParameterModel*/ + [YamlMember(Alias = "exampleValue")] + public ExampleValue ExampleValue { get; set; } + } + + internal partial class ExampleValue + { + [YamlMember(Alias = "language")] + public Languages Language { get; set; } + + [YamlMember(Alias = "schema")] + public Schema Schema { get; set; } + + [YamlMember(Alias = "flattenedNames")] + public ICollection? FlattenedNames { get; set; } + + /// + /// Use elements if schema.type==Array, use properties if schema.type==Object/Dictionary, otherwise use rawValue + /// We have to make Elements and Properties nullable because we need the ability to distinguish null value and an empty array/dictionary/object + /// + [YamlMember(Alias = "rawValue")] + public object? RawValue { get; set; } + + [YamlMember(Alias = "elements")] + public System.Collections.Generic.ICollection? Elements { get; set; } + + [YamlMember(Alias = "properties")] + public Dictionary? Properties { get; set; } + + // parent class Name--> value + [YamlMember(Alias = "parentsValue")] + public Dictionary ParentsValue { get; set; } = new(); + + public string CSharpName() => + (this.Language.Default.Name == null || this.Language.Default.Name == "null") ? "NullProperty" : this.Language.Default.Name.ToCleanName(); + } +} +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. diff --git a/logger/autorest.csharp/common/Input/CodeModelSerialization.cs b/logger/autorest.csharp/common/Input/CodeModelSerialization.cs new file mode 100644 index 0000000..8dfd03f --- /dev/null +++ b/logger/autorest.csharp/common/Input/CodeModelSerialization.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using AutoRest.CSharp.Utilities; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Utilities; + +namespace AutoRest.CSharp.Input +{ + internal static class CodeModelSerialization + { + private static readonly Type[] GeneratedTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => + t.Namespace == typeof(CodeModel).Namespace + //https://stackoverflow.com/a/4145127/294804 + && !(t.IsAbstract && t.IsSealed) + && t.CustomAttributes.All(ca => ca.AttributeType != typeof(CompilerGeneratedAttribute))).ToArray(); + + private static readonly Dictionary TagOverrides = new Dictionary + { + { typeof(HttpResponseHeader), "HttpHeader" }, + { typeof(RequestParameter), "Parameter" }, + { typeof(ServiceRequest), "Request" }, + { typeof(ServiceResponse), "Response" }, + { typeof(SerializationFormatMetadata), "SerializationFormat" } + }; + + private static string GetTagName(Type type) => TagOverrides.ContainsKey(type) ? TagOverrides[type] : type.Name; + private static KeyValuePair CreateTagPair(this Type type) => new KeyValuePair($"!{GetTagName(type)}", type); + private static readonly IEnumerable> TagMap = GeneratedTypes.Where(t => t.IsClass).Select(CreateTagPair); + + private static BuilderSkeleton WithTagMapping(this BuilderSkeleton builder, IEnumerable> mapping) where TBuilder : BuilderSkeleton + { + foreach (var (tagName, tagType) in mapping) + { + builder.WithTagMapping(tagName, tagType); + } + return builder; + } + + // Cannot cache deserializer as parallel plugins will access it and cause failures. + // https://github.com/aaubry/YamlDotNet/pull/353/files#diff-86074b6acff29ccad667aca741f62ac5R83 + private static IDeserializer Deserializer => new DeserializerBuilder() + .WithTagMapping(TagMap) + .WithTypeConverter(new YamlStringEnumConverter()).Build(); + public static CodeModel DeserializeCodeModel(string yaml) => Deserializer.Deserialize(yaml); + + public static Dictionary GetDeserializableProperties(this Type type) => type.GetProperties() + .Select(p => new KeyValuePair(p.GetCustomAttributes(true).Select(yma => yma.Alias).FirstOrDefault() ?? String.Empty, p)) + .Where(pa => !pa.Key.IsNullOrEmpty()).ToDictionary(pa => pa.Key, pa => pa.Value); + + //TODO: Handle custom type dictionaries. + // Only allows deserialization of properties that are primitives or type Dictionary. Does not support properties that are custom classes. + public static object? DeserializeDictionary(this PropertyInfo info, object value) + { + if (!(value is Dictionary)) return TypeConverter.ChangeType(value, info.PropertyType); + + var type = info.PropertyType; + var properties = type.GetDeserializableProperties(); + var property = Activator.CreateInstance(type); + var matchedProperties = ((Dictionary)value) + .Select(e => (Key: e.Key?.ToString() ?? String.Empty, e.Value)) + .Where(e => properties.ContainsKey(e.Key)); + foreach (var (propKey, propValue) in matchedProperties) + { + var innerInfo = properties[propKey]; + innerInfo.SetValue(property, innerInfo.DeserializeDictionary(propValue!)); + } + return property; + } + + private class YamlStringEnumConverter : IYamlTypeConverter + { + public bool Accepts(Type type) => type.IsEnum; + + public object ReadYaml(IParser parser, Type type) + { + var parsedEnum = parser.Consume(); + var serializableValues = type.GetMembers().Select(m => (Value: m.GetEnumMemberValue() ?? String.Empty, Info: m)).Where(pa => !pa.Value.IsNullOrEmpty()).ToDictionary(pa => pa.Value, pa => pa.Info); + if (!serializableValues.ContainsKey(parsedEnum.Value)) + { + throw new YamlException(parsedEnum.Start, parsedEnum.End, $"Value '{parsedEnum.Value}' not found in enum '{type.Name}'"); + } + + return Enum.Parse(type, serializableValues[parsedEnum.Value].Name); + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + var yamlValue = value.GetEnumMemberValue() ?? value?.ToString() ?? String.Empty; + emitter.Emit(new Scalar(yamlValue)); + } + } + } +} diff --git a/logger/autorest.csharp/common/Input/Configuration.cs b/logger/autorest.csharp/common/Input/Configuration.cs new file mode 100644 index 0000000..eb83b2a --- /dev/null +++ b/logger/autorest.csharp/common/Input/Configuration.cs @@ -0,0 +1,739 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using AutoRest.CSharp.AutoRest.Communication; +using AutoRest.CSharp.Input; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Input +{ + internal static class Configuration + { + internal static readonly string ProjectFolderDefault = "../"; + + public static class Options + { + public const string OutputFolder = "output-folder"; + public const string Namespace = "namespace"; + public const string LibraryName = "library-name"; + public const string SharedSourceFolders = "shared-source-folders"; + public const string SaveInputs = "save-inputs"; + public const string AzureArm = "azure-arm"; + public const string PublicClients = "public-clients"; + public const string ModelNamespace = "model-namespace"; + public const string HeadAsBoolean = "head-as-boolean"; + public const string SkipCSProj = "skip-csproj"; + public const string SkipCSProjPackageReference = "skip-csproj-packagereference"; + public const string Generation1ConvenienceClient = "generation1-convenience-client"; + public const string SingleTopLevelClient = "single-top-level-client"; + public const string AttachDebuggerFormat = "{0}.attach"; + public const string ProjectFolder = "project-folder"; + public const string ExistingProjectfolder = "existing-project-folder"; + public const string ProtocolMethodList = "protocol-method-list"; + public const string SkipSerializationFormatXml = "skip-serialization-format-xml"; + public const string DisablePaginationTopRenaming = "disable-pagination-top-renaming"; + public const string SuppressAbstractBaseClasses = "suppress-abstract-base-class"; + public const string UnreferencedTypesHandling = "unreferenced-types-handling"; + public const string KeepNonOverloadableProtocolSignature = "keep-non-overloadable-protocol-signature"; + public const string ModelFactoryForHlc = "model-factory-for-hlc"; + public const string GenerateModelFactory = "generate-model-factory"; + public const string ModelsToTreatEmptyStringAsNull = "models-to-treat-empty-string-as-null"; + public const string IntrinsicTypesToTreatEmptyStringAsNull = "intrinsic-types-to-treat-empty-string-as-null"; + public const string AdditionalIntrinsicTypesToTreatEmptyStringAsNull = "additional-intrinsic-types-to-treat-empty-string-as-null"; + public const string PublicDiscriminatorProperty = "public-discriminator-property"; + public const string ShouldTreatBase64AsBinaryData = "should-treat-base64-as-binary-data"; + public const string MethodsToKeepClientDefaultValue = "methods-to-keep-client-default-value"; + public const string DeserializeNullCollectionAsNullValue = "deserialize-null-collection-as-null-value"; + public const string UseCoreDataFactoryReplacements = "use-core-datafactory-replacements"; + public const string Flavor = "flavor"; + public const string GenerateSampleProject = "generate-sample-project"; + public const string GenerateTestProject = "generate-test-project"; + public const string ExamplesDirectory = "examples-dir"; + // TODO - this configuration only exists here because we would like a rolling update for all libraries for this feature since it changes so many files. + public const string UseModelReaderWriter = "use-model-reader-writer"; + // TODO - this configuration only exists here because we would like a rolling update for all libraries for this feature since it changes so many files. + // It is only respected if UseModelReaderWriter is true. + public const string EnableBicepSerialization = "enable-bicep-serialization"; + // This configuration is only respected if UseModelReaderWriter is true. + public const string EnableInternalRawData = "enable-internal-raw-data"; + public const string HelperNamespace = "helper-namespace"; + public const string DisableXmlDocs = "disable-xml-docs"; + public const string CustomHeader = "custom-header"; + } + + public enum UnreferencedTypesHandlingOption + { + RemoveOrInternalize = 0, + Internalize = 1, + KeepAll = 2 + } + + public static void Initialize( + string outputFolder, + string ns, + string libraryName, + string[] sharedSourceFolders, + bool saveInputs, + bool azureArm, + bool publicClients, + bool modelNamespace, + bool headAsBoolean, + bool skipCSProj, + bool skipCSProjPackageReference, + bool generation1ConvenienceClient, + bool singleTopLevelClient, + bool skipSerializationFormatXml, + bool disablePaginationTopRenaming, + bool generateModelFactory, + bool publicDiscriminatorProperty, + bool deserializeNullCollectionAsNullValue, + bool useCoreDataFactoryReplacements, + bool useModelReaderWriter, + bool enableBicepSerialization, + bool enableInternalRawData, + IReadOnlyList modelFactoryForHlc, + UnreferencedTypesHandlingOption unreferencedTypesHandling, + bool keepNonOverloadableProtocolSignature, + string? projectFolder, + string? existingProjectFolder, + IReadOnlyList protocolMethodList, + IReadOnlyList suppressAbstractBaseClasses, + IReadOnlyList modelsToTreatEmptyStringAsNull, + IReadOnlyList additionalIntrinsicTypesToTreatEmptyStringAsNull, + bool shouldTreatBase64AsBinaryData, + IReadOnlyList methodsToKeepClientDefaultValue, + MgmtConfiguration mgmtConfiguration, + MgmtTestConfiguration? mgmtTestConfiguration, + string? flavor, + bool disableXmlDocs, + bool generateSampleProject, + bool generateTestProject, + string? examplesDirectory, + string? helperNamespace, + string? customHeader) + { + _outputFolder = outputFolder; + _namespace = ns; + _libraryName = libraryName; + _sharedSourceFolders = sharedSourceFolders; + SaveInputs = saveInputs; + AzureArm = azureArm; + PublicClients = publicClients || AzureArm; + ModelNamespace = azureArm || modelNamespace; + HeadAsBoolean = headAsBoolean; + SkipCSProj = skipCSProj; + SkipCSProjPackageReference = skipCSProjPackageReference; + Generation1ConvenienceClient = generation1ConvenienceClient; + + PublicRestClientsTemporaryFlag = generation1ConvenienceClient; + GenerateLongRunningOperationTypes = generation1ConvenienceClient; + GenerateResponseHeaderModels = generation1ConvenienceClient; + + SingleTopLevelClient = singleTopLevelClient; + GenerateModelFactory = generateModelFactory; + PublicDiscriminatorProperty = publicDiscriminatorProperty; + DeserializeNullCollectionAsNullValue = deserializeNullCollectionAsNullValue; + UnreferencedTypesHandling = unreferencedTypesHandling; + KeepNonOverloadableProtocolSignature = keepNonOverloadableProtocolSignature; + ShouldTreatBase64AsBinaryData = !azureArm && !generation1ConvenienceClient ? shouldTreatBase64AsBinaryData : false; + UseCoreDataFactoryReplacements = useCoreDataFactoryReplacements; + UseModelReaderWriter = useModelReaderWriter; + // TODO: remove use-writ-core after we enable all RPs for DPG + EnableBicepSerialization = enableBicepSerialization; + EnableInternalRawData = enableInternalRawData; + projectFolder ??= ProjectFolderDefault; + (_absoluteProjectFolder, _relativeProjectFolder) = ParseProjectFolders(outputFolder, projectFolder); + + ExistingProjectFolder = existingProjectFolder == null ? DownloadLatestContract(_absoluteProjectFolder) : Path.GetFullPath(Path.Combine(_absoluteProjectFolder, existingProjectFolder)); + var isAzureProject = ns.StartsWith("Azure.") || ns.StartsWith("Microsoft.Azure"); + // we only check the combination for Azure projects whose namespace starts with "Azure." or "Microsoft.Azure." + // issue: https://github.com/Azure/autorest.csharp/issues/3179 + if (publicClients && generation1ConvenienceClient && isAzureProject) + { + var binaryLocation = typeof(Configuration).Assembly.Location; + if (!binaryLocation.EndsWith(Path.Combine("artifacts", "bin", "AutoRest.CSharp", "Debug", "net8.0", "AutoRest.CSharp.dll"))) + { + if (_absoluteProjectFolder is not null) + { + //TODO Remove after resolving https://github.com/Azure/autorest.csharp/issues/3151 + var absoluteProjectFolderSPlit = new HashSet(_absoluteProjectFolder.Split(Path.DirectorySeparatorChar), StringComparer.Ordinal); + if (!absoluteProjectFolderSPlit.Contains("src") || + !absoluteProjectFolderSPlit.Contains("Azure.Analytics.Synapse.Spark") && + !absoluteProjectFolderSPlit.Contains("Azure.Analytics.Synapse.Monitoring") && + !absoluteProjectFolderSPlit.Contains("Azure.Analytics.Synapse.ManagedPrivateEndpoints") && + !absoluteProjectFolderSPlit.Contains("Azure.Analytics.Synapse.Artifacts") && + !absoluteProjectFolderSPlit.Contains("Azure.Communication.PhoneNumbers")) + throw new Exception($"Unsupported combination of settings both {Options.PublicClients} and {Options.Generation1ConvenienceClient} cannot be true at the same time."); + } + } + } + + _protocolMethodList = protocolMethodList; + SkipSerializationFormatXml = skipSerializationFormatXml; + DisablePaginationTopRenaming = disablePaginationTopRenaming; + _oldModelFactoryEntries = modelFactoryForHlc; + _mgmtConfiguration = mgmtConfiguration; + MgmtTestConfiguration = mgmtTestConfiguration; + _suppressAbstractBaseClasses = suppressAbstractBaseClasses; + _modelsToTreatEmptyStringAsNull = new HashSet(modelsToTreatEmptyStringAsNull); + _intrinsicTypesToTreatEmptyStringAsNull.UnionWith(additionalIntrinsicTypesToTreatEmptyStringAsNull); + _methodsToKeepClientDefaultValue = methodsToKeepClientDefaultValue ?? Array.Empty(); + _apiTypes = "azure".Equals(flavor, StringComparison.InvariantCultureIgnoreCase) ? new AzureApiTypes() : new SystemApiTypes(); + Flavor = flavor; + DisableXmlDocs = disableXmlDocs; + GenerateSampleProject = DisableXmlDocs ? false : generateSampleProject; // turn off the samples if all xml docs are explicitly disabled + GenerateTestProject = generateTestProject; + ExamplesDirectory = examplesDirectory; + CustomHeader = customHeader; + _helperNamespace = helperNamespace ?? Namespace; + } + + internal static (string AbsoluteProjectFolder, string RelativeProjectFolder) ParseProjectFolders(string outputFolder, string projectFolder) + { + if (Path.IsPathRooted(projectFolder)) + { + return (projectFolder, Path.GetRelativePath(outputFolder, projectFolder)); + } + else + { + return (Path.GetFullPath(Path.Combine(outputFolder, projectFolder)), projectFolder); + } + } + + private static string? DownloadLatestContract(string projectFolder) + { + if (AzureArm || Generation1ConvenienceClient) + { + return null; + } + + int sdkFolderIndex = projectFolder.LastIndexOf("\\sdk\\", StringComparison.InvariantCultureIgnoreCase); + if (sdkFolderIndex == -1) + { + return null; + } + + string rootFolder = projectFolder.Substring(0, sdkFolderIndex); + var scriptPath = Path.Join(rootFolder, "eng", "common", "scripts", "Download-Latest-Contract.ps1"); + if (File.Exists(scriptPath)) + { + string projectDirectory = projectFolder.EndsWith("src") ? Path.GetFullPath(Path.Join(projectFolder, "..")) : projectFolder; + var scriptStartInfo = new ProcessStartInfo("pwsh", $"-ExecutionPolicy ByPass {scriptPath} {projectDirectory}") + { + RedirectStandardOutput = false, + RedirectStandardError = false, + CreateNoWindow = true, + UseShellExecute = false, + WorkingDirectory = rootFolder + }; + var scriptProcess = Process.Start(scriptStartInfo); + if (scriptProcess != null) + { + scriptProcess.WaitForExit(); + + string projectName = new DirectoryInfo(projectDirectory).Name; + string relativeProject = projectFolder.Substring(sdkFolderIndex); + return Path.GetFullPath(Path.Join(rootFolder, "..", "sparse-spec", "sdk", projectName, relativeProject)); + } + } + + return null; + } + + private static string? _helperNamespace; + public static string HelperNamespace => _helperNamespace ?? throw new InvalidOperationException("Configuration has not been initialized"); + + public static string? CustomHeader { get; private set; } + + public static bool DisableXmlDocs { get; private set; } + + public static bool GenerateSampleProject { get; private set; } + + public static bool GenerateTestProject { get; private set; } + + public static string? ExamplesDirectory { get; private set; } + + private static ApiTypes? _apiTypes; + public static ApiTypes ApiTypes => _apiTypes ?? new AzureApiTypes(); + public static bool IsBranded => ApiTypes is AzureApiTypes; + public static string? Flavor { get; private set; } + + public static bool ShouldTreatBase64AsBinaryData { get; private set; } + + public static bool UseCoreDataFactoryReplacements { get; private set; } + + public static bool UseModelReaderWriter { get; private set; } + + public static bool EnableBicepSerialization { get; private set; } + + public static bool EnableInternalRawData { get; private set; } + + private static string? _outputFolder; + public static string OutputFolder => _outputFolder ?? throw new InvalidOperationException("Configuration has not been initialized"); + public static string? ExistingProjectFolder { get; private set; } + + private static string? _namespace; + public static string Namespace => _namespace ?? throw new InvalidOperationException("Configuration has not been initialized"); + + private static string? _libraryName; + public static string LibraryName => _libraryName ?? throw new InvalidOperationException("Configuration has not been initialized"); + + private static string[]? _sharedSourceFolders; + public static string[] SharedSourceFolders => _sharedSourceFolders ?? throw new InvalidOperationException("Configuration has not been initialized"); + public static bool SaveInputs { get; private set; } + public static bool AzureArm { get; private set; } + public static bool PublicClients { get; private set; } + public static bool ModelNamespace { get; private set; } + public static bool HeadAsBoolean { get; private set; } + public static bool SkipCSProj { get; private set; } + public static bool SkipCSProjPackageReference { get; private set; } + public static bool Generation1ConvenienceClient { get; private set; } + public static bool SingleTopLevelClient { get; private set; } + public static bool SkipSerializationFormatXml { get; private set; } + public static bool DisablePaginationTopRenaming { get; private set; } + + // Temporary flag needed in the process of consolidation + // Will be eliminated at the final step + public static bool PublicRestClientsTemporaryFlag { get; private set; } + public static bool GenerateLongRunningOperationTypes { get; private set; } + public static bool GenerateResponseHeaderModels { get; private set; } + + /// + /// Whether we will generate model factory for this library. + /// If true (default), the model factory will be generated. If false, the model factory will not be generated. + /// + public static bool GenerateModelFactory { get; private set; } + + /// + /// Whether we will generate the discriminator property as public or internal. + /// If true, the discriminator property will be public. If false (default), the discriminator property will be internal. + /// + public static bool PublicDiscriminatorProperty { get; private set; } + + /// + /// Whether we should deserialize null collections in the payload as null values if this sets to true. + /// Default value is false, where we will construct an empty collection (ChangeTrackingList or ChangeTrackingDictionary) if we get null value for collections in the payload + /// + public static bool DeserializeNullCollectionAsNullValue { get; private set; } + public static bool KeepNonOverloadableProtocolSignature { get; private set; } + + private static IReadOnlyList? _oldModelFactoryEntries; + /// + /// This is a shim flag that keeps the old behavior of model factory generation. This configuration should be only used on HLC packages. + /// + public static IReadOnlyList ModelFactoryForHlc => _oldModelFactoryEntries ?? throw new InvalidOperationException("Configuration has not been initialized"); + public static UnreferencedTypesHandlingOption UnreferencedTypesHandling { get; private set; } + private static IReadOnlyList? _suppressAbstractBaseClasses; + public static IReadOnlyList SuppressAbstractBaseClasses => _suppressAbstractBaseClasses ?? throw new InvalidOperationException("Configuration has not been initialized"); + + private static IReadOnlyList? _protocolMethodList; + public static IReadOnlyList ProtocolMethodList => _protocolMethodList ?? throw new InvalidOperationException("Configuration has not been initialized"); + + private static HashSet? _modelsToTreatEmptyStringAsNull; + public static HashSet ModelsToTreatEmptyStringAsNull => _modelsToTreatEmptyStringAsNull ?? throw new InvalidOperationException("Configuration has not been initialized"); + + private static HashSet _intrinsicTypesToTreatEmptyStringAsNull = new HashSet() { nameof(Uri), nameof(Guid), nameof(ResourceIdentifier), nameof(DateTimeOffset) }; + public static HashSet IntrinsicTypesToTreatEmptyStringAsNull => _intrinsicTypesToTreatEmptyStringAsNull; + public static IReadOnlyList? _methodsToKeepClientDefaultValue; + public static IReadOnlyList MethodsToKeepClientDefaultValue => _methodsToKeepClientDefaultValue ??= Array.Empty(); + private static MgmtConfiguration? _mgmtConfiguration; + public static MgmtConfiguration MgmtConfiguration => _mgmtConfiguration ?? throw new InvalidOperationException("Configuration has not been initialized"); + + public static MgmtTestConfiguration? MgmtTestConfiguration { get; private set; } + + private static string? _relativeProjectFolder; + public static string RelativeProjectFolder => _relativeProjectFolder ?? throw new InvalidOperationException("Configuration has not been initialized"); + private static string? _absoluteProjectFolder; + public static string AbsoluteProjectFolder => _absoluteProjectFolder ?? throw new InvalidOperationException("Configuration has not been initialized"); + + public static void Initialize(IPluginCommunication autoRest, string defaultNamespace, string defaultLibraryName) + { + Initialize( + outputFolder: GetOutputFolderOption(autoRest), + ns: GetNamespaceOption(autoRest, defaultNamespace), + libraryName: GetLibraryNameOption(autoRest, defaultLibraryName), + sharedSourceFolders: GetRequiredOption(autoRest, Options.SharedSourceFolders).Select(TrimFileSuffix).ToArray(), + saveInputs: GetOptionBoolValue(autoRest, Options.SaveInputs), + azureArm: GetAzureArmOption(autoRest), + publicClients: GetOptionBoolValue(autoRest, Options.PublicClients), + modelNamespace: GetOptionBoolValue(autoRest, Options.ModelNamespace), + headAsBoolean: GetOptionBoolValue(autoRest, Options.HeadAsBoolean), + skipCSProj: GetOptionBoolValue(autoRest, Options.SkipCSProj), + skipCSProjPackageReference: GetSkipCSProjPackageReferenceOption(autoRest), + generation1ConvenienceClient: GetGeneration1ConvenienceClientOption(autoRest), + singleTopLevelClient: GetOptionBoolValue(autoRest, Options.SingleTopLevelClient), + skipSerializationFormatXml: GetOptionBoolValue(autoRest, Options.SkipSerializationFormatXml), + disablePaginationTopRenaming: GetOptionBoolValue(autoRest, Options.DisablePaginationTopRenaming), + generateModelFactory: GetOptionBoolValue(autoRest, Options.GenerateModelFactory), + publicDiscriminatorProperty: GetOptionBoolValue(autoRest, Options.PublicDiscriminatorProperty), + deserializeNullCollectionAsNullValue: GetOptionBoolValue(autoRest, Options.DeserializeNullCollectionAsNullValue), + modelFactoryForHlc: autoRest.GetValue(Options.ModelFactoryForHlc).GetAwaiter().GetResult() ?? Array.Empty(), + unreferencedTypesHandling: GetOptionEnumValue(autoRest, Options.UnreferencedTypesHandling), + keepNonOverloadableProtocolSignature: GetOptionBoolValue(autoRest, Options.KeepNonOverloadableProtocolSignature), + useCoreDataFactoryReplacements: GetOptionBoolValue(autoRest, Options.UseCoreDataFactoryReplacements), + useModelReaderWriter: GetOptionBoolValue(autoRest, Options.UseModelReaderWriter), + enableBicepSerialization: GetOptionBoolValue(autoRest, Options.EnableBicepSerialization), + enableInternalRawData: GetOptionBoolValue(autoRest, Options.EnableInternalRawData), + projectFolder: GetProjectFolderOption(autoRest), + existingProjectFolder: autoRest.GetValue(Options.ExistingProjectfolder).GetAwaiter().GetResult(), + protocolMethodList: autoRest.GetValue(Options.ProtocolMethodList).GetAwaiter().GetResult() ?? Array.Empty(), + suppressAbstractBaseClasses: autoRest.GetValue(Options.SuppressAbstractBaseClasses).GetAwaiter().GetResult() ?? Array.Empty(), + modelsToTreatEmptyStringAsNull: autoRest.GetValue(Options.ModelsToTreatEmptyStringAsNull).GetAwaiter().GetResult() ?? Array.Empty(), + additionalIntrinsicTypesToTreatEmptyStringAsNull: autoRest.GetValue(Options.AdditionalIntrinsicTypesToTreatEmptyStringAsNull).GetAwaiter().GetResult() ?? Array.Empty(), + shouldTreatBase64AsBinaryData: GetOptionBoolValue(autoRest, Options.ShouldTreatBase64AsBinaryData), + methodsToKeepClientDefaultValue: autoRest.GetValue(Options.MethodsToKeepClientDefaultValue).GetAwaiter().GetResult() ?? Array.Empty(), + mgmtConfiguration: MgmtConfiguration.GetConfiguration(autoRest), + mgmtTestConfiguration: MgmtTestConfiguration.GetConfiguration(autoRest), + flavor: autoRest.GetValue(Options.Flavor).GetAwaiter().GetResult() ?? "azure", // for autorest input, always branded + generateSampleProject: GetOptionBoolValue(autoRest, Options.GenerateSampleProject), + generateTestProject: GetOptionBoolValue(autoRest, Options.GenerateTestProject), + examplesDirectory: null, // TODO -- what we put here? + helperNamespace: autoRest.GetValue(Options.HelperNamespace).GetAwaiter().GetResult(), + disableXmlDocs: GetOptionBoolValue(autoRest, Options.DisableXmlDocs), + customHeader: autoRest.GetValue(Options.CustomHeader).GetAwaiter().GetResult() + ); + } + + private static T GetOptionEnumValue(IPluginCommunication autoRest, string option) where T : struct, Enum + { + var enumStr = autoRest.GetValue(option).GetAwaiter().GetResult(); + return GetOptionEnumValueFromString(option, enumStr); + } + + internal static T GetOptionEnumValueFromString(string option, string? enumStrValue) where T : struct, Enum + { + if (Enum.TryParse(enumStrValue, true, out var enumValue)) + { + return enumValue; + } + + return (T)GetDefaultEnumOptionValue(option)!; + } + + public static Enum? GetDefaultEnumOptionValue(string option) => option switch + { + Options.UnreferencedTypesHandling => UnreferencedTypesHandlingOption.RemoveOrInternalize, + _ => null + }; + + private static bool GetOptionBoolValue(IPluginCommunication autoRest, string option) + { + return autoRest.GetValue(option).GetAwaiter().GetResult() ?? GetDefaultBoolOptionValue(option)!.Value; + } + + public static bool? GetDefaultBoolOptionValue(string option) + { + switch (option) + { + case Options.SaveInputs: + return false; + case Options.AzureArm: + return false; + case Options.PublicClients: + return false; + case Options.ModelNamespace: + return true; + case Options.HeadAsBoolean: + return false; + case Options.SkipCSProj: + return false; + case Options.SkipCSProjPackageReference: + return false; + case Options.Generation1ConvenienceClient: + return false; + case Options.SingleTopLevelClient: + return false; + case Options.SkipSerializationFormatXml: + return false; + case Options.DisablePaginationTopRenaming: + return false; + case Options.GenerateModelFactory: + return true; + case Options.PublicDiscriminatorProperty: + return false; + case Options.KeepNonOverloadableProtocolSignature: + return false; + case Options.ShouldTreatBase64AsBinaryData: + return true; + case Options.DeserializeNullCollectionAsNullValue: + return false; + case Options.UseCoreDataFactoryReplacements: + return true; + case Options.GenerateSampleProject: + return true; + case Options.GenerateTestProject: + return false; + case Options.UseModelReaderWriter: + return false; + case Options.EnableBicepSerialization: + return false; + case Options.DisableXmlDocs: + return false; + case Options.EnableInternalRawData: + return false; + default: + return null; + } + } + + private static T GetRequiredOption(IPluginCommunication autoRest, string name) + { + return autoRest.GetValue(name).GetAwaiter().GetResult() ?? throw new InvalidOperationException($"{name} configuration parameter is required"); + } + + internal static string TrimFileSuffix(string path) + { + if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) + { + path = new Uri(path).LocalPath; + } + + return path; + } + + internal static bool IsValidJsonElement(JsonElement? element) + { + return element != null && element?.ValueKind != JsonValueKind.Null && element?.ValueKind != JsonValueKind.Undefined; + } + + public static bool DeserializeBoolean(JsonElement? jsonElement, bool defaultValue = false) + => jsonElement == null || !IsValidJsonElement(jsonElement) ? defaultValue : Convert.ToBoolean(jsonElement.ToString()); + + public static IReadOnlyList DeserializeArray(JsonElement jsonElement) + => jsonElement.ValueKind != JsonValueKind.Array ? Array.Empty() : jsonElement.EnumerateArray().Select(t => t.ToString()).ToArray(); + + internal static void LoadConfiguration(JsonElement root, string? projectPath, string outputPath, string? existingProjectFolder) + { + var sharedSourceFolders = new List(); + foreach (var sharedSourceFolder in root.GetProperty(Options.SharedSourceFolders).EnumerateArray()) + { + sharedSourceFolders.Add(Path.Combine(outputPath, sharedSourceFolder.GetString()!)); + } + + root.TryGetProperty(Options.ProtocolMethodList, out var protocolMethodList); + var protocolMethods = DeserializeArray(protocolMethodList); + root.TryGetProperty(Options.SuppressAbstractBaseClasses, out var suppressAbstractBaseClassesElement); + var suppressAbstractBaseClasses = DeserializeArray(suppressAbstractBaseClassesElement); + root.TryGetProperty(Options.ModelsToTreatEmptyStringAsNull, out var modelsToTreatEmptyStringAsNullElement); + var modelsToTreatEmptyStringAsNull = DeserializeArray(modelsToTreatEmptyStringAsNullElement); + root.TryGetProperty(Options.IntrinsicTypesToTreatEmptyStringAsNull, out var intrinsicTypesToTreatEmptyStringAsNullElement); + var intrinsicTypesToTreatEmptyStringAsNull = DeserializeArray(intrinsicTypesToTreatEmptyStringAsNullElement); + root.TryGetProperty(Options.ModelFactoryForHlc, out var oldModelFactoryEntriesElement); + var oldModelFactoryEntries = DeserializeArray(oldModelFactoryEntriesElement); + root.TryGetProperty(Options.MethodsToKeepClientDefaultValue, out var methodsToKeepClientDefaultValueElement); + var methodsToKeepClientDefaultValue = DeserializeArray(methodsToKeepClientDefaultValueElement); + + Initialize( + Path.Combine(outputPath, root.GetProperty(Options.OutputFolder).GetString()!), + root.GetProperty(Options.Namespace).GetString()!, + root.GetProperty(Options.LibraryName).GetString()!, + sharedSourceFolders.ToArray(), + saveInputs: false, + azureArm: ReadOption(root, Options.AzureArm), + publicClients: ReadOption(root, Options.PublicClients), + modelNamespace: ReadOption(root, Options.ModelNamespace), + headAsBoolean: ReadOption(root, Options.HeadAsBoolean), + skipCSProj: ReadOption(root, Options.SkipCSProj), + skipCSProjPackageReference: ReadOption(root, Options.SkipCSProjPackageReference), + generation1ConvenienceClient: ReadOption(root, Options.Generation1ConvenienceClient), + singleTopLevelClient: ReadOption(root, Options.SingleTopLevelClient), + skipSerializationFormatXml: ReadOption(root, Options.SkipSerializationFormatXml), + disablePaginationTopRenaming: ReadOption(root, Options.DisablePaginationTopRenaming), + generateModelFactory: ReadOption(root, Options.GenerateModelFactory), + publicDiscriminatorProperty: ReadOption(root, Options.PublicDiscriminatorProperty), + deserializeNullCollectionAsNullValue: ReadOption(root, Options.DeserializeNullCollectionAsNullValue), + useCoreDataFactoryReplacements: ReadOption(root, Options.UseCoreDataFactoryReplacements), + useModelReaderWriter: ReadOption(root, Options.UseModelReaderWriter), + enableBicepSerialization: ReadOption(root, Options.EnableBicepSerialization), + enableInternalRawData: ReadOption(root, Options.EnableInternalRawData), + modelFactoryForHlc: oldModelFactoryEntries, + unreferencedTypesHandling: ReadEnumOption(root, Options.UnreferencedTypesHandling), + keepNonOverloadableProtocolSignature: ReadOption(root, Options.KeepNonOverloadableProtocolSignature), + projectFolder: projectPath ?? ReadStringOption(root, Options.ProjectFolder), + existingProjectFolder: existingProjectFolder, + protocolMethodList: protocolMethods, + suppressAbstractBaseClasses: suppressAbstractBaseClasses, + modelsToTreatEmptyStringAsNull: modelsToTreatEmptyStringAsNull, + additionalIntrinsicTypesToTreatEmptyStringAsNull: intrinsicTypesToTreatEmptyStringAsNull, + shouldTreatBase64AsBinaryData: ReadOption(root, Options.ShouldTreatBase64AsBinaryData), + methodsToKeepClientDefaultValue: methodsToKeepClientDefaultValue, + mgmtConfiguration: MgmtConfiguration.LoadConfiguration(root), + mgmtTestConfiguration: MgmtTestConfiguration.LoadConfiguration(root), + flavor: ReadStringOption(root, Options.Flavor), + disableXmlDocs: ReadOption(root, Options.DisableXmlDocs), + generateSampleProject: ReadOption(root, Options.GenerateSampleProject), + generateTestProject: ReadOption(root, Options.GenerateTestProject), + examplesDirectory: ReadStringOption(root, Options.ExamplesDirectory), + helperNamespace: ReadStringOption(root, Options.HelperNamespace), + customHeader: ReadStringOption(root, Options.CustomHeader)); + } + + internal static string SaveConfiguration() + { + using (var memoryStream = new MemoryStream()) + { + var options = new JsonWriterOptions() + { + Indented = true, + }; + using (var writer = new Utf8JsonWriter(memoryStream, options)) + { + WriteConfiguration(writer); + } + + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + } + + private static void WriteConfiguration(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString(Options.OutputFolder, Path.GetRelativePath(OutputFolder, OutputFolder)); + writer.WriteString(Options.Namespace, Namespace); + writer.WriteString(Options.LibraryName, LibraryName); + writer.WriteStartArray(Options.SharedSourceFolders); + foreach (var sharedSourceFolder in SharedSourceFolders) + { + writer.WriteStringValue(NormalizePath(sharedSourceFolder)); + } + writer.WriteEndArray(); + WriteIfNotDefault(writer, Options.AzureArm, AzureArm); + WriteIfNotDefault(writer, Options.PublicClients, PublicClients); + WriteIfNotDefault(writer, Options.ModelNamespace, ModelNamespace); + WriteIfNotDefault(writer, Options.HeadAsBoolean, HeadAsBoolean); + WriteIfNotDefault(writer, Options.SkipCSProj, SkipCSProj); + WriteIfNotDefault(writer, Options.SkipCSProjPackageReference, SkipCSProjPackageReference); + WriteIfNotDefault(writer, Options.Generation1ConvenienceClient, Generation1ConvenienceClient); + WriteIfNotDefault(writer, Options.SingleTopLevelClient, SingleTopLevelClient); + WriteIfNotDefault(writer, Options.GenerateModelFactory, GenerateModelFactory); + WriteNonEmptyArray(writer, Options.ModelFactoryForHlc, ModelFactoryForHlc); + WriteIfNotDefault(writer, Options.UnreferencedTypesHandling, UnreferencedTypesHandling); + WriteIfNotDefault(writer, Options.ProjectFolder, RelativeProjectFolder); + WriteIfNotDefault(writer, Options.UseCoreDataFactoryReplacements, UseCoreDataFactoryReplacements); + WriteIfNotDefault(writer, Options.UseModelReaderWriter, UseModelReaderWriter); + WriteIfNotDefault(writer, Options.EnableBicepSerialization, EnableBicepSerialization); + WriteNonEmptyArray(writer, Options.ProtocolMethodList, ProtocolMethodList); + WriteNonEmptyArray(writer, Options.SuppressAbstractBaseClasses, SuppressAbstractBaseClasses); + WriteNonEmptyArray(writer, Options.ModelsToTreatEmptyStringAsNull, ModelsToTreatEmptyStringAsNull.ToList()); + if (ModelsToTreatEmptyStringAsNull.Any()) + { + WriteNonEmptyArray(writer, Options.IntrinsicTypesToTreatEmptyStringAsNull, IntrinsicTypesToTreatEmptyStringAsNull.ToList()); + } + + MgmtConfiguration.SaveConfiguration(writer); + + if (MgmtTestConfiguration != null) + { + MgmtTestConfiguration.SaveConfiguration(writer); + } + if (Flavor != null) + { + writer.WriteString(Options.Flavor, Flavor); + } + WriteIfNotDefault(writer, Options.GenerateSampleProject, GenerateSampleProject); + WriteIfNotDefault(writer, Options.GenerateTestProject, GenerateTestProject); + WriteIfNotDefault(writer, Options.HelperNamespace, HelperNamespace); + WriteIfNotDefault(writer, Options.DisableXmlDocs, DisableXmlDocs); + WriteIfNotDefault(writer, Options.CustomHeader, CustomHeader); + + writer.WriteEndObject(); + } + + private static string NormalizePath(string sharedSourceFolder) + { + return Path.GetRelativePath(OutputFolder, sharedSourceFolder); + } + + private static void WriteIfNotDefault(Utf8JsonWriter writer, string option, T enumValue) where T : struct, Enum + { + var defaultValue = GetDefaultEnumOptionValue(option); + if (!enumValue.Equals(defaultValue)) + { + writer.WriteString(option, enumValue.ToString()); + } + } + + private static void WriteIfNotDefault(Utf8JsonWriter writer, string option, bool value) + { + var defaultValue = GetDefaultBoolOptionValue(option); + if (!defaultValue.HasValue || defaultValue.Value != value) + { + writer.WriteBoolean(option, value); + } + } + + private static void WriteIfNotDefault(Utf8JsonWriter writer, string option, string? value) + { + if (value == null) + { + return; + } + + switch (option) + { + case Options.ProjectFolder: + if (value != ProjectFolderDefault) + writer.WriteString(option, value); + break; + default: + writer.WriteString(option, value); + break; + } + } + + private static void WriteNonEmptyArray(Utf8JsonWriter writer, string name, IReadOnlyList values) + { + if (values.Any()) + { + writer.WriteStartArray(name); + foreach (var s in values) + { + writer.WriteStringValue(s); + } + + writer.WriteEndArray(); + } + } + + private static bool ReadOption(JsonElement root, string option) + { + if (root.TryGetProperty(option, out JsonElement value)) + { + return value.GetBoolean(); + } + else + { + return GetDefaultBoolOptionValue(option)!.Value; + } + } + + private static T ReadEnumOption(JsonElement root, string option) where T : struct, Enum + { + var enumStr = ReadStringOption(root, option); + return GetOptionEnumValueFromString(option, enumStr); + } + + private static string? ReadStringOption(JsonElement root, string option) + { + if (root.TryGetProperty(option, out JsonElement value)) + return value.GetString(); + + return null; + } + + internal static string GetOutputFolderOption(IPluginCommunication autoRest) => TrimFileSuffix(GetRequiredOption(autoRest, Options.OutputFolder)); + internal static string? GetProjectFolderOption(IPluginCommunication autoRest) => autoRest.GetValue(Options.ProjectFolder).GetAwaiter().GetResult(); + internal static bool GetAzureArmOption(IPluginCommunication autoRest) => GetOptionBoolValue(autoRest, Options.AzureArm); + internal static bool GetSkipCSProjPackageReferenceOption(IPluginCommunication autoRest) => GetOptionBoolValue(autoRest, Options.SkipCSProjPackageReference); + internal static bool GetGeneration1ConvenienceClientOption(IPluginCommunication autoRest) => GetOptionBoolValue(autoRest, Options.Generation1ConvenienceClient); + internal static string GetLibraryNameOption(IPluginCommunication autoRest, string defaultLibraryName) => autoRest.GetValue(Options.LibraryName).GetAwaiter().GetResult() ?? defaultLibraryName; + internal static string GetNamespaceOption(IPluginCommunication autoRest, string defaultNamespace) => autoRest.GetValue(Options.Namespace).GetAwaiter().GetResult() ?? defaultNamespace; + } +} diff --git a/logger/autorest.csharp/common/Input/Generated/CodeModel.cs b/logger/autorest.csharp/common/Input/Generated/CodeModel.cs new file mode 100644 index 0000000..756d839 --- /dev/null +++ b/logger/autorest.csharp/common/Input/Generated/CodeModel.cs @@ -0,0 +1,2906 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.0.23.0 (Newtonsoft.Json v9.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + +namespace AutoRest.CSharp.Input +{ + #pragma warning disable // Disable all warnings + + /// - since API version formats range from + /// Azure ARM API date style (2018-01-01) to semver (1.2.3) + /// and virtually any other text, this value tends to be an + /// opaque string with the possibility of a modifier to indicate + /// that it is a range. + /// + /// options: + /// - prepend a dash or append a plus to indicate a range + /// (ie, '2018-01-01+' or '-2019-01-01', or '1.0+' ) + /// + /// - semver-range style (ie, '^1.0.0' or '~1.0.0' ) + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ApiVersion + { + /// the actual api version string used in the API + [YamlDotNet.Serialization.YamlMember(Alias = "version")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Version { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "range")] + public ApiVersionRange? Range { get; set; } + } + + /// a collection of api versions + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ApiVersions : System.Collections.ObjectModel.Collection + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Schema + { + /// per-language information for Schema + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// the schema type + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public AllSchemaTypes Type { get; set; } + + /// a short description + [YamlDotNet.Serialization.YamlMember(Alias = "summary")] + public string? Summary { get; set; } + + /// example information + [YamlDotNet.Serialization.YamlMember(Alias = "example")] + public object? Example { get; set; } + + /// If the value isn't sent on the wire, the service will assume this + [YamlDotNet.Serialization.YamlMember(Alias = "defaultValue")] + public object? DefaultValue { get; set; } + + /// per-serialization information for this Schema + [YamlDotNet.Serialization.YamlMember(Alias = "serialization")] + public SerializationFormats? Serialization { get; set; } + + /// API versions that this applies to. Undefined means all versions + [YamlDotNet.Serialization.YamlMember(Alias = "apiVersions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection ApiVersions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Represent the deprecation information if api is deprecated. + [YamlDotNet.Serialization.YamlMember(Alias = "deprecated")] + public Deprecation? Deprecated { get; set; } + + /// where did this aspect come from (jsonpath or 'modelerfour:<soemthing>') + [YamlDotNet.Serialization.YamlMember(Alias = "origin")] + public string? Origin { get; set; } + + /// External Documentation Links + [YamlDotNet.Serialization.YamlMember(Alias = "externalDocs")] + public ExternalDocumentation? ExternalDocs { get; set; } + + /// per-protocol information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "protocol")] + [System.ComponentModel.DataAnnotations.Required] + public Protocols Protocol { get; set; } = new Protocols(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// Represent information about a deprecation + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Deprecation + { + /// Reason why this was deprecated. + [YamlDotNet.Serialization.YamlMember(Alias = "reason")] + public string? Reason { get; set; } + } + + /// A dictionary of open-ended 'x-*' extensions propogated from the original source document. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Extensions + { + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions1 { get; set; } + } + + /// a reference to external documentation + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ExternalDocumentation + { + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "url")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Url { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// custom extensible metadata for individual language generators + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Languages + { + [YamlDotNet.Serialization.YamlMember(Alias = "default")] + [System.ComponentModel.DataAnnotations.Required] + public Language Default { get; set; } = new Language(); + + [YamlDotNet.Serialization.YamlMember(Alias = "csharp")] + public Language? CSharp { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "python")] + public Language? Python { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "ruby")] + public Language? Ruby { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "go")] + public Language? Go { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "typescript")] + public Language? Typescript { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "javascript")] + public Language? Javascript { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "powershell")] + public Language? Powershell { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "java")] + public Language? Java { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "c")] + public Language? C { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "cpp")] + public Language? Cpp { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "swift")] + public Language? Swift { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "objectivec")] + public Language? Objectivec { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "sputnik")] + public Language? Sputnik { get; set; } + } + + /// custom extensible metadata for individual protocols (ie, HTTP, etc) + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Protocols + { + [YamlDotNet.Serialization.YamlMember(Alias = "http")] + public Protocol? Http { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "amqp")] + public Protocol? Amqp { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "mqtt")] + public Protocol? Mqtt { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "jsonrpc")] + public Protocol? Jsonrpc { get; set; } + } + + /// common pattern for Metadata on aspects + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Metadata + { + /// per-language information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// per-protocol information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "protocol")] + [System.ComponentModel.DataAnnotations.Required] + public Protocols Protocol { get; set; } = new Protocols(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// the bare-minimum fields for per-language metadata on a given aspect + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Language + { + /// name used in actual implementation + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + /// description text - describes this node. + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Description { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class CSharpLanguage + { + } + + /// the bare-minimum fields for per-protocol metadata on a given aspect + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Protocol + { + } + + /// contact information + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Contact + { + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + public string? Name { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "url")] + public string? Url { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "email")] + public string? Email { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// license information + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class License + { + /// the nameof the license + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + /// an uri pointing to the full license text + [YamlDotNet.Serialization.YamlMember(Alias = "url")] + public string? Url { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// code model information + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Info + { + /// the title of this service. + [YamlDotNet.Serialization.YamlMember(Alias = "title")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Title { get; set; } + + /// a text description of the service + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + /// an uri to the terms of service specified to access the service + [YamlDotNet.Serialization.YamlMember(Alias = "termsOfService")] + public string? TermsOfService { get; set; } + + /// contact information for the service + [YamlDotNet.Serialization.YamlMember(Alias = "contact")] + public Contact? Contact { get; set; } + + /// license information for th service + [YamlDotNet.Serialization.YamlMember(Alias = "license")] + public License? License { get; set; } + + /// External Documentation + [YamlDotNet.Serialization.YamlMember(Alias = "externalDocs")] + public ExternalDocumentation? ExternalDocs { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class XmlSerlializationFormat : SerializationFormatMetadata + { + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + public string? Name { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "namespace")] + public string? Namespace { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "prefix")] + public string? Prefix { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "attribute")] + public bool? Attribute { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "wrapped")] + public bool? Wrapped { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "text")] + public bool? Text { get; set; } + } + + /// custom extensible metadata for individual serialization formats + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SerializationFormats + { + [YamlDotNet.Serialization.YamlMember(Alias = "json")] + public SerializationFormatMetadata? Json { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "xml")] + public XmlSerlializationFormat? Xml { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "protobuf")] + public SerializationFormatMetadata? Protobuf { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "binary")] + public SerializationFormatMetadata? Binary { get; set; } + } + + /// possible schema types that indicate the type of schema. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum SchemaType + { + [System.Runtime.Serialization.EnumMember(Value = @"any")] + Any = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"any-object")] + AnyObject = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"arm-id")] + ArmId = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"array")] + Array = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"binary")] + Binary = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"boolean")] + Boolean = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"byte-array")] + ByteArray = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"char")] + Char = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"choice")] + Choice = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"conditional")] + Conditional = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"constant")] + Constant = 10, + + [System.Runtime.Serialization.EnumMember(Value = @"credential")] + Credential = 11, + + [System.Runtime.Serialization.EnumMember(Value = @"date")] + Date = 12, + + [System.Runtime.Serialization.EnumMember(Value = @"date-time")] + DateTime = 13, + + [System.Runtime.Serialization.EnumMember(Value = @"dictionary")] + Dictionary = 14, + + [System.Runtime.Serialization.EnumMember(Value = @"duration")] + Duration = 15, + + [System.Runtime.Serialization.EnumMember(Value = @"flag")] + Flag = 16, + + [System.Runtime.Serialization.EnumMember(Value = @"group")] + Group = 17, + + [System.Runtime.Serialization.EnumMember(Value = @"integer")] + Integer = 18, + + [System.Runtime.Serialization.EnumMember(Value = @"not")] + Not = 19, + + [System.Runtime.Serialization.EnumMember(Value = @"number")] + Number = 20, + + [System.Runtime.Serialization.EnumMember(Value = @"object")] + Object = 21, + + [System.Runtime.Serialization.EnumMember(Value = @"odata-query")] + OdataQuery = 22, + + [System.Runtime.Serialization.EnumMember(Value = @"or")] + Or = 23, + + [System.Runtime.Serialization.EnumMember(Value = @"sealed-choice")] + SealedChoice = 24, + + [System.Runtime.Serialization.EnumMember(Value = @"sealed-conditional")] + SealedConditional = 25, + + [System.Runtime.Serialization.EnumMember(Value = @"string")] + String = 26, + + [System.Runtime.Serialization.EnumMember(Value = @"time")] + Time = 27, + + [System.Runtime.Serialization.EnumMember(Value = @"unixtime")] + Unixtime = 28, + + [System.Runtime.Serialization.EnumMember(Value = @"unknown")] + Unknown = 29, + + [System.Runtime.Serialization.EnumMember(Value = @"uri")] + Uri = 30, + + [System.Runtime.Serialization.EnumMember(Value = @"uuid")] + Uuid = 31, + + [System.Runtime.Serialization.EnumMember(Value = @"xor")] + Xor = 32, + } + + /// Compound schemas are used to construct complex objects or offer choices of a set of schemas. + /// + /// (ie, allOf, anyOf, oneOf ) + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum CompoundSchemaTypes + { + [System.Runtime.Serialization.EnumMember(Value = @"or")] + Or = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"xor")] + Xor = 1, + } + + /// Schema types that are primitive language values + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum PrimitiveSchemaTypes + { + [System.Runtime.Serialization.EnumMember(Value = @"arm-id")] + ArmId = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"boolean")] + Boolean = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"char")] + Char = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"credential")] + Credential = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"date")] + Date = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"date-time")] + DateTime = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"duration")] + Duration = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"integer")] + Integer = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"number")] + Number = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"string")] + String = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"time")] + Time = 10, + + [System.Runtime.Serialization.EnumMember(Value = @"unixtime")] + Unixtime = 11, + + [System.Runtime.Serialization.EnumMember(Value = @"uri")] + Uri = 12, + + [System.Runtime.Serialization.EnumMember(Value = @"uuid")] + Uuid = 13, + } + + /// schema types that are non-object or complex types + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum ValueSchemaTypes + { + [System.Runtime.Serialization.EnumMember(Value = @"arm-id")] + ArmId = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"array")] + Array = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"boolean")] + Boolean = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"byte-array")] + ByteArray = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"char")] + Char = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"choice")] + Choice = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"conditional")] + Conditional = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"credential")] + Credential = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"date")] + Date = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"date-time")] + DateTime = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"duration")] + Duration = 10, + + [System.Runtime.Serialization.EnumMember(Value = @"flag")] + Flag = 11, + + [System.Runtime.Serialization.EnumMember(Value = @"integer")] + Integer = 12, + + [System.Runtime.Serialization.EnumMember(Value = @"number")] + Number = 13, + + [System.Runtime.Serialization.EnumMember(Value = @"sealed-choice")] + SealedChoice = 14, + + [System.Runtime.Serialization.EnumMember(Value = @"sealed-conditional")] + SealedConditional = 15, + + [System.Runtime.Serialization.EnumMember(Value = @"string")] + String = 16, + + [System.Runtime.Serialization.EnumMember(Value = @"time")] + Time = 17, + + [System.Runtime.Serialization.EnumMember(Value = @"unixtime")] + Unixtime = 18, + + [System.Runtime.Serialization.EnumMember(Value = @"uri")] + Uri = 19, + + [System.Runtime.Serialization.EnumMember(Value = @"uuid")] + Uuid = 20, + } + + /// schema types that can be objects + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum ObjectSchemaTypes + { + [System.Runtime.Serialization.EnumMember(Value = @"dictionary")] + Dictionary = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"object")] + Object = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"or")] + Or = 2, + } + + /// all schema types + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum AllSchemaTypes + { + [System.Runtime.Serialization.EnumMember(Value = @"any")] + Any = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"any-object")] + AnyObject = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"arm-id")] + ArmId = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"array")] + Array = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"binary")] + Binary = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"boolean")] + Boolean = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"byte-array")] + ByteArray = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"char")] + Char = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"choice")] + Choice = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"conditional")] + Conditional = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"constant")] + Constant = 10, + + [System.Runtime.Serialization.EnumMember(Value = @"credential")] + Credential = 11, + + [System.Runtime.Serialization.EnumMember(Value = @"date")] + Date = 12, + + [System.Runtime.Serialization.EnumMember(Value = @"date-time")] + DateTime = 13, + + [System.Runtime.Serialization.EnumMember(Value = @"dictionary")] + Dictionary = 14, + + [System.Runtime.Serialization.EnumMember(Value = @"duration")] + Duration = 15, + + [System.Runtime.Serialization.EnumMember(Value = @"flag")] + Flag = 16, + + [System.Runtime.Serialization.EnumMember(Value = @"group")] + Group = 17, + + [System.Runtime.Serialization.EnumMember(Value = @"integer")] + Integer = 18, + + [System.Runtime.Serialization.EnumMember(Value = @"not")] + Not = 19, + + [System.Runtime.Serialization.EnumMember(Value = @"number")] + Number = 20, + + [System.Runtime.Serialization.EnumMember(Value = @"object")] + Object = 21, + + [System.Runtime.Serialization.EnumMember(Value = @"odata-query")] + OdataQuery = 22, + + [System.Runtime.Serialization.EnumMember(Value = @"or")] + Or = 23, + + [System.Runtime.Serialization.EnumMember(Value = @"sealed-choice")] + SealedChoice = 24, + + [System.Runtime.Serialization.EnumMember(Value = @"sealed-conditional")] + SealedConditional = 25, + + [System.Runtime.Serialization.EnumMember(Value = @"string")] + String = 26, + + [System.Runtime.Serialization.EnumMember(Value = @"time")] + Time = 27, + + [System.Runtime.Serialization.EnumMember(Value = @"unixtime")] + Unixtime = 28, + + [System.Runtime.Serialization.EnumMember(Value = @"uri")] + Uri = 29, + + [System.Runtime.Serialization.EnumMember(Value = @"uuid")] + Uuid = 30, + + [System.Runtime.Serialization.EnumMember(Value = @"xor")] + Xor = 31, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SerializationFormatMetadata + { + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// schema types that are non-object or complex types + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ValueSchema : Schema + { + } + + /// Schema types that are primitive language values + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class PrimitiveSchema : ValueSchema + { + } + + /// schema types that can be objects + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ComplexSchema : Schema + { + } + + /// common base interface for properties, parameters and the like. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Value + { + /// the schema of this Value + [YamlDotNet.Serialization.YamlMember(Alias = "schema")] + [System.ComponentModel.DataAnnotations.Required] + public Schema Schema { get; set; } = new Schema(); + + /// if the value is marked 'required'. + [YamlDotNet.Serialization.YamlMember(Alias = "required")] + public bool? Required { get; set; } + + /// can null be passed in instead + [YamlDotNet.Serialization.YamlMember(Alias = "nullable")] + public bool? Nullable { get; set; } + + /// the value that the remote will assume if this value is not present + [YamlDotNet.Serialization.YamlMember(Alias = "assumedValue")] + public object? AssumedValue { get; set; } + + /// the value that the client should provide if the consumer doesn't provide one + [YamlDotNet.Serialization.YamlMember(Alias = "clientDefaultValue")] + public object? ClientDefaultValue { get; set; } + + /// a short description + [YamlDotNet.Serialization.YamlMember(Alias = "summary")] + public string? Summary { get; set; } + + /// API versions that this applies to. Undefined means all versions + [YamlDotNet.Serialization.YamlMember(Alias = "apiVersions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection ApiVersions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Represent the deprecation information if api is deprecated. + [YamlDotNet.Serialization.YamlMember(Alias = "deprecated")] + public Deprecation? Deprecated { get; set; } + + /// where did this aspect come from (jsonpath or 'modelerfour:<soemthing>') + [YamlDotNet.Serialization.YamlMember(Alias = "origin")] + public string? Origin { get; set; } + + /// External Documentation Links + [YamlDotNet.Serialization.YamlMember(Alias = "externalDocs")] + public ExternalDocumentation? ExternalDocs { get; set; } + + /// per-language information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// per-protocol information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "protocol")] + [System.ComponentModel.DataAnnotations.Required] + public Protocols Protocol { get; set; } = new Protocols(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// a property is a child value in an object + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Property : Value + { + /// if the property is marked read-only (ie, not intended to be sent to the service) + [YamlDotNet.Serialization.YamlMember(Alias = "readOnly")] + public bool? ReadOnly { get; set; } + + /// the wire name of this property + [YamlDotNet.Serialization.YamlMember(Alias = "serializedName")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string SerializedName { get; set; } + + /// when a property is flattened, the property will be the set of serialized names to get to that target property. + /// + /// If flattenedName is present, then this property is a flattened property. + /// + /// (ie, ['properties','name'] ) + [YamlDotNet.Serialization.YamlMember(Alias = "flattenedNames")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection FlattenedNames { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// if this property is used as a discriminator for a polymorphic type + [YamlDotNet.Serialization.YamlMember(Alias = "isDiscriminator")] + public bool? IsDiscriminator { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum ImplementationLocation + { + [System.Runtime.Serialization.EnumMember(Value = @"Client")] + Client = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Context")] + Context = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"Method")] + Method = 2, + } + + /// a definition of an discrete input for an operation + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class RequestParameter : Value + { + /// suggested implementation location for this parameter + [YamlDotNet.Serialization.YamlMember(Alias = "implementation")] + public ImplementationLocation? Implementation { get; set; } + + /// When a parameter is flattened, it will be left in the list, but marked hidden (so, don't generate those!) + [YamlDotNet.Serialization.YamlMember(Alias = "flattened")] + public bool? Flattened { get; set; } + + /// When a parameter is grouped into another, this will tell where the parameter got grouped into. + [YamlDotNet.Serialization.YamlMember(Alias = "groupedBy")] + public RequestParameter? GroupedBy { get; set; } + + /// If this parameter is only part of the body request(for multipart and form bodies.) + [YamlDotNet.Serialization.YamlMember(Alias = "isPartialBody")] + public bool? IsPartialBody { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class VirtualParameter : RequestParameter + { + /// the original body parameter that this parameter is in effect replacing + [YamlDotNet.Serialization.YamlMember(Alias = "originalParameter")] + [System.ComponentModel.DataAnnotations.Required] + public RequestParameter OriginalParameter { get; set; } = new RequestParameter(); + + /// if this parameter is for a nested property, this is the path of properties it takes to get there + [YamlDotNet.Serialization.YamlMember(Alias = "pathToProperty")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection PathToProperty { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// the target property this virtual parameter represents + [YamlDotNet.Serialization.YamlMember(Alias = "targetProperty")] + [System.ComponentModel.DataAnnotations.Required] + public Property TargetProperty { get; set; } = new Property(); + } + + /// a response from a service. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ServiceResponse : Metadata + { + } + + /// a response where the content should be treated as a binary instead of a value or object + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class BinaryResponse : ServiceResponse + { + /// indicates that this response is a binary stream + [YamlDotNet.Serialization.YamlMember(Alias = "binary")] + public bool? Binary { get; set; } + } + + /// a response that should be deserialized into a result of type(schema) + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SchemaResponse : ServiceResponse + { + /// the content returned by the service for a given operaiton + [YamlDotNet.Serialization.YamlMember(Alias = "schema")] + [System.ComponentModel.DataAnnotations.Required] + public Schema Schema { get; set; } = new Schema(); + + /// indicates whether the response can be 'null' + [YamlDotNet.Serialization.YamlMember(Alias = "nullable")] + public bool? Nullable { get; set; } + } + + /// represents a single callable endpoint with a discrete set of inputs, and any number of output possibilities (responses or exceptions) + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Operation + { + /// Original Operation ID if present. + /// This can be used to identify the original id of an operation before it is styled. + /// THIS IS NOT the name of the operation that should be used in the generator. Use `.language.default.name` for this + [YamlDotNet.Serialization.YamlMember(Alias = "operationId")] + public string? OperationId { get; set; } + + /// common parameters when there are multiple requests + [YamlDotNet.Serialization.YamlMember(Alias = "parameters")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Parameters { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a common filtered list of parameters that is (assumably) the actual method signature parameters + [YamlDotNet.Serialization.YamlMember(Alias = "signatureParameters")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SignatureParameters { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Mapping of all the content types available for this operation to the coresponding request. + [YamlDotNet.Serialization.YamlMember(Alias = "requestMediaTypes")] + public RecordOfStringAndRequest? RequestMediaTypes { get; set; } + + /// List of headers that parameters should not handle as parameters but with special logic. + /// See https://github.com/Azure/autorest/tree/main/packages/extensions/modelerfour for configuration `skip-special-headers` to exclude headers. + [YamlDotNet.Serialization.YamlMember(Alias = "specialHeaders")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SpecialHeaders { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// the different possibilities to build the request. + [YamlDotNet.Serialization.YamlMember(Alias = "requests")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Requests { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// responses that indicate a successful call + [YamlDotNet.Serialization.YamlMember(Alias = "responses")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Responses { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// responses that indicate a failed call + [YamlDotNet.Serialization.YamlMember(Alias = "exceptions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Exceptions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// the apiVersion to use for a given profile name + [YamlDotNet.Serialization.YamlMember(Alias = "profile")] + public RecordOfStringAndApiVersion? Profile { get; set; } + + /// a short description + [YamlDotNet.Serialization.YamlMember(Alias = "summary")] + public string? Summary { get; set; } + + /// API versions that this applies to. Undefined means all versions + [YamlDotNet.Serialization.YamlMember(Alias = "apiVersions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection ApiVersions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Represent the deprecation information if api is deprecated. + [YamlDotNet.Serialization.YamlMember(Alias = "deprecated")] + public Deprecation? Deprecated { get; set; } + + /// where did this aspect come from (jsonpath or 'modelerfour:<soemthing>') + [YamlDotNet.Serialization.YamlMember(Alias = "origin")] + public string? Origin { get; set; } + + /// External Documentation Links + [YamlDotNet.Serialization.YamlMember(Alias = "externalDocs")] + public ExternalDocumentation? ExternalDocs { get; set; } + + /// per-language information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// per-protocol information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "protocol")] + [System.ComponentModel.DataAnnotations.Required] + public Protocols Protocol { get; set; } = new Protocols(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ServiceRequest : Metadata + { + /// the parameter inputs to the operation + [YamlDotNet.Serialization.YamlMember(Alias = "parameters")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Parameters { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a filtered list of parameters that is (assumably) the actual method signature parameters + [YamlDotNet.Serialization.YamlMember(Alias = "signatureParameters")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SignatureParameters { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// an operation group represents a container around set of operations + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class OperationGroup : Metadata + { + [YamlDotNet.Serialization.YamlMember(Alias = "$key")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Key { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "operations")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Operations { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class AnySchema : Schema + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class AnyObjectSchema : Schema + { + } + + /// a Schema that represents and array of values + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ArraySchema : ValueSchema + { + /// elementType of the array + [YamlDotNet.Serialization.YamlMember(Alias = "elementType")] + [System.ComponentModel.DataAnnotations.Required] + public Schema ElementType { get; set; } = new Schema(); + + /// maximum number of elements in the array + [YamlDotNet.Serialization.YamlMember(Alias = "maxItems")] + public double? MaxItems { get; set; } + + /// minimum number of elements in the array + [YamlDotNet.Serialization.YamlMember(Alias = "minItems")] + public double? MinItems { get; set; } + + /// if the elements in the array should be unique + [YamlDotNet.Serialization.YamlMember(Alias = "uniqueItems")] + public bool? UniqueItems { get; set; } + + /// if elements in the array should be nullable + [YamlDotNet.Serialization.YamlMember(Alias = "nullableItems")] + public bool? NullableItems { get; set; } + } + + /// a schema that represents a ByteArray value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ByteArraySchema : ValueSchema + { + /// date-time format + [YamlDotNet.Serialization.YamlMember(Alias = "format")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public ByteArraySchemaFormat Format { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class BinarySchema : Schema + { + } + + /// a Schema that represents a string value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class StringSchema : PrimitiveSchema + { + /// the maximum length of the string + [YamlDotNet.Serialization.YamlMember(Alias = "maxLength")] + public double? MaxLength { get; set; } + + /// the minimum length of the string + [YamlDotNet.Serialization.YamlMember(Alias = "minLength")] + public double? MinLength { get; set; } + + /// a regular expression that the string must be validated against + [YamlDotNet.Serialization.YamlMember(Alias = "pattern")] + public string? Pattern { get; set; } + } + + /// a schema that represents a ODataQuery value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ODataQuerySchema : Schema + { + } + + /// a schema that represents a Credential value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class CredentialSchema : PrimitiveSchema + { + /// the maximum length of the string + [YamlDotNet.Serialization.YamlMember(Alias = "maxLength")] + public double? MaxLength { get; set; } + + /// the minimum length of the string + [YamlDotNet.Serialization.YamlMember(Alias = "minLength")] + public double? MinLength { get; set; } + + /// a regular expression that the string must be validated against + [YamlDotNet.Serialization.YamlMember(Alias = "pattern")] + public string? Pattern { get; set; } + } + + /// a schema that represents a Uri value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class UriSchema : PrimitiveSchema + { + /// the maximum length of the string + [YamlDotNet.Serialization.YamlMember(Alias = "maxLength")] + public double? MaxLength { get; set; } + + /// the minimum length of the string + [YamlDotNet.Serialization.YamlMember(Alias = "minLength")] + public double? MinLength { get; set; } + + /// a regular expression that the string must be validated against + [YamlDotNet.Serialization.YamlMember(Alias = "pattern")] + public string? Pattern { get; set; } + } + + /// a schema that represents a Uuid value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class UuidSchema : PrimitiveSchema + { + } + + /// a schema that represents a Uuid value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ArmIdSchema : PrimitiveSchema + { + } + + /// a schema that represents a choice of several values (ie, an 'enum') + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ChoiceSchema : ValueSchema + { + /// the primitive type for the choices + [YamlDotNet.Serialization.YamlMember(Alias = "choiceType")] + [System.ComponentModel.DataAnnotations.Required] + public PrimitiveSchema ChoiceType { get; set; } = new PrimitiveSchema(); + + /// the possible choices for in the set + [YamlDotNet.Serialization.YamlMember(Alias = "choices")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Choices { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// an individual choice in a ChoiceSchema + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ChoiceValue + { + /// per-language information for this value + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// the actual value + [YamlDotNet.Serialization.YamlMember(Alias = "value")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Value { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// a schema that represents a choice of several values (ie, an 'enum') + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SealedChoiceSchema : ValueSchema + { + /// the primitive type for the choices + [YamlDotNet.Serialization.YamlMember(Alias = "choiceType")] + [System.ComponentModel.DataAnnotations.Required] + public PrimitiveSchema ChoiceType { get; set; } = new PrimitiveSchema(); + + /// the possible choices for in the set + [YamlDotNet.Serialization.YamlMember(Alias = "choices")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Choices { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// a schema that represents a value dependent on another + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ConditionalSchema : ValueSchema + { + /// the primitive type for the conditional + [YamlDotNet.Serialization.YamlMember(Alias = "conditionalType")] + [System.ComponentModel.DataAnnotations.Required] + public PrimitiveSchema ConditionalType { get; set; } = new PrimitiveSchema(); + + /// the possible conditinal values + [YamlDotNet.Serialization.YamlMember(Alias = "conditions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Conditions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// the source value that drives the target value (property or parameter) + [YamlDotNet.Serialization.YamlMember(Alias = "sourceValue")] + [System.ComponentModel.DataAnnotations.Required] + public Value SourceValue { get; set; } = new Value(); + } + + /// an individual value in a ConditionalSchema + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ConditionalValue + { + /// per-language information for this value + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// the actual value + [YamlDotNet.Serialization.YamlMember(Alias = "target")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Target { get; set; } + + /// the actual value + [YamlDotNet.Serialization.YamlMember(Alias = "source")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Source { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// a schema that represents a value dependent on another (not overridable) + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SealedConditionalSchema : ValueSchema + { + /// the primitive type for the condition + [YamlDotNet.Serialization.YamlMember(Alias = "conditionalType")] + [System.ComponentModel.DataAnnotations.Required] + public PrimitiveSchema ConditionalType { get; set; } = new PrimitiveSchema(); + + /// the possible conditional values + [YamlDotNet.Serialization.YamlMember(Alias = "conditions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Conditions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// the source value that drives the target value + [YamlDotNet.Serialization.YamlMember(Alias = "sourceValue")] + [System.ComponentModel.DataAnnotations.Required] + public Value SourceValue { get; set; } = new Value(); + } + + /// a container for the actual constant value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ConstantValue + { + /// per-language information for this value + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + public Languages? Language { get; set; } + + /// the actual constant value to use + [YamlDotNet.Serialization.YamlMember(Alias = "value")] + [System.ComponentModel.DataAnnotations.Required] + public object Value { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// a schema that represents a constant value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ConstantSchema : Schema + { + /// the schema type of the constant value (ie, StringSchema, NumberSchema, etc) + [YamlDotNet.Serialization.YamlMember(Alias = "valueType")] + [System.ComponentModel.DataAnnotations.Required] + public Schema ValueType { get; set; } = new Schema(); + + /// the actual constant value + [YamlDotNet.Serialization.YamlMember(Alias = "value")] + [System.ComponentModel.DataAnnotations.Required] + public ConstantValue Value { get; set; } = new ConstantValue(); + } + + /// a schema that represents a key-value collection + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class DictionarySchema : ComplexSchema + { + /// the element type of the dictionary. (Keys are always strings) + [YamlDotNet.Serialization.YamlMember(Alias = "elementType")] + [System.ComponentModel.DataAnnotations.Required] + public Schema ElementType { get; set; } = new Schema(); + + /// if elements in the dictionary should be nullable + [YamlDotNet.Serialization.YamlMember(Alias = "nullableItems")] + public bool? NullableItems { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class FlagValue + { + /// per-language information for this value + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + [YamlDotNet.Serialization.YamlMember(Alias = "value")] + public double? Value { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class FlagSchema : ValueSchema + { + /// the possible choices for in the set + [YamlDotNet.Serialization.YamlMember(Alias = "choices")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Choices { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// a Schema that represents a Number value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class NumberSchema : PrimitiveSchema + { + /// precision (# of bits?) of the number + [YamlDotNet.Serialization.YamlMember(Alias = "precision")] + public double? Precision { get; set; } + + /// if present, the number must be an exact multiple of this value + [YamlDotNet.Serialization.YamlMember(Alias = "multipleOf")] + public double? MultipleOf { get; set; } + + /// if present, the value must be lower than or equal to this (unless exclusiveMaximum is true) + [YamlDotNet.Serialization.YamlMember(Alias = "maximum")] + public double? Maximum { get; set; } + + /// if present, the value must be lower than maximum + [YamlDotNet.Serialization.YamlMember(Alias = "exclusiveMaximum")] + public bool? ExclusiveMaximum { get; set; } + + /// if present, the value must be highter than or equal to this (unless exclusiveMinimum is true) + [YamlDotNet.Serialization.YamlMember(Alias = "minimum")] + public double? Minimum { get; set; } + + /// if present, the value must be higher than minimum + [YamlDotNet.Serialization.YamlMember(Alias = "exclusiveMinimum")] + public bool? ExclusiveMinimum { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum SchemaContext + { + [System.Runtime.Serialization.EnumMember(Value = @"exception")] + Exception = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"input")] + Input = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"output")] + Output = 2, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SchemaUsage + { + /// contexts in which the schema is used + [YamlDotNet.Serialization.YamlMember(Alias = "usage")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Usage { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Known media types in which this schema can be serialized + [YamlDotNet.Serialization.YamlMember(Alias = "serializationFormats")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SerializationFormats { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Relations + { + [YamlDotNet.Serialization.YamlMember(Alias = "immediate")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Immediate { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "all")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection All { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Discriminator + { + [YamlDotNet.Serialization.YamlMember(Alias = "property")] + [System.ComponentModel.DataAnnotations.Required] + public Property Property { get; set; } = new Property(); + + [YamlDotNet.Serialization.YamlMember(Alias = "immediate")] + [System.ComponentModel.DataAnnotations.Required] + public RecordOfStringAndComplexSchema Immediate { get; set; } = new RecordOfStringAndComplexSchema(); + + [YamlDotNet.Serialization.YamlMember(Alias = "all")] + [System.ComponentModel.DataAnnotations.Required] + public RecordOfStringAndComplexSchema All { get; set; } = new RecordOfStringAndComplexSchema(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class GroupProperty : Property + { + [YamlDotNet.Serialization.YamlMember(Alias = "originalParameter")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection OriginalParameter { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// a schema that represents a type with child properties. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ObjectSchema : ComplexSchema + { + /// the property of the polymorphic descriminator for this type, if there is one + [YamlDotNet.Serialization.YamlMember(Alias = "discriminator")] + public Discriminator? Discriminator { get; set; } + + /// the collection of properties that are in this object + [YamlDotNet.Serialization.YamlMember(Alias = "properties")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Properties { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// maximum number of properties permitted + [YamlDotNet.Serialization.YamlMember(Alias = "maxProperties")] + public double? MaxProperties { get; set; } + + /// minimum number of properties permitted + [YamlDotNet.Serialization.YamlMember(Alias = "minProperties")] + public double? MinProperties { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "parents")] + public Relations? Parents { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "children")] + public Relations? Children { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "discriminatorValue")] + public string? DiscriminatorValue { get; set; } + + /// contexts in which the schema is used + [YamlDotNet.Serialization.YamlMember(Alias = "usage")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Usage { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Known media types in which this schema can be serialized + [YamlDotNet.Serialization.YamlMember(Alias = "serializationFormats")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SerializationFormats { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// a schema that represents a boolean value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class BooleanSchema : PrimitiveSchema + { + } + + /// a schema that represents a Char value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class CharSchema : PrimitiveSchema + { + } + + /// an OR relationship between several schemas + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class OrSchema : ComplexSchema + { + /// the set of schemas that this schema is composed of. Every schema is optional + [YamlDotNet.Serialization.YamlMember(Alias = "anyOf")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection AnyOf { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// an XOR relationship between several schemas + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class XorSchema : Schema + { + /// the set of schemas that this must be one and only one of. + [YamlDotNet.Serialization.YamlMember(Alias = "oneOf")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection OneOf { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// a NOT relationship between schemas + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class NotSchema : Schema + { + /// the schema that this may not be. + [YamlDotNet.Serialization.YamlMember(Alias = "not")] + [System.ComponentModel.DataAnnotations.Required] + public Schema Not { get; set; } = new Schema(); + } + + /// a schema that represents a Duration value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class DurationSchema : PrimitiveSchema + { + } + + /// a schema that represents a DateTime value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class DateTimeSchema : PrimitiveSchema + { + /// date-time format + [YamlDotNet.Serialization.YamlMember(Alias = "format")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public DateTimeSchemaFormat Format { get; set; } + } + + /// a schema that represents a Date value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class DateSchema : PrimitiveSchema + { + } + + /// a schema that represents a Date value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class TimeSchema : PrimitiveSchema + { + } + + /// a schema that represents a UnixTime value + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class UnixTimeSchema : PrimitiveSchema + { + } + + /// the full set of schemas for a given service, categorized into convenient collections + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Schemas + { + /// a collection of items + [YamlDotNet.Serialization.YamlMember(Alias = "arrays")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Arrays { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// an associative array (ie, dictionary, hashtable, etc) + [YamlDotNet.Serialization.YamlMember(Alias = "dictionaries")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Dictionaries { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a true or false value + [YamlDotNet.Serialization.YamlMember(Alias = "booleans")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Booleans { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a number value + [YamlDotNet.Serialization.YamlMember(Alias = "numbers")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Numbers { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// an object of some type + [YamlDotNet.Serialization.YamlMember(Alias = "objects")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Objects { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a string of characters + [YamlDotNet.Serialization.YamlMember(Alias = "strings")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Strings { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// UnixTime + [YamlDotNet.Serialization.YamlMember(Alias = "unixtimes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Unixtimes { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// ByteArray -- an array of bytes + [YamlDotNet.Serialization.YamlMember(Alias = "byteArrays")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection ByteArrays { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "streams")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Streams { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a single character + [YamlDotNet.Serialization.YamlMember(Alias = "chars")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Chars { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a Date + [YamlDotNet.Serialization.YamlMember(Alias = "dates")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Dates { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a time + [YamlDotNet.Serialization.YamlMember(Alias = "times")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Times { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a DateTime + [YamlDotNet.Serialization.YamlMember(Alias = "dateTimes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection DateTimes { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a Duration + [YamlDotNet.Serialization.YamlMember(Alias = "durations")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Durations { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a universally unique identifier + [YamlDotNet.Serialization.YamlMember(Alias = "uuids")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Uuids { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// an URI of some kind + [YamlDotNet.Serialization.YamlMember(Alias = "uris")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Uris { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// an URI of some kind + [YamlDotNet.Serialization.YamlMember(Alias = "armIds")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection ArmIds { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a password or credential + [YamlDotNet.Serialization.YamlMember(Alias = "credentials")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Credentials { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// OData Query + [YamlDotNet.Serialization.YamlMember(Alias = "odataQueries")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection OdataQueries { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// - this is essentially can be thought of as an 'enum' + /// that is a choice between one of several items, but an unspecified value is permitted. + [YamlDotNet.Serialization.YamlMember(Alias = "choices")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Choices { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// - this is essentially can be thought of as an 'enum' + /// that is a choice between one of several items, but an unknown value is not allowed. + [YamlDotNet.Serialization.YamlMember(Alias = "sealedChoices")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SealedChoices { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// ie, when 'profile' is 'production', use '2018-01-01' for apiversion + [YamlDotNet.Serialization.YamlMember(Alias = "conditionals")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Conditionals { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "sealedConditionals")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection SealedConditionals { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "flags")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Flags { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// a constant value + [YamlDotNet.Serialization.YamlMember(Alias = "constants")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Constants { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "ors")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Ors { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "xors")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Xors { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "binaries")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Binaries { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// it's possible that we just may make this an error + /// in representation. + [YamlDotNet.Serialization.YamlMember(Alias = "unknowns")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Unknowns { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "groups")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Groups { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "any")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Any { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "anyObjects")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection AnyObjects { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + /// The security information for the API surface + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Security + { + /// indicates that the API surface requires authentication + [YamlDotNet.Serialization.YamlMember(Alias = "authenticationRequired")] + public bool? AuthenticationRequired { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "schemes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Schemes { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Type { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class OAuth2SecurityScheme : SecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "scopes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Scopes { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class KeySecurityScheme : SecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "in")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public KeySecuritySchemeIn In { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ValueOrFactory + { + } + + /// example data [UNFINISHED] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class Example + { + [YamlDotNet.Serialization.YamlMember(Alias = "summary")] + public string? Summary { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "value")] + public object? Value { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "externalValue")] + public string? ExternalValue { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class AADTokenSecurityScheme : SecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "scopes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Scopes { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class AzureKeySecurityScheme : SecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "headerName")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string HeaderName { get; set; } + } + + /// standard HTTP protocol methods + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum HttpMethod + { + [System.Runtime.Serialization.EnumMember(Value = @"delete")] + Delete = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"get")] + Get = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"head")] + Head = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"options")] + Options = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"patch")] + Patch = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"post")] + Post = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"put")] + Put = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"trace")] + Trace = 7, + } + + /// the location that this parameter is placed in the http request + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum ParameterLocation + { + [System.Runtime.Serialization.EnumMember(Value = @"body")] + Body = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"cookie")] + Cookie = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"header")] + Header = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"none")] + None = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"path")] + Path = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"query")] + Query = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"uri")] + Uri = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"virtual")] + Virtual = 7, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum Scheme + { + [System.Runtime.Serialization.EnumMember(Value = @"bearer")] + Bearer = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum SecurityType + { + [System.Runtime.Serialization.EnumMember(Value = @"apiKey")] + ApiKey = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"http")] + Http = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"oauth2")] + Oauth2 = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"openIdConnect")] + OpenIdConnect = 3, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class AuthorizationCodeOAuthFlow + { + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "authorizationUrl")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string AuthorizationUrl { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "tokenUrl")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string TokenUrl { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "refreshUrl")] + public string? RefreshUrl { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "scopes")] + [System.ComponentModel.DataAnnotations.Required] + public RecordOfStringAndString Scopes { get; set; } = new RecordOfStringAndString(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class BearerHTTPSecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "scheme")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public BearerHTTPSecuritySchemeScheme Scheme { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "bearerFormat")] + public string? BearerFormat { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public BearerHTTPSecuritySchemeType Type { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ClientCredentialsFlow + { + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "tokenUrl")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string TokenUrl { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "refreshUrl")] + public string? RefreshUrl { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "scopes")] + [System.ComponentModel.DataAnnotations.Required] + public RecordOfStringAndString Scopes { get; set; } = new RecordOfStringAndString(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ImplicitOAuthFlow + { + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "authorizationUrl")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string AuthorizationUrl { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "refreshUrl")] + public string? RefreshUrl { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "scopes")] + [System.ComponentModel.DataAnnotations.Required] + public RecordOfStringAndString Scopes { get; set; } = new RecordOfStringAndString(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class NonBearerHTTPSecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "scheme")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Scheme { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public NonBearerHTTPSecuritySchemeType Type { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class OAuthFlows + { + [YamlDotNet.Serialization.YamlMember(Alias = "implicit")] + public ImplicitOAuthFlow? Implicit { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "password")] + public PasswordOAuthFlow? Password { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "clientCredentials")] + public ClientCredentialsFlow? ClientCredentials { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "authorizationCode")] + public AuthorizationCodeOAuthFlow? AuthorizationCode { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HTTPSecurityScheme + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class APIKeySecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public APIKeySecuritySchemeType Type { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Name { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "in")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public ParameterLocation In { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class OpenIdConnectSecurityScheme + { + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public OpenIdConnectSecuritySchemeType Type { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "openIdConnectUrl")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string OpenIdConnectUrl { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "description")] + public string? Description { get; set; } + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class PasswordOAuthFlow + { + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "tokenUrl")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string TokenUrl { get; set; } + + /// an URI + [YamlDotNet.Serialization.YamlMember(Alias = "refreshUrl")] + public string? RefreshUrl { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "scopes")] + [System.ComponentModel.DataAnnotations.Required] + public RecordOfStringAndString Scopes { get; set; } = new RecordOfStringAndString(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + /// common ways of serializing simple parameters + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class SecurityRequirement + { + } + + /// The Serialization Style used for the parameter. + /// + /// Describes how the parameter value will be serialized depending on the type of the parameter value. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum SerializationStyle + { + [System.Runtime.Serialization.EnumMember(Value = @"binary")] + Binary = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"deepObject")] + DeepObject = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"form")] + Form = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"json")] + Json = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"label")] + Label = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"matrix")] + Matrix = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"pipeDelimited")] + PipeDelimited = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"simple")] + Simple = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"spaceDelimited")] + SpaceDelimited = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"tabDelimited")] + TabDelimited = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"xml")] + Xml = 10, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum QueryEncodingStyle + { + [System.Runtime.Serialization.EnumMember(Value = @"deepObject")] + DeepObject = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"form")] + Form = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"pipeDelimited")] + PipeDelimited = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"spaceDelimited")] + SpaceDelimited = 3, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum PathEncodingStyle + { + [System.Runtime.Serialization.EnumMember(Value = @"label")] + Label = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"matrix")] + Matrix = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"simple")] + Simple = 2, + } + + /// A catch-all for all un-handled response codes. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum Default + { + [System.Runtime.Serialization.EnumMember(Value = @"default")] + Default = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum StatusCode + { + [System.Runtime.Serialization.EnumMember(Value = @"100")] + _100 = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"101")] + _101 = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"102")] + _102 = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"103")] + _103 = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"200")] + _200 = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"201")] + _201 = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"202")] + _202 = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"203")] + _203 = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"204")] + _204 = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"205")] + _205 = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"206")] + _206 = 10, + + [System.Runtime.Serialization.EnumMember(Value = @"207")] + _207 = 11, + + [System.Runtime.Serialization.EnumMember(Value = @"208")] + _208 = 12, + + [System.Runtime.Serialization.EnumMember(Value = @"226")] + _226 = 13, + + [System.Runtime.Serialization.EnumMember(Value = @"300")] + _300 = 14, + + [System.Runtime.Serialization.EnumMember(Value = @"301")] + _301 = 15, + + [System.Runtime.Serialization.EnumMember(Value = @"302")] + _302 = 16, + + [System.Runtime.Serialization.EnumMember(Value = @"303")] + _303 = 17, + + [System.Runtime.Serialization.EnumMember(Value = @"304")] + _304 = 18, + + [System.Runtime.Serialization.EnumMember(Value = @"305")] + _305 = 19, + + [System.Runtime.Serialization.EnumMember(Value = @"306")] + _306 = 20, + + [System.Runtime.Serialization.EnumMember(Value = @"307")] + _307 = 21, + + [System.Runtime.Serialization.EnumMember(Value = @"308")] + _308 = 22, + + [System.Runtime.Serialization.EnumMember(Value = @"400")] + _400 = 23, + + [System.Runtime.Serialization.EnumMember(Value = @"401")] + _401 = 24, + + [System.Runtime.Serialization.EnumMember(Value = @"402")] + _402 = 25, + + [System.Runtime.Serialization.EnumMember(Value = @"403")] + _403 = 26, + + [System.Runtime.Serialization.EnumMember(Value = @"404")] + _404 = 27, + + [System.Runtime.Serialization.EnumMember(Value = @"405")] + _405 = 28, + + [System.Runtime.Serialization.EnumMember(Value = @"406")] + _406 = 29, + + [System.Runtime.Serialization.EnumMember(Value = @"407")] + _407 = 30, + + [System.Runtime.Serialization.EnumMember(Value = @"408")] + _408 = 31, + + [System.Runtime.Serialization.EnumMember(Value = @"409")] + _409 = 32, + + [System.Runtime.Serialization.EnumMember(Value = @"410")] + _410 = 33, + + [System.Runtime.Serialization.EnumMember(Value = @"411")] + _411 = 34, + + [System.Runtime.Serialization.EnumMember(Value = @"412")] + _412 = 35, + + [System.Runtime.Serialization.EnumMember(Value = @"413")] + _413 = 36, + + [System.Runtime.Serialization.EnumMember(Value = @"414")] + _414 = 37, + + [System.Runtime.Serialization.EnumMember(Value = @"415")] + _415 = 38, + + [System.Runtime.Serialization.EnumMember(Value = @"416")] + _416 = 39, + + [System.Runtime.Serialization.EnumMember(Value = @"417")] + _417 = 40, + + [System.Runtime.Serialization.EnumMember(Value = @"418")] + _418 = 41, + + [System.Runtime.Serialization.EnumMember(Value = @"421")] + _421 = 42, + + [System.Runtime.Serialization.EnumMember(Value = @"422")] + _422 = 43, + + [System.Runtime.Serialization.EnumMember(Value = @"423")] + _423 = 44, + + [System.Runtime.Serialization.EnumMember(Value = @"424")] + _424 = 45, + + [System.Runtime.Serialization.EnumMember(Value = @"425")] + _425 = 46, + + [System.Runtime.Serialization.EnumMember(Value = @"426")] + _426 = 47, + + [System.Runtime.Serialization.EnumMember(Value = @"428")] + _428 = 48, + + [System.Runtime.Serialization.EnumMember(Value = @"429")] + _429 = 49, + + [System.Runtime.Serialization.EnumMember(Value = @"431")] + _431 = 50, + + [System.Runtime.Serialization.EnumMember(Value = @"451")] + _451 = 51, + + [System.Runtime.Serialization.EnumMember(Value = @"500")] + _500 = 52, + + [System.Runtime.Serialization.EnumMember(Value = @"501")] + _501 = 53, + + [System.Runtime.Serialization.EnumMember(Value = @"502")] + _502 = 54, + + [System.Runtime.Serialization.EnumMember(Value = @"503")] + _503 = 55, + + [System.Runtime.Serialization.EnumMember(Value = @"504")] + _504 = 56, + + [System.Runtime.Serialization.EnumMember(Value = @"505")] + _505 = 57, + + [System.Runtime.Serialization.EnumMember(Value = @"506")] + _506 = 58, + + [System.Runtime.Serialization.EnumMember(Value = @"507")] + _507 = 59, + + [System.Runtime.Serialization.EnumMember(Value = @"508")] + _508 = 60, + + [System.Runtime.Serialization.EnumMember(Value = @"510")] + _510 = 61, + + [System.Runtime.Serialization.EnumMember(Value = @"511")] + _511 = 62, + + [System.Runtime.Serialization.EnumMember(Value = @"default")] + Default = 63, + } + + /// extended metadata for HTTP operation parameters + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpParameter : Protocol + { + /// the location that this parameter is placed in the http request + [YamlDotNet.Serialization.YamlMember(Alias = "in")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public HttpParameterIn In { get; set; } + + /// the Serialization Style used for the parameter. + [YamlDotNet.Serialization.YamlMember(Alias = "style")] + public SerializationStyle? Style { get; set; } + + /// when set, 'form' style parameters generate separate parameters for each value of an array. + [YamlDotNet.Serialization.YamlMember(Alias = "explode")] + public bool? Explode { get; set; } + + /// when set, this indicates that the content of the parameter should not be subject to URI encoding rules. + [YamlDotNet.Serialization.YamlMember(Alias = "skipUriEncoding")] + public bool? SkipUriEncoding { get; set; } + } + + /// HTTP operation protocol data + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpRequest : Protocol + { + /// A relative path to an individual endpoint. + /// + /// The field name MUST begin with a slash. + /// The path is appended (no relative URL resolution) to the expanded URL from the Server Object's url field in order to construct the full URL. + /// Path templating is allowed. + /// + /// When matching URLs, concrete (non-templated) paths would be matched before their templated counterparts. + [YamlDotNet.Serialization.YamlMember(Alias = "path")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Path { get; set; } + + /// the base URI template for the operation. This will be a template that has Uri parameters to craft the base url to use. + [YamlDotNet.Serialization.YamlMember(Alias = "uri")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Uri { get; set; } + + /// the HTTP Method used to process this operation + [YamlDotNet.Serialization.YamlMember(Alias = "method")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public HttpMethod Method { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpWithBodyRequest : HttpRequest + { + /// a normalized value for the media type (ie, distills down to a well-known moniker (ie, 'json')) + [YamlDotNet.Serialization.YamlMember(Alias = "knownMediaType")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public KnownMediaType KnownMediaType { get; set; } + + /// must contain at least one media type to send for the body + [YamlDotNet.Serialization.YamlMember(Alias = "mediaTypes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection MediaTypes { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpBinaryRequest : HttpWithBodyRequest + { + [YamlDotNet.Serialization.YamlMember(Alias = "binary")] + public bool? Binary { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpMultipartRequest : HttpWithBodyRequest + { + /// indicates that the HTTP Request should be a multipart request + /// + /// ie, that it has multiple requests in a single request. + [YamlDotNet.Serialization.YamlMember(Alias = "multipart")] + public bool? Multipart { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpResponseHeader + { + [YamlDotNet.Serialization.YamlMember(Alias = "header")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Header { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "schema")] + [System.ComponentModel.DataAnnotations.Required] + public Schema Schema { get; set; } = new Schema(); + + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpResponse : Protocol + { + /// the possible HTTP status codes that this response MUST match one of. + [YamlDotNet.Serialization.YamlMember(Alias = "statusCodes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection StatusCodes { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// canonical response type (ie, 'json'). + [YamlDotNet.Serialization.YamlMember(Alias = "knownMediaType")] + public KnownMediaType? KnownMediaType { get; set; } + + /// The possible media types that this response MUST match one of. + [YamlDotNet.Serialization.YamlMember(Alias = "mediaTypes")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection MediaTypes { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// content returned by the service in the HTTP headers + [YamlDotNet.Serialization.YamlMember(Alias = "headers")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Headers { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// sets of HTTP headers grouped together into a single schema + [YamlDotNet.Serialization.YamlMember(Alias = "headerGroups")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection HeaderGroups { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpBinaryResponse : HttpResponse + { + /// binary responses + [YamlDotNet.Serialization.YamlMember(Alias = "binary")] + public bool? Binary { get; set; } + } + + /// code model metadata for HTTP protocol + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class HttpModel : Protocol + { + /// a collection of security requirements for the service + [YamlDotNet.Serialization.YamlMember(Alias = "security")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Security { get; set; } = new System.Collections.ObjectModel.Collection(); + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class RecordOfStringAndAny : System.Collections.Generic.Dictionary + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class RecordOfStringAndRequest + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class RecordOfStringAndApiVersion + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class ConstantType + { + /// per-language information for Schema + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// the schema type + [YamlDotNet.Serialization.YamlMember(Alias = "type")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public AllSchemaTypes Type { get; set; } + + /// a short description + [YamlDotNet.Serialization.YamlMember(Alias = "summary")] + public string? Summary { get; set; } + + /// example information + [YamlDotNet.Serialization.YamlMember(Alias = "example")] + public object? Example { get; set; } + + /// If the value isn't sent on the wire, the service will assume this + [YamlDotNet.Serialization.YamlMember(Alias = "defaultValue")] + public object? DefaultValue { get; set; } + + /// per-serialization information for this Schema + [YamlDotNet.Serialization.YamlMember(Alias = "serialization")] + public SerializationFormats? Serialization { get; set; } + + /// API versions that this applies to. Undefined means all versions + [YamlDotNet.Serialization.YamlMember(Alias = "apiVersions")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection ApiVersions { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// Represent the deprecation information if api is deprecated. + [YamlDotNet.Serialization.YamlMember(Alias = "deprecated")] + public Deprecation? Deprecated { get; set; } + + /// where did this aspect come from (jsonpath or 'modelerfour:<soemthing>') + [YamlDotNet.Serialization.YamlMember(Alias = "origin")] + public string? Origin { get; set; } + + /// External Documentation Links + [YamlDotNet.Serialization.YamlMember(Alias = "externalDocs")] + public ExternalDocumentation? ExternalDocs { get; set; } + + /// per-protocol information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "protocol")] + [System.ComponentModel.DataAnnotations.Required] + public Protocols Protocol { get; set; } = new Protocols(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class RecordOfStringAndComplexSchema : System.Collections.Generic.Dictionary + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class RecordOfStringAndString : System.Collections.Generic.Dictionary + { + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum KnownMediaType + { + [System.Runtime.Serialization.EnumMember(Value = @"binary")] + Binary = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"form")] + Form = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"json")] + Json = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"multipart")] + Multipart = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"text")] + Text = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"unknown")] + Unknown = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"xml")] + Xml = 6, + } + + /// the model that contains all the information required to generate a service api + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal partial class CodeModel + { + /// Code model information + [YamlDotNet.Serialization.YamlMember(Alias = "info")] + [System.ComponentModel.DataAnnotations.Required] + public Info Info { get; set; } = new Info(); + + /// All schemas for the model + [YamlDotNet.Serialization.YamlMember(Alias = "schemas")] + [System.ComponentModel.DataAnnotations.Required] + public Schemas Schemas { get; set; } = new Schemas(); + + /// All operations + [YamlDotNet.Serialization.YamlMember(Alias = "operationGroups")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection OperationGroups { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// all global parameters (ie, ImplementationLocation = client ) + [YamlDotNet.Serialization.YamlMember(Alias = "globalParameters")] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection GlobalParameters { get; set; } = new System.Collections.ObjectModel.Collection(); + + [YamlDotNet.Serialization.YamlMember(Alias = "security")] + [System.ComponentModel.DataAnnotations.Required] + public Security Security { get; set; } = new Security(); + + /// per-language information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "language")] + [System.ComponentModel.DataAnnotations.Required] + public Languages Language { get; set; } = new Languages(); + + /// per-protocol information for this aspect + [YamlDotNet.Serialization.YamlMember(Alias = "protocol")] + [System.ComponentModel.DataAnnotations.Required] + public Protocols Protocol { get; set; } = new Protocols(); + + /// additional metadata extensions dictionary + [YamlDotNet.Serialization.YamlMember(Alias = "extensions")] + public RecordOfStringAndAny? Extensions { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum ApiVersionRange + { + [System.Runtime.Serialization.EnumMember(Value = @"+")] + Plus = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"-")] + Minus = 1, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum ByteArraySchemaFormat + { + [System.Runtime.Serialization.EnumMember(Value = @"base64url")] + Base64url = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"byte")] + Byte = 1, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum DateTimeSchemaFormat + { + [System.Runtime.Serialization.EnumMember(Value = @"date-time")] + DateTime = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"date-time-rfc1123")] + DateTimeRfc1123 = 1, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum KeySecuritySchemeIn + { + [System.Runtime.Serialization.EnumMember(Value = @"header")] + Header = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum BearerHTTPSecuritySchemeScheme + { + [System.Runtime.Serialization.EnumMember(Value = @"bearer")] + Bearer = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum BearerHTTPSecuritySchemeType + { + [System.Runtime.Serialization.EnumMember(Value = @"http")] + Http = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum NonBearerHTTPSecuritySchemeType + { + [System.Runtime.Serialization.EnumMember(Value = @"http")] + Http = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum APIKeySecuritySchemeType + { + [System.Runtime.Serialization.EnumMember(Value = @"apiKey")] + ApiKey = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum OpenIdConnectSecuritySchemeType + { + [System.Runtime.Serialization.EnumMember(Value = @"openIdConnect")] + OpenIdConnect = 0, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum HttpParameterIn + { + [System.Runtime.Serialization.EnumMember(Value = @"body")] + Body = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"cookie")] + Cookie = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"header")] + Header = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"none")] + None = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"path")] + Path = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"query")] + Query = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"uri")] + Uri = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"virtual")] + Virtual = 7, + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.23.0 (Newtonsoft.Json v9.0.0.0)")] + internal enum StatusCodes + { + [System.Runtime.Serialization.EnumMember(Value = @"100")] + _100 = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"101")] + _101 = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"102")] + _102 = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"103")] + _103 = 3, + + [System.Runtime.Serialization.EnumMember(Value = @"200")] + _200 = 4, + + [System.Runtime.Serialization.EnumMember(Value = @"201")] + _201 = 5, + + [System.Runtime.Serialization.EnumMember(Value = @"202")] + _202 = 6, + + [System.Runtime.Serialization.EnumMember(Value = @"203")] + _203 = 7, + + [System.Runtime.Serialization.EnumMember(Value = @"204")] + _204 = 8, + + [System.Runtime.Serialization.EnumMember(Value = @"205")] + _205 = 9, + + [System.Runtime.Serialization.EnumMember(Value = @"206")] + _206 = 10, + + [System.Runtime.Serialization.EnumMember(Value = @"207")] + _207 = 11, + + [System.Runtime.Serialization.EnumMember(Value = @"208")] + _208 = 12, + + [System.Runtime.Serialization.EnumMember(Value = @"226")] + _226 = 13, + + [System.Runtime.Serialization.EnumMember(Value = @"300")] + _300 = 14, + + [System.Runtime.Serialization.EnumMember(Value = @"301")] + _301 = 15, + + [System.Runtime.Serialization.EnumMember(Value = @"302")] + _302 = 16, + + [System.Runtime.Serialization.EnumMember(Value = @"303")] + _303 = 17, + + [System.Runtime.Serialization.EnumMember(Value = @"304")] + _304 = 18, + + [System.Runtime.Serialization.EnumMember(Value = @"305")] + _305 = 19, + + [System.Runtime.Serialization.EnumMember(Value = @"306")] + _306 = 20, + + [System.Runtime.Serialization.EnumMember(Value = @"307")] + _307 = 21, + + [System.Runtime.Serialization.EnumMember(Value = @"308")] + _308 = 22, + + [System.Runtime.Serialization.EnumMember(Value = @"400")] + _400 = 23, + + [System.Runtime.Serialization.EnumMember(Value = @"401")] + _401 = 24, + + [System.Runtime.Serialization.EnumMember(Value = @"402")] + _402 = 25, + + [System.Runtime.Serialization.EnumMember(Value = @"403")] + _403 = 26, + + [System.Runtime.Serialization.EnumMember(Value = @"404")] + _404 = 27, + + [System.Runtime.Serialization.EnumMember(Value = @"405")] + _405 = 28, + + [System.Runtime.Serialization.EnumMember(Value = @"406")] + _406 = 29, + + [System.Runtime.Serialization.EnumMember(Value = @"407")] + _407 = 30, + + [System.Runtime.Serialization.EnumMember(Value = @"408")] + _408 = 31, + + [System.Runtime.Serialization.EnumMember(Value = @"409")] + _409 = 32, + + [System.Runtime.Serialization.EnumMember(Value = @"410")] + _410 = 33, + + [System.Runtime.Serialization.EnumMember(Value = @"411")] + _411 = 34, + + [System.Runtime.Serialization.EnumMember(Value = @"412")] + _412 = 35, + + [System.Runtime.Serialization.EnumMember(Value = @"413")] + _413 = 36, + + [System.Runtime.Serialization.EnumMember(Value = @"414")] + _414 = 37, + + [System.Runtime.Serialization.EnumMember(Value = @"415")] + _415 = 38, + + [System.Runtime.Serialization.EnumMember(Value = @"416")] + _416 = 39, + + [System.Runtime.Serialization.EnumMember(Value = @"417")] + _417 = 40, + + [System.Runtime.Serialization.EnumMember(Value = @"418")] + _418 = 41, + + [System.Runtime.Serialization.EnumMember(Value = @"421")] + _421 = 42, + + [System.Runtime.Serialization.EnumMember(Value = @"422")] + _422 = 43, + + [System.Runtime.Serialization.EnumMember(Value = @"423")] + _423 = 44, + + [System.Runtime.Serialization.EnumMember(Value = @"424")] + _424 = 45, + + [System.Runtime.Serialization.EnumMember(Value = @"425")] + _425 = 46, + + [System.Runtime.Serialization.EnumMember(Value = @"426")] + _426 = 47, + + [System.Runtime.Serialization.EnumMember(Value = @"428")] + _428 = 48, + + [System.Runtime.Serialization.EnumMember(Value = @"429")] + _429 = 49, + + [System.Runtime.Serialization.EnumMember(Value = @"431")] + _431 = 50, + + [System.Runtime.Serialization.EnumMember(Value = @"451")] + _451 = 51, + + [System.Runtime.Serialization.EnumMember(Value = @"500")] + _500 = 52, + + [System.Runtime.Serialization.EnumMember(Value = @"501")] + _501 = 53, + + [System.Runtime.Serialization.EnumMember(Value = @"502")] + _502 = 54, + + [System.Runtime.Serialization.EnumMember(Value = @"503")] + _503 = 55, + + [System.Runtime.Serialization.EnumMember(Value = @"504")] + _504 = 56, + + [System.Runtime.Serialization.EnumMember(Value = @"505")] + _505 = 57, + + [System.Runtime.Serialization.EnumMember(Value = @"506")] + _506 = 58, + + [System.Runtime.Serialization.EnumMember(Value = @"507")] + _507 = 59, + + [System.Runtime.Serialization.EnumMember(Value = @"508")] + _508 = 60, + + [System.Runtime.Serialization.EnumMember(Value = @"510")] + _510 = 61, + + [System.Runtime.Serialization.EnumMember(Value = @"511")] + _511 = 62, + + [System.Runtime.Serialization.EnumMember(Value = @"default")] + Default = 63, + } +} \ No newline at end of file diff --git a/logger/autorest.csharp/common/Input/InputTypes/BodyMediaType.cs b/logger/autorest.csharp/common/Input/InputTypes/BodyMediaType.cs new file mode 100644 index 0000000..1bc106e --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/BodyMediaType.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal enum BodyMediaType +{ + None, + Binary, + Form, + Json, + Multipart, + Text, + Xml +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/BytesKnownEncoding.cs b/logger/autorest.csharp/common/Input/InputTypes/BytesKnownEncoding.cs new file mode 100644 index 0000000..2cb74df --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/BytesKnownEncoding.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal static class BytesKnownEncoding +{ + public const string Base64 = "base64"; + public const string Base64Url = "base64url"; +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/DateTimeKnownEncoding.cs b/logger/autorest.csharp/common/Input/InputTypes/DateTimeKnownEncoding.cs new file mode 100644 index 0000000..a7adf35 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/DateTimeKnownEncoding.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal enum DateTimeKnownEncoding +{ + Rfc3339, Rfc7231, UnixTimestamp +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/DurationKnownEncoding.cs b/logger/autorest.csharp/common/Input/InputTypes/DurationKnownEncoding.cs new file mode 100644 index 0000000..d04bb37 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/DurationKnownEncoding.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal enum DurationKnownEncoding +{ + Iso8601, Seconds, Constant +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/ExampleMockValueBuilder.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/ExampleMockValueBuilder.cs new file mode 100644 index 0000000..7ae9e1d --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/ExampleMockValueBuilder.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input.InputTypes; + +namespace AutoRest.CSharp.Common.Input.Examples +{ + internal class ExampleMockValueBuilder + { + public const string ShortVersionMockExampleKey = "ShortVersion"; + public const string MockExampleAllParameterKey = "AllParameters"; + + private static readonly string EndpointMockValue = Configuration.ApiTypes.EndPointSampleValue; + + private readonly static ConcurrentDictionary _cache = new(); + + public static IReadOnlyList BuildOperationExamples(InputOperation operation) + { + _cache.Clear(); + return new[] + { + BuildOperationExample(operation, ShortVersionMockExampleKey, false), + BuildOperationExample(operation, MockExampleAllParameterKey, true) + }; + } + + private static InputOperationExample BuildOperationExample(InputOperation operation, string name, bool useAllParameters) + { + var parameterExamples = new List(operation.Parameters.Count); + foreach (var parameter in operation.Parameters) + { + if (!useAllParameters && !parameter.IsRequired) + { + continue; + } + var parameterExample = BuildParameterExample(parameter, useAllParameters); + parameterExamples.Add(parameterExample); + } + + return new(name, null, string.Empty, parameterExamples); + } + + private static InputParameterExample BuildParameterExample(InputParameter parameter, bool useAllParameters) + { + // if the parameter is constant, we just put the constant into the example value instead of mocking a new one + if (parameter.Kind == InputOperationParameterKind.Constant) + { + InputExampleValue value; + if (parameter.Type is InputLiteralType { Value: not null } literalValue) + { + value = InputExampleValue.Value(parameter.Type, literalValue.Value); + } + else if (parameter.DefaultValue != null) + { + // when it is constant, it could have DefaultValue + value = InputExampleValue.Value(parameter.Type, parameter.DefaultValue.Value); + } + else if (parameter.Type is InputUnionType unionType && unionType.VariantTypes[0] is InputLiteralType literalType) + { + // or it could be a union of literal types + value = InputExampleValue.Value(parameter.Type, literalType.Value); + } + else if (parameter.Type is InputEnumType enumType && enumType.Values[0].Value is { } enumValue) + { + // or it could be an enum of a few values + value = InputExampleValue.Value(parameter.Type, enumValue); + } + else + { + // fallback to null + value = InputExampleValue.Null(parameter.Type); + } + return new(parameter, value); + } + + // if the parameter is endpoint + if (parameter.IsEndpoint) + { + var value = InputExampleValue.Value(parameter.Type, EndpointMockValue); + return new(parameter, value); + } + + if (parameter.DefaultValue != null) + { + var value = InputExampleValue.Value(parameter.Type, parameter.DefaultValue.Value); + return new(parameter, value); + } + + var exampleValue = BuildExampleValue(parameter.Type, parameter.Name, useAllParameters, new HashSet()); + return new(parameter, exampleValue); + } + + private static InputExampleValue BuildExampleValue(InputType type, string? hint, bool useAllParameters, HashSet visitedModels) => type switch + { + InputListType listType => BuildListExampleValue(listType, hint, useAllParameters, visitedModels), + InputDictionaryType dictionaryType => BuildDictionaryExampleValue(dictionaryType, hint, useAllParameters, visitedModels), + InputEnumType enumType => BuildEnumExampleValue(enumType), + InputPrimitiveType primitiveType => BuildPrimitiveExampleValue(primitiveType, hint), + InputLiteralType literalType => InputExampleValue.Value(literalType, literalType.Value), + InputModelType modelType => BuildModelExampleValue(modelType, useAllParameters, visitedModels), + InputUnionType unionType => BuildExampleValue(unionType.VariantTypes[0], hint, useAllParameters, visitedModels), + InputNullableType nullableType => BuildExampleValue(nullableType.Type, hint, useAllParameters, visitedModels), + InputDateTimeType dateTimeType => BuildDateTimeExampleValue(dateTimeType), + InputDurationType durationType => BuildDurationExampleValue(durationType), + _ => InputExampleValue.Object(type, new Dictionary()) + }; + + private static InputExampleValue BuildListExampleValue(InputListType listType, string? hint, bool useAllParameters, HashSet visitedModels) + { + var exampleElementValue = BuildExampleValue(listType.ValueType, hint, useAllParameters, visitedModels); + + return InputExampleValue.List(listType, new[] { exampleElementValue }); + } + + private static InputExampleValue BuildDictionaryExampleValue(InputDictionaryType dictionaryType, string? hint, bool useAllParameters, HashSet visitedModels) + { + var exampleValue = BuildExampleValue(dictionaryType.ValueType, hint, useAllParameters, visitedModels); + + return InputExampleValue.Object(dictionaryType, new Dictionary + { + ["key"] = exampleValue + }); + } + + private static InputExampleValue BuildEnumExampleValue(InputEnumType enumType) + { + var enumValue = enumType.Values.First(); + return InputExampleValue.Value(enumType, enumValue.Value); + } + + private static InputExampleValue BuildPrimitiveExampleValue(InputPrimitiveType primitiveType, string? hint) => primitiveType.Kind switch + { + InputPrimitiveTypeKind.Stream => InputExampleValue.Stream(primitiveType, ""), + InputPrimitiveTypeKind.Boolean => InputExampleValue.Value(primitiveType, true), + InputPrimitiveTypeKind.PlainDate => InputExampleValue.Value(primitiveType, "2022-05-10"), + InputPrimitiveTypeKind.Float32 => InputExampleValue.Value(primitiveType, 123.45f), + InputPrimitiveTypeKind.Float64 => InputExampleValue.Value(primitiveType, 123.45d), + InputPrimitiveTypeKind.Decimal or InputPrimitiveTypeKind.Decimal128 => InputExampleValue.Value(primitiveType, 123.45m), + InputPrimitiveTypeKind.Int8 => InputExampleValue.Value(primitiveType, (sbyte)123), + InputPrimitiveTypeKind.UInt8 => InputExampleValue.Value(primitiveType, (byte)123), + InputPrimitiveTypeKind.Int32 => InputExampleValue.Value(primitiveType, 1234), + InputPrimitiveTypeKind.Int64 => InputExampleValue.Value(primitiveType, 1234L), + InputPrimitiveTypeKind.SafeInt => InputExampleValue.Value(primitiveType, 1234L), + InputPrimitiveTypeKind.String => primitiveType.CrossLanguageDefinitionId switch + { + InputPrimitiveType.UuidId => InputExampleValue.Value(primitiveType, "73f411fe-4f43-4b4b-9cbd-6828d8f4cf9a"), + _ => string.IsNullOrWhiteSpace(hint) ? InputExampleValue.Value(primitiveType, "") : InputExampleValue.Value(primitiveType, $"<{hint}>") + }, + InputPrimitiveTypeKind.PlainTime => InputExampleValue.Value(primitiveType, "01:23:45"), + InputPrimitiveTypeKind.Url => InputExampleValue.Value(primitiveType, "http://localhost:3000"), + _ => InputExampleValue.Object(primitiveType, new Dictionary()) + }; + + private static InputExampleValue BuildDateTimeExampleValue(InputDateTimeType dateTimeType) => dateTimeType.Encode switch + { + DateTimeKnownEncoding.Rfc7231 => InputExampleValue.Value(dateTimeType.WireType, "Tue, 10 May 2022 18:57:31 GMT"), + DateTimeKnownEncoding.Rfc3339 => InputExampleValue.Value(dateTimeType.WireType, "2022-05-10T18:57:31.2311892Z"), + DateTimeKnownEncoding.UnixTimestamp => InputExampleValue.Value(dateTimeType.WireType, 1652209051), + _ => InputExampleValue.Null(dateTimeType) + }; + + private static InputExampleValue BuildDurationExampleValue(InputDurationType durationType) => durationType.Encode switch + { + DurationKnownEncoding.Iso8601 => InputExampleValue.Value(durationType.WireType, "PT1H23M45S"), + DurationKnownEncoding.Seconds => durationType.WireType.Kind switch + { + InputPrimitiveTypeKind.Int32 => InputExampleValue.Value(durationType.WireType, 10), + InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => InputExampleValue.Value(durationType.WireType, 10f), + _ => InputExampleValue.Value(durationType.WireType, 3.141592) + }, + _ => InputExampleValue.Null(durationType) + }; + + private static InputExampleValue BuildModelExampleValue(InputModelType model, bool useAllParameters, HashSet visitedModels) + { + if (visitedModels.Contains(model)) + return InputExampleValue.Null(model); + + var dict = new Dictionary(); + var result = InputExampleValue.Object(model, dict); + visitedModels.Add(model); + // if this model has a discriminator, we should return a derived type + if (model.DiscriminatorProperty != null) + { + var derived = model.DerivedModels.FirstOrDefault(); + if (derived is null) + { + return InputExampleValue.Null(model); + } + else + { + model = derived; + } + } + // then, we just iterate all the properties + foreach (var modelOrBase in model.GetSelfAndBaseModels()) + { + foreach (var property in modelOrBase.Properties) + { + if (property.IsReadOnly) + continue; + + if (!useAllParameters && !property.IsRequired) + continue; + + // this means a property is defined both on the base and derived type, we skip other occurrences only keep the first + // which means we only keep the property defined in the lowest layer (derived types) + if (dict.ContainsKey(property.SerializedName)) + continue; + + InputExampleValue exampleValue; + if (property.IsDiscriminator) + { + exampleValue = InputExampleValue.Value(property.Type, model.DiscriminatorValue!); + } + else if (property.ConstantValue is { Value: { } constantValue }) + { + exampleValue = InputExampleValue.Value(property.Type, constantValue); + } + else + { + exampleValue = BuildExampleValue(property.Type, property.SerializedName, useAllParameters, visitedModels); + } + + dict.Add(property.SerializedName, exampleValue); + } + } + + return result; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/InputExampleValue.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/InputExampleValue.cs new file mode 100644 index 0000000..dd22f94 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/InputExampleValue.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input.Examples +{ + internal abstract record InputExampleValue(InputType Type) + { + public static InputExampleValue Null(InputType type) => new InputExampleRawValue(type, null); + public static InputExampleValue Value(InputType type, object? rawValue) => new InputExampleRawValue(type, rawValue); + public static InputExampleValue List(InputType type, IReadOnlyList values) => new InputExampleListValue(type, values); + public static InputExampleValue Object(InputType type, IReadOnlyDictionary properties) => new InputExampleObjectValue(type, properties); + public static InputExampleValue Stream(InputType type, string filename) => new InputExampleStreamValue(type, filename); + } + + internal record InputExampleRawValue(InputType Type, object? RawValue) : InputExampleValue(Type); + + internal record InputExampleListValue(InputType Type, IReadOnlyList Values) : InputExampleValue(Type); + + internal record InputExampleObjectValue(InputType Type, IReadOnlyDictionary Values): InputExampleValue(Type); // TODO -- split this into model and dict + + internal record InputExampleStreamValue(InputType Type, string Filename): InputExampleValue(Type); +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/InputOperationExample.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/InputOperationExample.cs new file mode 100644 index 0000000..b25f4d8 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/InputOperationExample.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input.Examples +{ + // TODO -- currently we do not need the responses. In the case that in the future we need to handle the responses in examples, we need to add a new class InputExampleResponse and a new property here + internal record InputOperationExample(string Name, string? Description, string FilePath, IReadOnlyList Parameters); +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/InputParameterExample.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/InputParameterExample.cs new file mode 100644 index 0000000..2b89d7c --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/InputParameterExample.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Common.Input.Examples +{ + internal record InputParameterExample(InputParameter Parameter, InputExampleValue ExampleValue); +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputExampleValueConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputExampleValueConverter.cs new file mode 100644 index 0000000..b45d582 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputExampleValueConverter.cs @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input.Examples +{ + internal sealed class TypeSpecInputExampleValueConverter : JsonConverter + { + private const string KindPropertyName = "kind"; + private const string TypePropertyName = "type"; + private const string ValuePropertyName = "value"; + + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputExampleValueConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputExampleValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateObject(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, InputExampleValue value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private InputExampleValue CreateObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + string? id = null; + string? kind = null; + InputExampleValue? result = null; + var isFirstProperty = true; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isIdOrKind = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString(KindPropertyName, ref kind); + + if (isIdOrKind) + { + continue; + } + result = CreateDerivedType(ref reader, id, kind, options); + } + + return result ?? throw new JsonException(); + } + + private const string ModelKind = "model"; + private const string ArrayKind = "array"; + private const string DictionaryKind = "dict"; + private const string UnknownKind = "unknown"; + + private InputExampleValue CreateDerivedType(ref Utf8JsonReader reader, string? id, string? kind, JsonSerializerOptions options) => kind switch + { + null => throw new JsonException($"InputTypeExample (id: '{id}') must have a 'kind' property"), + ArrayKind => CreateArrayExample(ref reader, id, options, _referenceHandler.CurrentResolver), + DictionaryKind or ModelKind => CreateObjectExample(ref reader, id, options, _referenceHandler.CurrentResolver), + UnknownKind => CreateUnknownExample(ref reader, id, options, _referenceHandler.CurrentResolver), + _ => CreateOtherExample(ref reader, id, options, _referenceHandler.CurrentResolver), + }; + + private InputExampleValue CreateArrayExample(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver referenceResolver) + { + bool isFirstProperty = id == null; + InputType? type = null; + IReadOnlyList? value = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter(TypePropertyName, options, ref type) + || reader.TryReadWithConverter(ValuePropertyName, options, ref value); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + var result = new InputExampleListValue(type ?? throw new JsonException(), value ?? throw new JsonException()); + + if (id != null) + { + referenceResolver.AddReference(id, result); + } + + return result; + } + + private InputExampleValue CreateObjectExample(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver referenceResolver) + { + bool isFirstProperty = id == null; + InputType? type = null; + IReadOnlyDictionary? value = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter(TypePropertyName, options, ref type) + || reader.TryReadWithConverter(ValuePropertyName, options, ref value); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + var result = new InputExampleObjectValue(type ?? throw new JsonException(), value ?? throw new JsonException()); + + if (id != null) + { + referenceResolver.AddReference(id, result); + } + + return result; + } + + private InputExampleValue CreateUnknownExample(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver referenceResolver) + { + bool isFirstProperty = id == null; + InputType? type = null; + JsonElement? rawValue = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter(TypePropertyName, options, ref type) + || reader.TryReadWithConverter(ValuePropertyName, options, ref rawValue); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + var result = ParseUnknownValue(type ?? throw new JsonException(), rawValue); + + if (id != null) + { + referenceResolver.AddReference(id, result); + } + + return result; + + static InputExampleValue ParseUnknownValue(InputType type, JsonElement? rawValue) + { + switch (rawValue?.ValueKind) + { + case null or JsonValueKind.Null: + return InputExampleValue.Null(type); + case JsonValueKind.String: + return InputExampleValue.Value(type, rawValue.Value.GetString()); + case JsonValueKind.True or JsonValueKind.False: + return InputExampleValue.Value(type, rawValue.Value.GetBoolean()); + case JsonValueKind.Number: + var rawText = rawValue.Value.GetRawText(); + if (int.TryParse(rawText, out var int32Value)) + return InputExampleValue.Value(type, int32Value); + else if (long.TryParse(rawText, out var int64Value)) + return InputExampleValue.Value(type, int64Value); + else if (float.TryParse(rawText, out var floatValue)) + return InputExampleValue.Value(type, floatValue); + else if (double.TryParse(rawText, out var doubleValue)) + return InputExampleValue.Value(type, doubleValue); + else if (decimal.TryParse(rawText, out var decimalValue)) + return InputExampleValue.Value(type, decimalValue); + else + return InputExampleValue.Value(type, null); + case JsonValueKind.Array: + var length = rawValue.Value.GetArrayLength(); + var values = new List(length); + foreach (var item in rawValue.Value.EnumerateArray()) + { + values.Add(ParseUnknownValue(type, item)); + } + return InputExampleValue.List(type, values); + case JsonValueKind.Object: + var objValues = new Dictionary(); + foreach (var property in rawValue.Value.EnumerateObject()) + { + objValues.Add(property.Name, ParseUnknownValue(type, property.Value)); + } + return InputExampleValue.Object(type, objValues); + default: + throw new JsonException($"kind {rawValue?.ValueKind} is not expected here"); + } + } + } + + private InputExampleValue CreateOtherExample(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver referenceResolver) + { + bool isFirstProperty = id == null; + InputType? type = null; + JsonElement? rawValue = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter(TypePropertyName, options, ref type) + || reader.TryReadWithConverter(ValuePropertyName, options, ref rawValue); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + var effectiveType = type switch + { + null => throw new JsonException(), + InputEnumType enumType => enumType.ValueType, + InputDurationType durationType => durationType.WireType, + InputDateTimeType dateTimeType => dateTimeType.WireType, + _ => type + }; + + object? value = rawValue?.ValueKind switch + { + null or JsonValueKind.Null => null, + JsonValueKind.String => rawValue.Value.GetString(), + JsonValueKind.False => false, + JsonValueKind.True => true, + JsonValueKind.Number => effectiveType switch + { + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Int32 or InputPrimitiveTypeKind.UInt32 } => int.TryParse(rawValue.Value.GetRawText(), out var intValue) ? intValue : default(int), + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.UInt64 or InputPrimitiveTypeKind.Integer } => long.TryParse(rawValue.Value.GetRawText(), out var longValue) ? longValue : default(long), + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Float32 } => float.TryParse(rawValue.Value.GetRawText(), out var floatValue) ? floatValue : default(float), + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Float64 or InputPrimitiveTypeKind.Float } => double.TryParse(rawValue.Value.GetRawText(), out var doubleValue) ? doubleValue : default(double), + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Decimal or InputPrimitiveTypeKind.Decimal128 } => decimal.TryParse(rawValue.Value.GetRawText(), out var decimalValue) ? decimalValue : default(decimal), + _ => null, + }, + _ => throw new JsonException($"kind {rawValue?.ValueKind} is not expected here") + }; + + var result = new InputExampleRawValue(type ?? throw new JsonException(), value); + + if (id != null) + { + referenceResolver.AddReference(id, result); + } + + return result; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputOperationExampleConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputOperationExampleConverter.cs new file mode 100644 index 0000000..0aa0593 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputOperationExampleConverter.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input.Examples +{ + internal sealed class TypeSpecInputOperationExampleConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputOperationExampleConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputOperationExample? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputHttpOperationExample(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, InputOperationExample value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private InputOperationExample CreateInputHttpOperationExample(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var isFirstProperty = true; + string? id = null; + string? name = null; + string? description = null; + string? filePath = null; + IReadOnlyList? parameters = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("description", ref description) + || reader.TryReadString("filePath", ref filePath) + || reader.TryReadWithConverter("parameters", options, ref parameters); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + var result = new InputOperationExample(name ?? throw new JsonException(), description, filePath ?? throw new JsonException(), parameters ?? throw new JsonException()); + + if (id != null) + { + _referenceHandler.CurrentResolver.AddReference(id, result); + } + + return result; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputParameterExampleConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputParameterExampleConverter.cs new file mode 100644 index 0000000..c08fde8 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Examples/Serialization/TypeSpecInputParameterExampleConverter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input.Examples +{ + internal sealed class TypeSpecInputParameterExampleConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputParameterExampleConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputParameterExample? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputParameterExample(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, InputParameterExample value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private InputParameterExample CreateInputParameterExample(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + string? id = null; + InputParameter? parameter = null; + InputExampleValue? value = null; + var isFirstProperty = true; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter("parameter", options, ref parameter) + || reader.TryReadWithConverter("value", options, ref value); + + if (!isKnownProperty) + { + continue; + } + } + + var result = new InputParameterExample(parameter ?? throw new JsonException(), value ?? throw new JsonException()); + + if (id != null) + { + _referenceHandler.CurrentResolver.AddReference(id, result); + } + + return result; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputApiKeyAuth.cs b/logger/autorest.csharp/common/Input/InputTypes/InputApiKeyAuth.cs new file mode 100644 index 0000000..c13ab29 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputApiKeyAuth.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputApiKeyAuth(string Name, string? Prefix) +{ + public InputApiKeyAuth() : this(string.Empty, null) { } + public InputApiKeyAuth(string Name) : this(Name, null) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputAuth.cs b/logger/autorest.csharp/common/Input/InputTypes/InputAuth.cs new file mode 100644 index 0000000..4215c49 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputAuth.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputAuth(InputApiKeyAuth? ApiKey, InputOAuth2Auth? OAuth2) +{ + public InputAuth() : this(null, null) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputClient.cs b/logger/autorest.csharp/common/Input/InputTypes/InputClient.cs new file mode 100644 index 0000000..4967781 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputClient.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputClient(string Name, string Description, IReadOnlyList Operations, IReadOnlyList Parameters, string? Parent) +{ + private readonly string? _key; + + public string Key + { + get => _key ?? Name; + init => _key = value; + } + + public IReadOnlyList Operations { get; internal set; } = Operations ?? Array.Empty(); + public IReadOnlyList Decorators { get; internal set; } = new List(); + + public InputClient() : this(string.Empty, string.Empty, Array.Empty(), Array.Empty(), null) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputConstant.cs b/logger/autorest.csharp/common/Input/InputTypes/InputConstant.cs new file mode 100644 index 0000000..12acef8 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputConstant.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputConstant(object? Value, InputType Type); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputDateTimeType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputDateTimeType.cs new file mode 100644 index 0000000..09ed871 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputDateTimeType.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputDateTimeType(DateTimeKnownEncoding Encode, string Name, string CrossLanguageDefinitionId, InputPrimitiveType WireType, InputDateTimeType? BaseType = null) : InputType(Name); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputDecoratorInfo.cs b/logger/autorest.csharp/common/Input/InputTypes/InputDecoratorInfo.cs new file mode 100644 index 0000000..fd5529c --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputDecoratorInfo.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input +{ + public class InputDecoratorInfo + { + public InputDecoratorInfo(string name, IReadOnlyDictionary? arguments) + { + Name = name; + Arguments = arguments; + } + public string Name { get; } + public IReadOnlyDictionary? Arguments { get; } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputDictionaryType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputDictionaryType.cs new file mode 100644 index 0000000..65246e9 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputDictionaryType.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputDictionaryType(string Name, InputType KeyType, InputType ValueType) : InputType(Name) { } diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputDurationType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputDurationType.cs new file mode 100644 index 0000000..5d04225 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputDurationType.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputDurationType(DurationKnownEncoding Encode, string Name, string CrossLanguageDefinitionId, InputPrimitiveType WireType, InputDurationType? BaseType = null) : InputType(Name); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputEnumType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputEnumType.cs new file mode 100644 index 0000000..347d242 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputEnumType.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputEnumType(string Name, string CrossLanguageDefinitionId, string? Accessibility, string? Deprecated, string Description, InputModelTypeUsage Usage, InputPrimitiveType ValueType, IReadOnlyList Values, bool IsExtensible) + : InputType(Name); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeFloatValue.cs b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeFloatValue.cs new file mode 100644 index 0000000..fd36ed5 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeFloatValue.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputEnumTypeFloatValue(string Name, float FloatValue, string? Description) : InputEnumTypeValue(Name, FloatValue, Description); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeIntegerValue.cs b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeIntegerValue.cs new file mode 100644 index 0000000..4e893e3 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeIntegerValue.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputEnumTypeIntegerValue(string Name, int IntegerValue, string? Description) : InputEnumTypeValue(Name, IntegerValue, Description); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeStringValue.cs b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeStringValue.cs new file mode 100644 index 0000000..cf6c83f --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeStringValue.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputEnumTypeStringValue(string Name, string StringValue, string? Description) : InputEnumTypeValue(Name, StringValue, Description); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeValue.cs b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeValue.cs new file mode 100644 index 0000000..aad8f11 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputEnumTypeValue.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputEnumTypeValue(string Name, object Value, string? Description) +{ + public virtual string GetJsonValueString() => GetValueString(); + public string GetValueString() => (Value.ToString() ?? string.Empty); + + public string Name { get; internal set; } = Name; + public IReadOnlyList Decorators { get; internal set; } = new List(); +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputListType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputListType.cs new file mode 100644 index 0000000..6b69854 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputListType.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputListType(string Name, string CrossLanguageDefinitionId, InputType ValueType) : InputType(Name); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputLiteralType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputLiteralType.cs new file mode 100644 index 0000000..44fd4d1 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputLiteralType.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputLiteralType(InputType ValueType, object Value) : InputType("Literal") // TODO -- name? +{ + // Those two types are actually same, can we merge them? + public static implicit operator InputConstant(InputLiteralType literal) => new(literal.Value, literal.ValueType); +}; diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputModelProperty.cs b/logger/autorest.csharp/common/Input/InputTypes/InputModelProperty.cs new file mode 100644 index 0000000..4c3c10d --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputModelProperty.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputModelProperty(string Name, string SerializedName, string Description, InputType Type, InputConstant? ConstantValue, bool IsRequired, bool IsReadOnly, bool IsDiscriminator) +{ + public FormattableString? DefaultValue { get; init; } + + public string Name { get; internal set; } = Name; + public IReadOnlyList Decorators { get; internal set; } = new List(); + + // original emitter input + public bool IsFlattened { get; internal set; } + + // calculated flatten prefix names + public IReadOnlyList? FlattenedNames { get; internal set; } +}; diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputModelType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputModelType.cs new file mode 100644 index 0000000..273a7e1 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputModelType.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Text.Json; +using System.Linq; + +namespace AutoRest.CSharp.Common.Input +{ + internal record InputModelType(string Name, string CrossLanguageDefinitionId, string? Access, string? Deprecation, string? Description, InputModelTypeUsage Usage, IReadOnlyList Properties, InputModelType? BaseModel, IReadOnlyList DerivedModels, string? DiscriminatorValue, InputModelProperty? DiscriminatorProperty, IReadOnlyDictionary DiscriminatedSubtypes, InputType? AdditionalProperties, IReadOnlyList? ArgumentTypes = null) + : InputType(Name) + { + /// + /// Indicates if this model is the Unknown derived version of a model with discriminator + /// + public bool IsUnknownDiscriminatorModel { get; init; } = false; + /// + /// Indicates if this model is a property bag + /// + public bool IsPropertyBag { get; init; } = false; + + public InputModelType? BaseModel { get; private set; } = BaseModel; + /** In some case, its base model will have a propety whose type is the model, in tspCodeModel.json, the property type is a reference, + * during descerializing, we need to create the model and add it to the referernce map before load base model, otherwise, the deserialization crash. + * Then we need to set the BaseModel to the model instance after the base model is loaded. That is BaseModel is settable. + * This function is to set the BaseModel to an existing model instance. + */ + internal void SetBaseModel(InputModelType? baseModel, [CallerFilePath] string filepath = "", [CallerMemberName] string caller = "") + { + Debug.Assert(filepath.EndsWith($"{nameof(TypeSpecInputModelTypeConverter)}.cs"), $"This method is only allowed to be called in `TypeSpecInputModelTypeConverter.cs`"); + Debug.Assert(caller == nameof(TypeSpecInputModelTypeConverter.CreateModelType), $"This method is only allowed to be called in `TypeSpecInputModelTypeConverter.CreateModelType`"); + BaseModel = baseModel; + } + + public IEnumerable GetSelfAndBaseModels() => EnumerateBase(this); + + public IEnumerable GetAllBaseModels() => EnumerateBase(BaseModel); + + // The setter is only used for swagger input + private bool? _useSystemTextJsonConverter; + public bool UseSystemTextJsonConverter + { + get + { + return _useSystemTextJsonConverter ?? Decorators.Any(x => x.Name == "Azure.ClientGenerator.Core.@useSystemTextJsonConverter"); + } + internal set + { + _useSystemTextJsonConverter = value; + } + } + + private static IEnumerable EnumerateBase(InputModelType? model) + { + while (model != null) + { + yield return model; + model = model.BaseModel; + } + } + + public bool Equals(InputType other, bool handleCollections) + { + if (!handleCollections) + return Equals(other); + + switch (other) + { + case InputDictionaryType otherDictionary: + return Equals(otherDictionary.ValueType); + case InputListType otherList: + return Equals(otherList.ValueType); + default: + return Equals(other); + } + } + + internal InputModelProperty? GetProperty(InputModelType key) + { + foreach (var property in Properties) + { + if (key.Equals(property.Type, true)) + return property; + } + return null; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputModelTypeUsage.cs b/logger/autorest.csharp/common/Input/InputTypes/InputModelTypeUsage.cs new file mode 100644 index 0000000..2d1a428 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputModelTypeUsage.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace AutoRest.CSharp.Common.Input; + +[Flags] +internal enum InputModelTypeUsage +{ + None = 0, + Input = 1 << 1, + Output = 1 << 2, + ApiVersionEnum = 1 << 3, + JsonMergePatch = 1 << 4, + MultipartFormData = 1 << 5, + Spread = 1 << 6, + Error = 1 << 7, + Json = 1 << 8, + Xml = 1 << 9 +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputNamespace.cs b/logger/autorest.csharp/common/Input/InputTypes/InputNamespace.cs new file mode 100644 index 0000000..508e1ce --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputNamespace.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputNamespace(string Name, IReadOnlyList ApiVersions, IReadOnlyList Enums, IReadOnlyList Models, IReadOnlyList Clients, InputAuth Auth) +{ + public InputNamespace() : this(Name: string.Empty, ApiVersions: Array.Empty(), Enums: Array.Empty(), Models: Array.Empty(), Clients: Array.Empty(), Auth: new InputAuth()) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputNullableType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputNullableType.cs new file mode 100644 index 0000000..06a2e92 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputNullableType.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input.InputTypes +{ + internal sealed record InputNullableType(InputType Type) : InputType("nullable") + { + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputOAuth2Auth.cs b/logger/autorest.csharp/common/Input/InputTypes/InputOAuth2Auth.cs new file mode 100644 index 0000000..e3d63cf --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputOAuth2Auth.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputOAuth2Auth(IReadOnlyCollection Scopes) +{ + public InputOAuth2Auth() : this(Array.Empty()) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputOperation.cs b/logger/autorest.csharp/common/Input/InputTypes/InputOperation.cs new file mode 100644 index 0000000..6c84e09 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputOperation.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Utilities; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputOperation +{ + public InputOperation( + string name, + string? resourceName, + string? summary, + string? deprecated, + string description, + string? accessibility, + IReadOnlyList parameters, + IReadOnlyList responses, + RequestMethod httpMethod, + BodyMediaType requestBodyMediaType, + string uri, + string path, + string? externalDocsUrl, + IReadOnlyList? requestMediaTypes, + bool bufferResponse, + OperationLongRunning? longRunning, + OperationPaging? paging, + bool generateProtocolMethod, + bool generateConvenienceMethod, + string crossLanguageDefinitionId, + bool keepClientDefaultValue, + IReadOnlyList? examples = null) + { + Name = name; + SpecName = name; + ResourceName = resourceName; + Summary = summary; + Deprecated = deprecated; + Description = description; + Accessibility = accessibility; + Parameters = parameters; + Responses = responses; + HttpMethod = httpMethod; + RequestBodyMediaType = requestBodyMediaType; + Uri = uri; + Path = path; + ExternalDocsUrl = externalDocsUrl; + RequestMediaTypes = requestMediaTypes; + BufferResponse = bufferResponse; + LongRunning = longRunning; + Paging = paging; + GenerateProtocolMethod = generateProtocolMethod; + GenerateConvenienceMethod = generateConvenienceMethod; + CrossLanguageDefinitionId = crossLanguageDefinitionId; + KeepClientDefaultValue = keepClientDefaultValue; + _examples = examples; + } + + public InputOperation() : this( + name: string.Empty, + resourceName: null, + summary: null, + deprecated: null, + description: string.Empty, + accessibility: null, + parameters: Array.Empty(), + responses: Array.Empty(), + httpMethod: RequestMethod.Get, + requestBodyMediaType: BodyMediaType.None, + uri: string.Empty, + path: string.Empty, + externalDocsUrl: null, + requestMediaTypes: Array.Empty(), + bufferResponse: false, + longRunning: null, + paging: null, + generateProtocolMethod: true, + generateConvenienceMethod: false, + crossLanguageDefinitionId: string.Empty, + keepClientDefaultValue: false) + { + SpecName = string.Empty; + } + + public static InputOperation RemoveApiVersionParam(InputOperation operation) + { + return new InputOperation( + operation.Name, + operation.ResourceName, + operation.Summary, + operation.Deprecated, + operation.Description, + operation.Accessibility, + operation.Parameters.Where(p => !p.IsApiVersion).ToList(), + operation.Responses, + operation.HttpMethod, + operation.RequestBodyMediaType, + operation.Uri, + operation.Path, + operation.ExternalDocsUrl, + operation.RequestMediaTypes, + operation.BufferResponse, + operation.LongRunning, + operation.Paging, + operation.GenerateProtocolMethod, + operation.GenerateConvenienceMethod, + operation.CrossLanguageDefinitionId, + operation.KeepClientDefaultValue) + { + SpecName = operation.SpecName + }; + } + + public string CleanName => Name.IsNullOrEmpty() ? string.Empty : Name.ToCleanName(); + + private IReadOnlyList? _examples; + public IReadOnlyList Examples => _examples ??= EnsureExamples(); + + private IReadOnlyList EnsureExamples() + { + // see if we need to generate the mock examples + if (Configuration.ExamplesDirectory != null || Configuration.AzureArm) + { + return Array.Empty(); + } + + // build the mock examples + return ExampleMockValueBuilder.BuildOperationExamples(this); + } + + public bool IsLongRunning => LongRunning != null; + public string Name { get; internal set; } + public string? ResourceName { get; } + public string? Summary { get; } + public string? Deprecated { get; } + public string Description { get; } + public string? Accessibility { get; } + public IReadOnlyList Parameters { get; init; } + public IReadOnlyList Responses { get; } + public RequestMethod HttpMethod { get; } + public BodyMediaType RequestBodyMediaType { get; } + public string Uri { get; init; } + public string Path { get; } + public string? ExternalDocsUrl { get; } + public IReadOnlyList? RequestMediaTypes { get; } + public bool BufferResponse { get; } + public OperationLongRunning? LongRunning { get; } + public OperationPaging? Paging { get; init; } + public bool GenerateProtocolMethod { get; } + public bool GenerateConvenienceMethod { get; } + public string CrossLanguageDefinitionId { get; } + public bool KeepClientDefaultValue { get; } + public string? OperationName { get; } + public string? OperationVersion { get; } + public string? OperationType { get; } + public string OperationId => ResourceName is null ? Name : $"{ResourceName}_{Name.FirstCharToUpperCase()}"; + public IReadOnlyList Decorators { get; internal set; } = new List(); + //TODO: Remove this until the SDK nullable is enabled, tracking in https://github.com/Azure/autorest.csharp/issues/4780 + internal string SpecName { get; init; } + + internal bool IsNameChanged { get; init; } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputOperationParameterKind.cs b/logger/autorest.csharp/common/Input/InputTypes/InputOperationParameterKind.cs new file mode 100644 index 0000000..7078768 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputOperationParameterKind.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal enum InputOperationParameterKind +{ + Method = 0, + Client = 1, + Constant = 2, + Flattened = 3, + Spread = 4, + Grouped = 5, +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputParameter.cs b/logger/autorest.csharp/common/Input/InputTypes/InputParameter.cs new file mode 100644 index 0000000..e3a3dae --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputParameter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputParameter( + string Name, + string NameInRequest, + string? Description, + InputType Type, + RequestLocation Location, + InputConstant? DefaultValue, + // TODO: This should be removed, tracking it in https://github.com/Azure/autorest.csharp/issues/4779 + InputParameter? GroupedBy, + InputModelProperty? FlattenedBodyProperty, + InputOperationParameterKind Kind, + bool IsRequired, + bool IsApiVersion, + bool IsResourceParameter, + bool IsContentType, + bool IsEndpoint, + bool SkipUrlEncoding, + bool Explode, + string? ArraySerializationDelimiter, + string? HeaderCollectionPrefix) +{ + public InputParameter() : this( + Name: string.Empty, + NameInRequest: string.Empty, + Description: null, + Type: InputPrimitiveType.Unknown, + Location: RequestLocation.None, + DefaultValue: null, + FlattenedBodyProperty: null, + GroupedBy: null, + Kind: InputOperationParameterKind.Method, + IsRequired: false, + IsApiVersion: false, + IsResourceParameter: false, + IsContentType: false, + IsEndpoint: false, + SkipUrlEncoding: false, + Explode: false, + ArraySerializationDelimiter: null, + HeaderCollectionPrefix: null) + { } + + public string Name { get; internal set; } = Name; + public bool IsRequired { get; internal set; } = IsRequired; + public InputType Type { get; internal set; } = Type; + public InputOperationParameterKind Kind { get; internal set; } = Kind; + public IReadOnlyList Decorators { get; internal set; } = new List(); +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputPrimitiveType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputPrimitiveType.cs new file mode 100644 index 0000000..271a652 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputPrimitiveType.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputPrimitiveType(InputPrimitiveTypeKind Kind, string Name, string CrossLanguageDefinitionId) : InputType(Name) +{ + #region Scalars defined in typespec-azure-core + internal const string UuidId = "Azure.Core.uuid"; + internal const string IPv4AddressId = "Azure.Core.ipV4Address"; + internal const string IPv6AddressId = "Azure.Core.ipV6Address"; + internal const string ETagId = "Azure.Core.eTag"; + internal const string AzureLocationId = "Azure.Core.azureLocation"; + internal const string ArmIdId = "Azure.Core.armResourceIdentifier"; + #endregion + + #region Types we supported but not yet added to typespec-azure-core + internal const string CharId = "Azure.Core.char"; + internal const string ContentTypeId = "Azure.Core.contentType"; + internal const string ResourceTypeId = "Azure.Core.resourceType"; + internal const string RequestMethodId = "Azure.Core.requestMethod"; + #endregion + + #region These types are here only for backward compatibility + internal const string ObjectId = "Obsolete.object"; + internal const string IPAddressId = "Temp.ipAddress"; + #endregion + + internal InputPrimitiveType(InputPrimitiveTypeKind kind, string name, string crossLanguageDefinitionId, string? encode) : this(kind, name, crossLanguageDefinitionId) + { + Encode = encode; + } + internal InputPrimitiveType(InputPrimitiveTypeKind kind, string name, string crossLanguageDefinitionId, string? encode, InputPrimitiveType? baseType) : this(kind, name, crossLanguageDefinitionId) + { + Encode = encode; + BaseType = baseType; + } + + public string? Encode { get; init; } + public InputPrimitiveType? BaseType { get; init; } + + public static InputPrimitiveType AzureLocation { get; } = new(InputPrimitiveTypeKind.String, "azureLocation", AzureLocationId); + public static InputPrimitiveType Boolean { get; } = new(InputPrimitiveTypeKind.Boolean, "boolean", "TypeSpec.boolean"); + public static InputPrimitiveType Base64 { get; } = new(InputPrimitiveTypeKind.Bytes, "bytes", "TypeSpec.bytes", BytesKnownEncoding.Base64); + public static InputPrimitiveType Base64Url { get; } = new(InputPrimitiveTypeKind.Bytes, "bytes", "TypeSpec.bytes", BytesKnownEncoding.Base64Url); + public static InputPrimitiveType Char { get; } = new(InputPrimitiveTypeKind.String, "char", CharId); + public static InputPrimitiveType ContentType { get; } = new(InputPrimitiveTypeKind.String, "contentType", ContentTypeId); + public static InputPrimitiveType PlainDate { get; } = new(InputPrimitiveTypeKind.PlainDate, "plainDate", "TypeSpec.plainDate"); + public static InputPrimitiveType ETag { get; } = new(InputPrimitiveTypeKind.String, "eTag", ETagId); + public static InputPrimitiveType Float32 { get; } = new(InputPrimitiveTypeKind.Float32, "float32", "TypeSpec.float32"); + public static InputPrimitiveType Float64 { get; } = new(InputPrimitiveTypeKind.Float64, "float64", "TypeSpec.float64"); + public static InputPrimitiveType Decimal128 { get; } = new(InputPrimitiveTypeKind.Decimal128, "decimal128", "TypeSpec.decimal128"); + public static InputPrimitiveType Uuid { get; } = new(InputPrimitiveTypeKind.String, "uuid", UuidId); + public static InputPrimitiveType Int32 { get; } = new(InputPrimitiveTypeKind.Int32, "int32", "TypeSpec.int32"); + public static InputPrimitiveType Int64 { get; } = new(InputPrimitiveTypeKind.Int64, "int64", "TypeSpec.int64"); + public static InputPrimitiveType ResourceIdentifier { get; } = new(InputPrimitiveTypeKind.String, "armResourceIdentifier", ArmIdId); + public static InputPrimitiveType ResourceType { get; } = new(InputPrimitiveTypeKind.String, "resourceType", ResourceTypeId); + public static InputPrimitiveType RequestMethod { get; } = new(InputPrimitiveTypeKind.String, "requestMethod", RequestMethodId); + public static InputPrimitiveType Stream { get; } = new(InputPrimitiveTypeKind.Stream, "stream", "TypeSpec.Stream"); // TODO -- this is not a builtin type + public static InputPrimitiveType String { get; } = new(InputPrimitiveTypeKind.String, "string", "TypeSpec.string"); + public static InputPrimitiveType PlainTime { get; } = new(InputPrimitiveTypeKind.PlainTime, "plainTime", "TypeSpec.plainTime"); + public static InputPrimitiveType Url { get; } = new(InputPrimitiveTypeKind.Url, "url", "TypeSpec.url"); + public static InputPrimitiveType Unknown { get; } = new(InputPrimitiveTypeKind.Unknown, "unknown", string.Empty); + public static InputPrimitiveType Object { get; } = new(InputPrimitiveTypeKind.Unknown, "object", ObjectId); + + public static InputPrimitiveType IPAddress { get; } = new(InputPrimitiveTypeKind.String, "ipAddress", IPAddressId); + + public bool IsNumber => Kind is InputPrimitiveTypeKind.Integer or InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Int32 or InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.Float32 or InputPrimitiveTypeKind.Float64 or InputPrimitiveTypeKind.Decimal or InputPrimitiveTypeKind.Decimal128 or InputPrimitiveTypeKind.Numeric; +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputPrimitiveTypeKind.cs b/logger/autorest.csharp/common/Input/InputTypes/InputPrimitiveTypeKind.cs new file mode 100644 index 0000000..1dd7ff6 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputPrimitiveTypeKind.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal enum InputPrimitiveTypeKind +{ + Boolean, + Bytes, + PlainDate, + Decimal, + Decimal128, + Numeric,// in typespec, numeric is the base type of all number types, see type relation: https://typespec.io/docs/language-basics/type-relations + Float, // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations + Float32, + Float64, + Integer, // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations + Int8, // aka SByte + Int16, + Int32, + Int64, + SafeInt, + UInt8, // aka Byte + UInt16, + UInt32, + UInt64, + Stream, + String, + PlainTime, + Url, + Unknown, +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputType.cs new file mode 100644 index 0000000..3613879 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputType.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input.InputTypes; + +namespace AutoRest.CSharp.Common.Input; + +internal abstract record InputType +{ + protected InputType(string name) + { + Name = name; + SpecName = name; + } + + public InputTypeSerialization Serialization { get; init; } = InputTypeSerialization.Default; + + internal InputType GetCollectionEquivalent(InputType inputType) + { + switch (this) + { + case InputListType listType: + return new InputListType( + listType.Name, + listType.CrossLanguageDefinitionId, + listType.ValueType.GetCollectionEquivalent(inputType)) + { + Decorators = listType.Decorators + }; + case InputDictionaryType dictionaryType: + return new InputDictionaryType( + dictionaryType.Name, + dictionaryType.KeyType, + dictionaryType.ValueType.GetCollectionEquivalent(inputType)) + { + Decorators = dictionaryType.Decorators + }; + default: + return inputType; + } + } + + //public bool IsNullable { get; init; } + public string Name { get; internal set; } + + public IReadOnlyList Decorators { get; internal set; } = new List(); + + //TODO: Remove this until the SDK nullable is enabled, traking in https://github.com/Azure/autorest.csharp/issues/4780 + internal string? SpecName { get; init; } + + public InputType WithNullable(bool isNullable) + { + if (isNullable) + return new InputNullableType(this); + return this; + } + public InputType GetImplementType() => this switch + { + InputNullableType nullableType => nullableType.Type, + _ => this + }; +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputTypeSerialization.cs b/logger/autorest.csharp/common/Input/InputTypes/InputTypeSerialization.cs new file mode 100644 index 0000000..4c71aa2 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputTypeSerialization.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +// TODO: Remove this class and consume Decorators information instead +internal record InputTypeSerialization(bool Json, InputTypeXmlSerialization? Xml) +{ + public static InputTypeSerialization Default { get; } = new(true, null); +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputTypeXmlSerialization.cs b/logger/autorest.csharp/common/Input/InputTypes/InputTypeXmlSerialization.cs new file mode 100644 index 0000000..40ec044 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputTypeXmlSerialization.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record InputTypeXmlSerialization(string? Name, bool IsAttribute, bool IsContent, bool IsWrapped); diff --git a/logger/autorest.csharp/common/Input/InputTypes/InputUnionType.cs b/logger/autorest.csharp/common/Input/InputTypes/InputUnionType.cs new file mode 100644 index 0000000..845cbf6 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/InputUnionType.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record InputUnionType(string Name, IReadOnlyList VariantTypes) : InputType(Name); diff --git a/logger/autorest.csharp/common/Input/InputTypes/OperationLongRunning.cs b/logger/autorest.csharp/common/Input/InputTypes/OperationLongRunning.cs new file mode 100644 index 0000000..4ebb14f --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/OperationLongRunning.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; +using System.Linq; + +namespace AutoRest.CSharp.Common.Input; + +internal record OperationLongRunning(OperationFinalStateVia FinalStateVia, OperationResponse FinalResponse, string? ResultPath) +{ + public OperationLongRunning() : this(FinalStateVia: OperationFinalStateVia.Location, FinalResponse: new OperationResponse(), null) { } + + /// + /// Meaningful return type of the long running operation. + /// + public InputType? ReturnType + { + get + { + if (FinalResponse.BodyType is null) + return null; + + if (ResultPath is null) + return FinalResponse.BodyType; + + var rawResponseType = (InputModelType)FinalResponse.BodyType; + return rawResponseType.Properties.FirstOrDefault(p => p.SerializedName == ResultPath)!.Type; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/OperationPaging.cs b/logger/autorest.csharp/common/Input/InputTypes/OperationPaging.cs new file mode 100644 index 0000000..38c0807 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/OperationPaging.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input; + +internal record OperationPaging(string? NextLinkName, string? ItemName, InputOperation? NextLinkOperation, bool SelfNextLink) +{ + public OperationPaging() : this(null, null, null, false) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/OperationResponse.cs b/logger/autorest.csharp/common/Input/InputTypes/OperationResponse.cs new file mode 100644 index 0000000..afd62c4 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/OperationResponse.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record OperationResponse(IReadOnlyList StatusCodes, InputType? BodyType, BodyMediaType BodyMediaType, IReadOnlyList Headers, bool IsErrorResponse, IReadOnlyList ContentTypes) +{ + public OperationResponse() : this(StatusCodes: Array.Empty(), BodyType: null, BodyMediaType: BodyMediaType.None, Headers: Array.Empty(), IsErrorResponse: false, ContentTypes: Array.Empty()) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/OperationResponseHeader.cs b/logger/autorest.csharp/common/Input/InputTypes/OperationResponseHeader.cs new file mode 100644 index 0000000..b5b3601 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/OperationResponseHeader.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal record OperationResponseHeader(string Name, string NameInResponse, string Description, InputType Type) +{ + public OperationResponseHeader() : this("", "", "", InputPrimitiveType.String) { } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/RequestLocation.cs b/logger/autorest.csharp/common/Input/InputTypes/RequestLocation.cs new file mode 100644 index 0000000..ad64474 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/RequestLocation.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +namespace AutoRest.CSharp.Common.Input +{ + internal enum RequestLocation + { + None, + Uri, + Path, + Query, + Header, + Body, + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputClientConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputClientConverter.cs new file mode 100644 index 0000000..857bb64 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputClientConverter.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Input; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputClientConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputClientConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputClient? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputClient(ref reader, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputClient value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private static InputClient? CreateInputClient(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + string? name = null; + string? description = null; + IReadOnlyList? operations = null; + IReadOnlyList? parameters = null; + string? parent = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString(nameof(InputClient.Name), ref name) + || reader.TryReadString(nameof(InputClient.Description), ref description) + || reader.TryReadWithConverter(nameof(InputClient.Operations), options, ref operations) + || reader.TryReadWithConverter(nameof(InputClient.Parameters), options, ref parameters) + || reader.TryReadString(nameof(InputClient.Parent), ref parent); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name= name ?? throw new JsonException("InputClient must have name"); + description = description ?? string.Empty; + operations = operations ?? Array.Empty(); + parameters = parameters ?? Array.Empty(); + var inputClient = new InputClient(name, description, operations, parameters, parent); + if (id != null) + { + resolver.AddReference(id, inputClient); + } + + return inputClient; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputConstantConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputConstantConverter.cs new file mode 100644 index 0000000..4cd40e5 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputConstantConverter.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal class TypeSpecInputConstantConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputConstantConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputConstant Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputConstant(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputConstant value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputConstant CreateInputConstant(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + InputType? type = null; + + reader.TryReadReferenceId(ref isFirstProperty, ref id); + if (!reader.TryReadWithConverter(nameof(InputConstant.Type), options, ref type)) + { + throw new JsonException("Must provide type ahead of value."); + } + var value = ReadConstantValue(ref reader, nameof(InputConstant.Value), options, type); + + type = type ?? throw new JsonException("InputConstant must have type"); + + value = value ?? throw new JsonException("InputConstant must have value"); + + var constant = new InputConstant(value, type); + if (id != null) + { + resolver.AddReference(id, constant); + } + return constant; + } + + public static object ReadConstantValue(ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, InputType? type) + { + if (type == null) + { + throw new JsonException("Must place type ahead of value."); + } + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + throw new JsonException("This is not for json field " + propertyName); + } + + reader.Read(); + object? value; + switch (type) { + case InputPrimitiveType primitype: + switch (primitype.Kind) + { + case InputPrimitiveTypeKind.String: + value = reader.GetString() ?? throw new JsonException(); + break; + case InputPrimitiveTypeKind.Url: + var stringValue = reader.GetString() ?? throw new JsonException(); + value = new Uri(stringValue); + break; + case InputPrimitiveTypeKind.Int32: + value = reader.GetInt32(); + break; + case InputPrimitiveTypeKind.Int64: + value = reader.GetInt64(); + break; + case InputPrimitiveTypeKind.Boolean: + value = reader.GetBoolean(); + break; + default: + value = reader.GetString() ?? throw new JsonException(); + break; + + } + break; + case InputEnumType enumType: + switch (enumType.ValueType.Kind) + { + case InputPrimitiveTypeKind.String: + value = reader.GetString() ?? throw new JsonException(); + break; + case InputPrimitiveTypeKind.Int32: + value = reader.GetInt32(); + break; + case InputPrimitiveTypeKind.Float32: + value = reader.GetDouble(); + break; + default: + throw new JsonException($"Unsupported enum value type: {enumType.ValueType.Kind}"); + } + break; + case InputLiteralType literalType: + value = literalType.Value; + break; + default: + throw new JsonException($"Not supported type: {type.GetType()}"); + } + reader.Read(); + return value; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDateTimeTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDateTimeTypeConverter.cs new file mode 100644 index 0000000..8e3189b --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDateTimeTypeConverter.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal class TypeSpecInputDateTimeTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputDateTimeTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputDateTimeType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDateTimeType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputDateTimeType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputDateTimeType CreateDateTimeType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + string? crossLanguageDefinitionId = null; + string? encode = null; + InputPrimitiveType? wireType = null; + InputDateTimeType? baseType = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("crossLanguageDefinitionId", ref crossLanguageDefinitionId) + || reader.TryReadString("encode", ref encode) + || reader.TryReadWithConverter("wireType", options, ref wireType) + || reader.TryReadWithConverter("baseType", options, ref baseType) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("DateTime type must have name"); + crossLanguageDefinitionId = crossLanguageDefinitionId ?? throw new JsonException("DateTime type must have crossLanguageDefinitionId"); + encode = encode ?? throw new JsonException("DateTime type must have encoding"); + wireType = wireType ?? throw new JsonException("DateTime type must have wireType"); + + var dateTimeType = Enum.TryParse(encode, ignoreCase: true, out var encodeKind) + ? new InputDateTimeType(encodeKind, name, crossLanguageDefinitionId, wireType, baseType) + { + Decorators = decorators ?? Array.Empty() + } + : throw new JsonException($"Encoding of DateTime type {encode} is unknown."); + + if (id != null) + { + resolver.AddReference(id, dateTimeType); + } + return dateTimeType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDecoratorInfoConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDecoratorInfoConverter.cs new file mode 100644 index 0000000..4910be4 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDecoratorInfoConverter.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal class TypeSpecInputDecoratorInfoConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputDecoratorInfoConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputDecoratorInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDecoratorInfo(ref reader, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputDecoratorInfo value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private static InputDecoratorInfo? CreateDecoratorInfo(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + string? name = null; + IReadOnlyDictionary? arguments = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString(nameof(InputDecoratorInfo.Name).ToLower(), ref name) + || reader.TryReadStringBinaryDataDictionary(nameof(InputDecoratorInfo.Arguments).ToLower(), ref arguments); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + reader.Read(); + var decoratorInfo = new InputDecoratorInfo(name ?? throw new JsonException("InputDecoratorInfo must have name"), arguments); + + if (id != null) + { + resolver.AddReference(id, decoratorInfo); + } + return decoratorInfo; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs new file mode 100644 index 0000000..ef28ee8 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputDictionaryTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputDictionaryTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputDictionaryType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDictionaryType(ref reader, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputDictionaryType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputDictionaryType CreateDictionaryType(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + InputType? keyType = null; + InputType? valueType = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter("keyType", options, ref keyType) + || reader.TryReadWithConverter("valueType", options, ref valueType) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + keyType = keyType ?? throw new JsonException("Dictionary must have key type"); + valueType = valueType ?? throw new JsonException("Dictionary must have value type"); + + var dictType = new InputDictionaryType("Dictionary", keyType, valueType) + { + Decorators = decorators ?? [] + }; + if (id != null) + { + resolver.AddReference(id, dictType); + } + return dictType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDurationTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDurationTypeConverter.cs new file mode 100644 index 0000000..e04630d --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputDurationTypeConverter.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal class TypeSpecInputDurationTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputDurationTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputDurationType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDurationType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputDurationType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputDurationType CreateDurationType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + string? crossLanguageDefinitionId = null; + string? encode = null; + InputPrimitiveType? wireType = null; + InputDurationType? baseType = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("crossLanguageDefinitionId", ref crossLanguageDefinitionId) + || reader.TryReadString("encode", ref encode) + || reader.TryReadWithConverter("wireType", options, ref wireType) + || reader.TryReadWithConverter("baseType", options, ref baseType) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("Duration type must have name"); + crossLanguageDefinitionId = crossLanguageDefinitionId ?? throw new JsonException("Duration type must have crossLanguageDefinitionId"); + encode = encode ?? throw new JsonException("Duration type must have encode"); + wireType = wireType ?? throw new JsonException("Duration type must have wireType"); + + var dateTimeType = Enum.TryParse(encode, ignoreCase: true, out var encodeKind) + ? new InputDurationType(encodeKind, name, crossLanguageDefinitionId, wireType, baseType) { Decorators = decorators ?? [] } + : throw new JsonException($"Encoding of Duration type {encode} is unknown."); + + if (id != null) + { + resolver.AddReference(id, dateTimeType); + } + return dateTimeType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs new file mode 100644 index 0000000..87ae604 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputEnumTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputEnumTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputEnumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateEnumType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputEnumType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputEnumType CreateEnumType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && name == null; + string? crossLanguageDefinitionId = null; + string? access = null; + string? deprecation = null; + string? description = null; + string? usageString = null; + bool isFixed = false; + InputType? valueType = null; + IReadOnlyList? values = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("crossLanguageDefinitionId", ref crossLanguageDefinitionId) + || reader.TryReadString("access", ref access) + || reader.TryReadString("deprecation", ref deprecation) + || reader.TryReadString("description", ref description) + || reader.TryReadString("usage", ref usageString) + || reader.TryReadBoolean("isFixed", ref isFixed) + || reader.TryReadWithConverter("valueType", options, ref valueType) + || reader.TryReadWithConverter("values", options, ref values) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("Enum must have name"); + // TODO: roll back to throw JSON error when there is linter on the upstream to check enum without @doc + //description = description ?? throw new JsonException("Enum must have a description"); + if (description.IsNullOrEmpty()) + { + Console.Error.WriteLine($"[Warn]: Enum '{name}' must have a description"); + description = $"The {name}."; + } + + if (!Enum.TryParse(usageString, out var usage)) + { + throw new JsonException($"Cannot parse usage {usageString}"); + } + + if (values == null || values.Count == 0) + { + throw new JsonException("Enum must have at least one value"); + } + + if (valueType is not InputPrimitiveType inputValueType) + { + throw new JsonException("The ValueType of an EnumType must be a primitive type."); + } + + var enumType = new InputEnumType(name, crossLanguageDefinitionId ?? string.Empty, access, deprecation, description!, usage, inputValueType, values, !isFixed) + { + Decorators = decorators ?? [] + }; + if (id != null) + { + resolver.AddReference(id, enumType); + } + return enumType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputEnumTypeValueConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputEnumTypeValueConverter.cs new file mode 100644 index 0000000..d704417 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputEnumTypeValueConverter.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputEnumTypeValueConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputEnumTypeValueConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputEnumTypeValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateEnumTypeValue(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputEnumTypeValue value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private static InputEnumTypeValue CreateEnumTypeValue(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + JsonElement? rawValue = null; + InputPrimitiveType? valueType = null; + string? description = null; + IReadOnlyList? decorators = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadWithConverter("value", options, ref rawValue) + || reader.TryReadWithConverter("valueType", options, ref valueType) + || reader.TryReadString("description", ref description) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("EnumValue must have name"); + + if (rawValue == null) + { + throw new JsonException("EnumValue must have value"); + } + + valueType = valueType ?? throw new JsonException("EnumValue must have valueType"); + + InputEnumTypeValue enumValue = valueType.Kind switch + { + InputPrimitiveTypeKind.String => new InputEnumTypeStringValue(name, rawValue.Value.GetString() ?? throw new JsonException(), description) { Decorators = decorators ?? [] }, + InputPrimitiveTypeKind.Int32 => new InputEnumTypeIntegerValue(name, rawValue.Value.GetInt32(), description) { Decorators = decorators ?? [] }, + InputPrimitiveTypeKind.Float32 => new InputEnumTypeFloatValue(name, rawValue.Value.GetSingle(), description) { Decorators = decorators ?? [] }, + _ => throw new JsonException() + }; + if (id != null) + { + resolver.AddReference(id, enumValue); + } + return enumValue; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs new file mode 100644 index 0000000..6c34783 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputListTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputListTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputListType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateListType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputListType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputListType CreateListType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + string? crossLanguageDefinitionId = null; + InputType? valueType = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("crossLanguageDefinitionId", ref crossLanguageDefinitionId) + || reader.TryReadWithConverter("valueType", options, ref valueType) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("Array must have a name"); + valueType = valueType ?? throw new JsonException("Array must have an value type"); + var listType = new InputListType(name, crossLanguageDefinitionId ?? string.Empty, valueType) { Decorators = decorators ?? Array.Empty() }; + if (id != null) + { + resolver.AddReference(id, listType); + } + return listType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs new file mode 100644 index 0000000..e56702c --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input +{ + internal class TypeSpecInputLiteralTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputLiteralTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputLiteralType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputLiteralType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputLiteralType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputLiteralType CreateInputLiteralType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && name == null; + JsonElement? rawValue = null; + InputType? valueType = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadWithConverter("value", options, ref rawValue) + || reader.TryReadWithConverter("valueType", options, ref valueType) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + valueType = valueType ?? throw new JsonException("InputLiteralType must have type"); + + if (rawValue == null) + { + throw new JsonException("InputLiteralType must have value"); + } + + var valueKind = valueType switch + { + InputPrimitiveType primitiveType => primitiveType.Kind, + InputEnumType enumType => enumType.ValueType.Kind, + _ => throw new JsonException($"InputLiteralType does not support type {valueType.GetType()}") + }; + object value = valueKind switch + { + InputPrimitiveTypeKind.String => rawValue.Value.GetString() ?? throw new JsonException(), + InputPrimitiveTypeKind.Int32 => rawValue.Value.GetInt32(), + InputPrimitiveTypeKind.Float32 => rawValue.Value.GetSingle(), + InputPrimitiveTypeKind.Float64 => rawValue.Value.GetDouble(), + InputPrimitiveTypeKind.Boolean => rawValue.Value.GetBoolean(), + _ => throw new JsonException($"InputLiteralType does not support type {valueKind}") + }; + + var literalType = new InputLiteralType(valueType, value) + { + Decorators = decorators ?? [] + }; + + if (id != null) + { + resolver.AddReference(id, literalType); + } + return literalType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs new file mode 100644 index 0000000..1bb45ea --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputModelPropertyConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputModelPropertyConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputModelProperty Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? ReadInputModelProperty(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputModelProperty value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = true; + string? serializedName = null; + string? description = null; + InputType? propertyType = null; + InputConstant? defaultValue = null; + bool isReadOnly = false; + bool isOptional = false; + bool isDiscriminator = false; + IReadOnlyList? decorators = null; + bool isFlattened = false; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("serializedName", ref serializedName) + || reader.TryReadString("description", ref description) + || reader.TryReadWithConverter("type", options, ref propertyType) + || reader.TryReadBoolean("readOnly", ref isReadOnly) + || reader.TryReadBoolean("optional", ref isOptional) + || reader.TryReadBoolean("discriminator", ref isDiscriminator) + || reader.TryReadWithConverter("decorators", options, ref decorators) + || reader.TryReadBoolean("flatten", ref isFlattened); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException($"{nameof(InputModelProperty)} must have a name."); + description = BuilderHelpers.EscapeXmlDocDescription(description ?? string.Empty); + propertyType = propertyType ?? throw new JsonException($"{nameof(InputModelProperty)} must have a property type."); + + if (propertyType is InputLiteralType lt) + { + defaultValue = new InputConstant(lt.Value, lt.ValueType); + propertyType = lt.ValueType; + } + + var property = new InputModelProperty(name, serializedName ?? name, description, propertyType, defaultValue, !isOptional, isReadOnly, isDiscriminator) + { + Decorators = decorators ?? [], + IsFlattened = isFlattened + }; + if (id != null) + { + resolver.AddReference(id, property); + } + + return property; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs new file mode 100644 index 0000000..cc98645 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Input.InputTypes; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputModelTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputModelTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputModelType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => ReadModelType(ref reader, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputModelType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private static InputModelType? ReadModelType(ref Utf8JsonReader reader, JsonSerializerOptions options, ReferenceResolver resolver) + => reader.ReadReferenceAndResolve(resolver) ?? CreateModelType(ref reader, null, null, options, resolver); + + public static InputModelType CreateModelType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && name == null; + var properties = new List(); + var discriminatedSubtypes = new Dictionary(); + string? crossLanguageDefinitionId = null; + string? access = null; + string? deprecation = null; + string? description = null; + string? usageString = null; + InputModelProperty? discriminatorProperty = null; + string? discriminatorValue = null; + InputType? additionalProperties = null; + InputModelType? baseModel = null; + InputModelType? model = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadString("crossLanguageDefinitionId", ref crossLanguageDefinitionId) + || reader.TryReadString("access", ref access) + || reader.TryReadString("deprecation", ref deprecation) + || reader.TryReadString("description", ref description) + || reader.TryReadString("usage", ref usageString) + || reader.TryReadWithConverter("discriminatorProperty", options, ref discriminatorProperty) + || reader.TryReadString("discriminatorValue", ref discriminatorValue) + || reader.TryReadWithConverter("additionalProperties", options, ref additionalProperties) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (isKnownProperty) + { + continue; + } + /** + * If the model has base model, `BaseModel` and `Properties` should be the last two items in tspCodeModel. + * and `BaseModel` should be last but one, and `Properties` should be the last one. + */ + if (reader.GetString() == "baseModel") + { + model = CreateInputModelTypeInstance(id, name, crossLanguageDefinitionId, access, deprecation, description, usageString, discriminatorValue, discriminatorProperty, baseModel, properties, discriminatedSubtypes, additionalProperties, decorators, resolver); + reader.TryReadWithConverter("baseModel", options, ref baseModel); + if (baseModel != null) + { + model.SetBaseModel(baseModel); + var baseModelDerived = (List)resolver.ResolveReference($"{baseModel.Name}.{nameof(InputModelType.DerivedModels)}"); + baseModelDerived.Add(model); + } + continue; + } + if (reader.GetString() == "properties") + { + model = model ?? CreateInputModelTypeInstance(id, name, crossLanguageDefinitionId, access, deprecation, description, usageString, discriminatorValue, discriminatorProperty, baseModel, properties, discriminatedSubtypes, additionalProperties, decorators, resolver); + reader.Read(); + CreateProperties(ref reader, properties, options, name!, model.Usage.HasFlag(InputModelTypeUsage.MultipartFormData)); + continue; + } + if (reader.GetString() == "discriminatedSubtypes") + { + model = model ?? CreateInputModelTypeInstance(id, name, crossLanguageDefinitionId, access, deprecation, description, usageString, discriminatorValue, discriminatorProperty, baseModel, properties, discriminatedSubtypes, additionalProperties, decorators, resolver); + reader.Read(); + CreateDiscriminatedSubtypes(ref reader, discriminatedSubtypes, options); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException($"{nameof(InputModelType)}.{nameof(InputModelType.Properties)} must be the last defined property for id '{id}', name '{name}'"); + } + continue; + } + + reader.SkipProperty(); + } + + var result = model ?? CreateInputModelTypeInstance(id, name, crossLanguageDefinitionId, access, deprecation, description, usageString, discriminatorValue, discriminatorProperty, baseModel, properties, discriminatedSubtypes, additionalProperties, decorators, resolver); + result.Decorators = decorators ?? Array.Empty(); + return result; + } + + private static InputModelType CreateInputModelTypeInstance(string? id, string? name, string? crossLanguageDefinitionId, string? accessibility, string? deprecated, string? description, string? usageString, string? discriminatorValue, InputModelProperty? discriminatorProperty, InputModelType? baseModel, IReadOnlyList properties, IReadOnlyDictionary discriminatedSubtypes, InputType? additionalProperties, IReadOnlyList? decorators, ReferenceResolver resolver) + { + name = name ?? throw new JsonException("Model must have name"); + if (!Enum.TryParse(usageString, out var usage)) + { + throw new JsonException($"Cannot parse usage {usageString}"); + } + + var derivedModels = new List(); + decorators = decorators ?? Array.Empty(); + var model = new InputModelType(name, crossLanguageDefinitionId ?? string.Empty, accessibility, deprecated, description, usage, properties, baseModel, derivedModels, discriminatorValue, discriminatorProperty, discriminatedSubtypes, additionalProperties) + { + Decorators = decorators + }; + + if (id is not null) + { + resolver.AddReference(id, model); + resolver.AddReference($"{model.Name}.{nameof(InputModelType.DerivedModels)}", derivedModels); + } + + if (baseModel is not null) + { + var baseModelDerived = (List)resolver.ResolveReference($"{baseModel.Name}.{nameof(InputModelType.DerivedModels)}"); + baseModelDerived.Add(model); + } + + return model; + } + + private static void CreateProperties(ref Utf8JsonReader reader, ICollection properties, JsonSerializerOptions options, string modelName, bool isMultipartType) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException($"Invalid JSON format. 'properties' property of '{modelName}' should be an array."); + } + reader.Read(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + var rawProperty = reader.ReadWithConverter(options); + if (rawProperty == null) + { + throw new JsonException($"Property of model '{modelName}' cannot be null."); + } + + if (rawProperty.IsFlattened) + { + var flattenedProperties = FlattenProperty(rawProperty); + + foreach (var property in flattenedProperties) + { + properties.Add(property); + } + } + else + { + properties.Add(isMultipartType ? ConvertMultipartProperty(rawProperty) : rawProperty); + } + + } + reader.Read(); + } + + /// + /// Flatten the property of which the is true. + /// + /// model property type passed in by emitter to be flattened. + /// One or more instances. + private static IEnumerable FlattenProperty(InputModelProperty propertyToFlatten) + { + if (propertyToFlatten.Type is InputModelType model) + { + foreach (var p in model.Properties) + { + var newFlattenedNames = new List() { propertyToFlatten.Name }; + if (p.FlattenedNames != null) + { + newFlattenedNames.AddRange(p.FlattenedNames); + } + else + { + newFlattenedNames.Add(p.Name); + } + yield return p with { FlattenedNames = newFlattenedNames, IsRequired = (propertyToFlatten.IsRequired ? p.IsRequired : false) }; + } + } + else + { + throw new JsonException($"Flattened property '{propertyToFlatten.Name}' must be a model type."); + } + } + + private static void CreateDiscriminatedSubtypes(ref Utf8JsonReader reader, IDictionary discriminatedSubtypes, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + reader.Read(); + + while (reader.TokenType != JsonTokenType.EndObject) + { + var discriminatorValue = reader.GetString() ?? throw new JsonException("Discriminator value cannot be null"); + reader.Read(); + if (reader.TokenType == JsonTokenType.StartObject) + { + var subtype = reader.ReadWithConverter(options) ?? throw new JsonException("Discriminated Subtype cannot be null"); + discriminatedSubtypes.Add(discriminatorValue, subtype); + } + else + { + reader.Read(); + } + } + + reader.Read(); + } + + /* TODO: in Multipart body model, if the property is of type Bytes, it should be converted to Stream + * In future, we will convert this in emitter when we adopt tcgc. + */ + private static InputModelProperty ConvertMultipartProperty(InputModelProperty property) + => new InputModelProperty( + property.Name, + property.SerializedName, + property.Description, + ConvertPropertyType(property.Type), + property.ConstantValue, + property.IsRequired, + property.IsReadOnly, + property.IsDiscriminator) + { + DefaultValue = property.DefaultValue, + Decorators = property.Decorators, + FlattenedNames = property.FlattenedNames + }; + + private static InputType ConvertPropertyType(InputType propertyType) + { + return propertyType switch + { + InputPrimitiveType { Kind: InputPrimitiveTypeKind.Bytes } => InputPrimitiveType.Stream, + InputListType listType => new InputListType(listType.Name, listType.CrossLanguageDefinitionId, ConvertPropertyType(listType.ValueType)) + { + Decorators = listType.Decorators + }, + InputDictionaryType dictionaryType => new InputDictionaryType(dictionaryType.Name, dictionaryType.KeyType, ConvertPropertyType(dictionaryType.ValueType)) + { + Decorators = dictionaryType.Decorators + }, + InputNullableType nullableType => new InputNullableType(ConvertPropertyType(nullableType.Type)) + { + Decorators = nullableType.Decorators + }, + _ => propertyType + }; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputNullableTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputNullableTypeConverter.cs new file mode 100644 index 0000000..d5963f9 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputNullableTypeConverter.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input.InputTypes.Serialization +{ + internal class TypeSpecInputNullableTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputNullableTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputNullableType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateNullableType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputNullableType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputNullableType CreateNullableType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && name == null; + InputType? valueType = null; + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadWithConverter("type", options, ref valueType) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + valueType = valueType ?? throw new JsonException("InputNullableType must have value type"); + + var nullableType = new InputNullableType(valueType) + { + Decorators = decorators ?? [] + }; + if (id != null) + { + resolver.AddReference(id, nullableType); + } + return nullableType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputOperationConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputOperationConverter.cs new file mode 100644 index 0000000..4de539e --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputOperationConverter.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Input.Examples; +using Azure.Core; +using Humanizer; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputOperationConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputOperationConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateOperation(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputOperation value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputOperation CreateOperation(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && name == null; + string? resourceName = null; + string? summary = null; + string? deprecated = null; + string? accessibility = null; + string? description = null; + IReadOnlyList? parameters = null; + IReadOnlyList? responses = null; + RequestMethod httpMethod = default; + BodyMediaType requestBodyMediaType = default; + string? uri = null; + string? path = null; + string? externalDocsUri = null; + IReadOnlyList? requestMediaTypes = null; + bool bufferResponse = false; + OperationLongRunning? longRunning= null; + OperationPaging? paging = null; + bool generateProtocolMethod = false; + bool generateConvenienceMethod = false; + string? crossLanguageDefinitionId = null; + bool keepClientDefaultValue = false; + IReadOnlyList? examples = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString(nameof(InputOperation.Name), ref name) + || reader.TryReadString(nameof(InputOperation.ResourceName), ref resourceName) + || reader.TryReadString(nameof(InputOperation.Summary), ref summary) + || reader.TryReadString(nameof(InputOperation.Deprecated), ref deprecated) + || reader.TryReadString(nameof(InputOperation.Description), ref description) + || reader.TryReadString(nameof(InputOperation.Accessibility), ref accessibility) + || reader.TryReadWithConverter(nameof(InputOperation.Parameters), options, ref parameters) + || reader.TryReadWithConverter(nameof(InputOperation.Responses), options, ref responses) + || reader.TryReadWithConverter(nameof(InputOperation.HttpMethod), options, ref httpMethod) + || reader.TryReadWithConverter(nameof(InputOperation.RequestBodyMediaType), options, ref requestBodyMediaType) + || reader.TryReadString(nameof(InputOperation.Uri), ref uri) + || reader.TryReadString(nameof(InputOperation.Path), ref path) + || reader.TryReadString(nameof(InputOperation.ExternalDocsUrl), ref externalDocsUri) + || reader.TryReadWithConverter(nameof(InputOperation.RequestMediaTypes), options, ref requestMediaTypes) + || reader.TryReadBoolean(nameof(InputOperation.BufferResponse), ref bufferResponse) + || reader.TryReadWithConverter(nameof(InputOperation.LongRunning), options, ref longRunning) + || reader.TryReadWithConverter(nameof(InputOperation.Paging), options, ref paging) + || reader.TryReadBoolean(nameof(InputOperation.GenerateProtocolMethod), ref generateProtocolMethod) + || reader.TryReadBoolean(nameof(InputOperation.GenerateConvenienceMethod), ref generateConvenienceMethod) + || reader.TryReadString(nameof(InputOperation.CrossLanguageDefinitionId), ref crossLanguageDefinitionId) + || reader.TryReadBoolean(nameof(InputOperation.KeepClientDefaultValue), ref keepClientDefaultValue) + || reader.TryReadWithConverter(nameof(InputOperation.Examples), options, ref examples); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("Enum must have name"); + if (string.IsNullOrEmpty(description)) + { + Console.Error.WriteLine($"[Warn]: InputOperation '{name}' does not have a description"); + description = $"{name.Humanize()}."; + } + crossLanguageDefinitionId = crossLanguageDefinitionId ?? throw new JsonException("InputOperation must have crossLanguageDefinitionId"); + uri = uri ?? throw new JsonException("InputOperation must have uri"); + path = path ?? throw new JsonException("InputOperation must have path"); + parameters = parameters ?? throw new JsonException("InputOperation must have parameters"); + responses = responses ?? throw new JsonException("InputOperation must have responses"); + + var inputOperation = new InputOperation(name, resourceName, summary, deprecated, description, accessibility, parameters, responses, httpMethod, requestBodyMediaType, uri, path, externalDocsUri, requestMediaTypes, bufferResponse, longRunning, paging, generateProtocolMethod, generateConvenienceMethod, crossLanguageDefinitionId, keepClientDefaultValue, examples) + { + IsNameChanged = IsNameChanged(crossLanguageDefinitionId, name) + }; + if (id != null) + { + resolver.AddReference(id, inputOperation); + } + return inputOperation; + + static bool IsNameChanged(string crossLanguageDefinitionId, string name) + { + int lastDotIndex = crossLanguageDefinitionId.LastIndexOf('.'); + if (lastDotIndex < 0) + { + throw new JsonException($"The crossLanguageDefinitionId of {crossLanguageDefinitionId} does not contain a dot in it, this should be impossible to happen"); + } + var span = crossLanguageDefinitionId.AsSpan(lastDotIndex + 1); + var nameSpan = name.AsSpan(); + if (nameSpan.Equals(span, StringComparison.Ordinal)) + { + return false; + } + + return true; + } + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputParameterConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputParameterConverter.cs new file mode 100644 index 0000000..cad323b --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputParameterConverter.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputParameterConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputParameterConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputParameter? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => ReadInputParameter(ref reader, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputParameter value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private static InputParameter? ReadInputParameter(ref Utf8JsonReader reader, JsonSerializerOptions options, ReferenceResolver resolver) + => reader.ReadReferenceAndResolve(resolver) ?? CreateInputParameter(ref reader, null, null, options, resolver); + + public static InputParameter CreateInputParameter(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && name == null; + + string? nameInRequest = null; + string? description = null; + InputType? parameterType = null; + string? location = null; + InputConstant? defaultValue = null; + InputParameter? groupBy = null; + string? kind = null; + bool isRequired = false; + bool isApiVersion = false; + bool isResourceParameter = false; + bool isContentType = false; + bool isEndpoint = false; + bool skipUrlEncoding = false; + bool explode = false; + string? arraySerializationDelimiter = null; + string? headerCollectionPrefix = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString(nameof(InputParameter.Name), ref name) + || reader.TryReadString(nameof(InputParameter.NameInRequest), ref nameInRequest) + || reader.TryReadString(nameof(InputParameter.Description), ref description) + || reader.TryReadWithConverter(nameof(InputParameter.Type), options, ref parameterType) + || reader.TryReadString(nameof(InputParameter.Location), ref location) + || reader.TryReadWithConverter(nameof(InputParameter.DefaultValue), options, ref defaultValue) + || reader.TryReadWithConverter(nameof(InputParameter.GroupedBy), options, ref groupBy) + || reader.TryReadString(nameof(InputParameter.Kind), ref kind) + || reader.TryReadBoolean(nameof(InputParameter.IsRequired), ref isRequired) + || reader.TryReadBoolean(nameof(InputParameter.IsApiVersion), ref isApiVersion) + || reader.TryReadBoolean(nameof(InputParameter.IsResourceParameter), ref isResourceParameter) + || reader.TryReadBoolean(nameof(InputParameter.IsContentType), ref isContentType) + || reader.TryReadBoolean(nameof(InputParameter.IsEndpoint), ref isEndpoint) + || reader.TryReadBoolean(nameof(InputParameter.SkipUrlEncoding), ref skipUrlEncoding) + || reader.TryReadBoolean(nameof(InputParameter.Explode), ref explode) + || reader.TryReadString(nameof(InputParameter.ArraySerializationDelimiter), ref arraySerializationDelimiter) + || reader.TryReadString(nameof(InputParameter.HeaderCollectionPrefix), ref headerCollectionPrefix); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException("Parameter must have name"); + nameInRequest = nameInRequest ?? throw new JsonException("Parameter must have nameInRequest"); + parameterType = parameterType ?? throw new JsonException("Parameter must have type"); + + if (location == null) + { + throw new JsonException("Parameter must have location"); + } + Enum.TryParse(location, ignoreCase: true, out var requestLocation); + + if (kind == null) + { + throw new JsonException("Parameter must have kind"); + } + Enum.TryParse(kind, ignoreCase: true, out var parameterKind); + + var parameter = new InputParameter( + Name: name, + NameInRequest: nameInRequest, + Description: description, + Type: parameterType, + Location: requestLocation, + DefaultValue: defaultValue, + GroupedBy: groupBy, + FlattenedBodyProperty: null, + Kind: parameterKind, + IsRequired: isRequired, + IsApiVersion: isApiVersion, + IsResourceParameter: isResourceParameter, + IsContentType: isContentType, + IsEndpoint: isEndpoint, + SkipUrlEncoding: skipUrlEncoding, + Explode: explode, + ArraySerializationDelimiter: arraySerializationDelimiter, + HeaderCollectionPrefix: headerCollectionPrefix); + + if (id != null) + { + resolver.AddReference(id, parameter); + } + + return parameter; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputPrimitiveTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputPrimitiveTypeConverter.cs new file mode 100644 index 0000000..7bf5b2b --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputPrimitiveTypeConverter.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputPrimitiveTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputPrimitiveTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputPrimitiveType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreatePrimitiveType(ref reader, null, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputPrimitiveType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputPrimitiveType CreatePrimitiveType(ref Utf8JsonReader reader, string? id, string? kind, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null && kind == null && name == null; + string? crossLanguageDefinitionId = null; + string? encode = null; + InputPrimitiveType? baseType = null; + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("kind", ref kind) + || reader.TryReadString("name", ref name) + || reader.TryReadString("crossLanguageDefinitionId", ref crossLanguageDefinitionId) + || reader.TryReadString("encode", ref encode) + || reader.TryReadWithConverter("baseType", options, ref baseType); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + kind = kind ?? throw new JsonException("Primitive types must have kind"); + name = name ?? string.Empty; + crossLanguageDefinitionId = crossLanguageDefinitionId ?? string.Empty; + + if (!Enum.TryParse(kind, true, out var primitiveTypeKind)) + { + throw new JsonException($"Unknown primitive type kind: {kind}"); + } + + var primitiveType = new InputPrimitiveType(primitiveTypeKind, name, crossLanguageDefinitionId, encode, baseType); + if (id != null) + { + resolver.AddReference(id, primitiveType); + } + return primitiveType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputTypeConverter.cs new file mode 100644 index 0000000..484c591 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputTypeConverter.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Input.InputTypes.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal sealed class TypeSpecInputTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + + public TypeSpecInputTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputType(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, InputType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + private InputType CreateInputType(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + string? id = null; + string? kind = null; + string? name = null; + InputType? result = null; + var isFirstProperty = true; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isIdOrNameOrKind = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("kind", ref kind) + || reader.TryReadString("name", ref name); + + if (isIdOrNameOrKind) + { + continue; + } + result = CreateDerivedType(ref reader, id, kind, name, options); + } + + return result ?? CreateDerivedType(ref reader, id, kind, name, options); + } + + private const string LiteralKind = "constant"; + private const string UnionKind = "union"; + private const string ModelKind = "model"; + private const string EnumKind = "enum"; + private const string ArrayKind = "array"; + private const string DictionaryKind = "dict"; + private const string NullableKind = "nullable"; + private const string UtcDateTimeKind = "utcDateTime"; + private const string OffsetDateTimeKind = "offsetDateTime"; + private const string DurationKind = "duration"; + + private InputType CreateDerivedType(ref Utf8JsonReader reader, string? id, string? kind, string? name, JsonSerializerOptions options) => kind switch + { + null => throw new JsonException($"InputType (id: '{id}', name: '{name}') must have a 'Kind' property"), + LiteralKind => TypeSpecInputLiteralTypeConverter.CreateInputLiteralType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + UnionKind => TypeSpecInputUnionTypeConverter.CreateInputUnionType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + ModelKind => TypeSpecInputModelTypeConverter.CreateModelType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + EnumKind => TypeSpecInputEnumTypeConverter.CreateEnumType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + ArrayKind => TypeSpecInputListTypeConverter.CreateListType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + DictionaryKind => TypeSpecInputDictionaryTypeConverter.CreateDictionaryType(ref reader, id, options, _referenceHandler.CurrentResolver), + NullableKind => TypeSpecInputNullableTypeConverter.CreateNullableType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + UtcDateTimeKind or OffsetDateTimeKind => TypeSpecInputDateTimeTypeConverter.CreateDateTimeType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + DurationKind => TypeSpecInputDurationTypeConverter.CreateDurationType(ref reader, id, name, options, _referenceHandler.CurrentResolver), + _ => TypeSpecInputPrimitiveTypeConverter.CreatePrimitiveType(ref reader, id, kind, name, options, _referenceHandler.CurrentResolver), + }; + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs new file mode 100644 index 0000000..9e8ffa9 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal class TypeSpecInputUnionTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputUnionTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputUnionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateInputUnionType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputUnionType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputUnionType CreateInputUnionType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + var variantTypes = new List(); + IReadOnlyList? decorators = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadString("name", ref name) + || reader.TryReadWithConverter("variantTypes", options, ref variantTypes) + || reader.TryReadWithConverter("decorators", options, ref decorators); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + name = name ?? throw new JsonException($"{nameof(InputUnionType)} must have a name."); + if (variantTypes == null || variantTypes.Count == 0) + { + throw new JsonException("Union must have variant types."); + } + + var unionType = new InputUnionType(name, variantTypes) { Decorators = decorators ?? Array.Empty() }; + if (id != null) + { + resolver.AddReference(id, unionType); + } + return unionType; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecReferenceHandler.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecReferenceHandler.cs new file mode 100644 index 0000000..e06264e --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecReferenceHandler.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + /// + /// Custom reference handler that preserves the same instance of the resolver across multiple calls of the converters Read method + /// Required for the reference preservation to work with custom converters + /// + internal sealed class TypeSpecReferenceHandler : ReferenceHandler + { + public ReferenceResolver CurrentResolver { get; } = new TypeSpecReferenceResolver(); + + public override ReferenceResolver CreateResolver() => CurrentResolver; + + private class TypeSpecReferenceResolver : ReferenceResolver + { + private readonly Dictionary _referenceIdToObjectMap = new(); + + public override void AddReference(string referenceId, object value) + { + if (!_referenceIdToObjectMap.TryAdd(referenceId, value)) + { + throw new JsonException($"Failed to add reference ID '{referenceId}' with value type '{value.GetType()}'"); + } + } + + public override string GetReference(object value, out bool alreadyExists) + => throw new InvalidOperationException("JSON writing isn't supported"); + + public override object ResolveReference(string referenceId) + => _referenceIdToObjectMap.TryGetValue(referenceId, out object? value) ? value : throw new JsonException($"Cannot resolve reference {referenceId}"); + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecSerialization.cs b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecSerialization.cs new file mode 100644 index 0000000..96ea68d --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Serialization/TypeSpecSerialization.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Input.Examples; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Input +{ + internal static class TypeSpecSerialization + { + public static InputNamespace? Deserialize(string json) + { + var referenceHandler = new TypeSpecReferenceHandler(); + var options = new JsonSerializerOptions + { + ReferenceHandler = referenceHandler, + AllowTrailingCommas = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + new RequestMethodConverter(), + new TypeSpecInputTypeConverter(referenceHandler), + new TypeSpecInputListTypeConverter(referenceHandler), + new TypeSpecInputDictionaryTypeConverter(referenceHandler), + new TypeSpecInputEnumTypeConverter(referenceHandler), + new TypeSpecInputEnumTypeValueConverter(referenceHandler), + new TypeSpecInputModelTypeConverter(referenceHandler), + new TypeSpecInputModelPropertyConverter(referenceHandler), + new TypeSpecInputConstantConverter(referenceHandler), + new TypeSpecInputLiteralTypeConverter(referenceHandler), + new TypeSpecInputUnionTypeConverter(referenceHandler), + new TypeSpecInputParameterConverter(referenceHandler), + new TypeSpecInputOperationConverter(referenceHandler), + new TypeSpecInputClientConverter(referenceHandler), + new TypeSpecInputDateTimeTypeConverter(referenceHandler), + new TypeSpecInputDurationTypeConverter(referenceHandler), + new TypeSpecInputPrimitiveTypeConverter(referenceHandler), + new TypeSpecInputDecoratorInfoConverter(referenceHandler), + new TypeSpecInputExampleValueConverter(referenceHandler), + new TypeSpecInputOperationExampleConverter(referenceHandler), + new TypeSpecInputParameterExampleConverter(referenceHandler), + } + }; + + return JsonSerializer.Deserialize(json, options); + } + + private class RequestMethodConverter : JsonConverter + { + public override RequestMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var stringValue = reader.GetString(); + return stringValue != null ? RequestMethod.Parse(stringValue) : RequestMethod.Get; + } + + public override void Write(Utf8JsonWriter writer, RequestMethod value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Method.AsSpan()); + } + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputAcronymTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputAcronymTransformer.cs new file mode 100644 index 0000000..74d48be --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputAcronymTransformer.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Mgmt.Models; +using System.Collections.Generic; +using System; + +namespace AutoRest.CSharp.Common.Input +{ + internal class InputAcronymTransformer + { + public static void Transform(InputNamespace input) + { + UpdateAcronyms(input.Models); + UpdateAcronyms(input.Enums); + // transform all the parameter names + UpdateAcronyms(input.Clients); + } + + + private static void UpdateAcronyms(IEnumerable allModels) + { + foreach (var model in allModels) + { + TransformInputType(model); + } + } + + private static void UpdateAcronyms(IEnumerable allEnums) + { + foreach (var inputEnum in allEnums) + { + TransformInputType(inputEnum); + } + } + + private static void UpdateAcronyms(IEnumerable clients) + { + foreach (var client in clients) + { + foreach (var operation in client.Operations) + { + TransformOperation(operation); + } + } + } + + private static void TransformOperation(InputOperation inputOperation) + { + var originalName = inputOperation.Name; + var result = NameTransformer.Instance.EnsureNameCase(originalName); + inputOperation.Name = result.Name; + + // this iteration only applies to path and query parameter (maybe headers?) but not to body parameter + foreach (var parameter in inputOperation.Parameters) + { + TransformInputParameter(parameter); + } + } + + private static void TransformInputType(InputType inputType) + { + switch (inputType) + { + case InputEnumType inputEnum: + TransformInputEnumType(inputEnum, inputEnum.Values); + break; + case InputModelType inputModel: + TransformInputModel(inputModel); + break; + default: + throw new InvalidOperationException($"Unknown input type {inputType.GetType()}"); + } + } + + private static void TransformInputEnumType(InputEnumType inputEnum, IReadOnlyList choiceValues) + { + TransformInputTypeName(inputEnum); + TransformChoices(inputEnum, choiceValues); + } + + private static void TransformChoices(InputEnumType inputEnum, IReadOnlyList choiceValues) + { + foreach (var choiceValue in choiceValues) + { + var originalName = choiceValue.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName); + choiceValue.Name = result.Name; + } + } + + private static void TransformInputTypeName(InputType inputType) + { + var originalName = inputType.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName); + inputType.Name = result.Name; + } + + private static void TransformInputParameter(InputParameter inputParameter) + { + var originalName = inputParameter.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName); + inputParameter.Name = result.Name; + } + + private static void TransformInputModel(InputModelType inputModel) + { + TransformInputTypeName(inputModel); + foreach (var property in inputModel.Properties) + { + TransformInputTypeName(property.Type); + TransformInputModelProperty(property); + } + } + + private static void TransformInputModelProperty(InputModelProperty property) + { + var originalName = property.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName); + property.Name = result.Name; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputClientTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputClientTransformer.cs new file mode 100644 index 0000000..ce936dd --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputClientTransformer.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AutoRest.CSharp.Common.Input +{ + internal class InputClientTransformer + { + public static void Transform(InputNamespace input) + { + foreach (var client in input.Clients) + { + RemoveInternalOperationFromInputClient(client); + UpdateSubscriptionId(client); + } + } + + private static void RemoveInternalOperationFromInputClient(InputClient inputClient) + { + var operationsToKeep = new List(); + foreach (var operation in inputClient.Operations) + { + // operations_list has been covered in Azure.ResourceManager already, we don't need to generate it in the client + if (operation.CrossLanguageDefinitionId != "Azure.ResourceManager.Operations.list") + { + operationsToKeep.Add(operation); + } + } + inputClient.Operations = operationsToKeep; + } + + private static void UpdateSubscriptionId(InputClient inputClient) + { + foreach (var operation in inputClient.Operations) + { + var subscriptionIdParameter = operation.Parameters.FirstOrDefault(p => p.NameInRequest.Equals("subscriptionId", StringComparison.OrdinalIgnoreCase)); + if (subscriptionIdParameter != null) + { + subscriptionIdParameter.Kind = InputOperationParameterKind.Method; + subscriptionIdParameter.Type = InputPrimitiveType.String; + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputCommonSingleWordModelTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputCommonSingleWordModelTransformer.cs new file mode 100644 index 0000000..3498213 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputCommonSingleWordModelTransformer.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AutoRest.CSharp.Mgmt.AutoRest; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Common.Input +{ + internal class InputCommonSingleWordModelTransformer + { + private static readonly HashSet _schemasToChange = new HashSet() + { + "Sku", + "SkuName", + "SkuTier", + "SkuFamily", + "SkuInformation", + "Plan", + "Usage", + "Resource", + "Kind", + // Private endpoint definitions which are defined in swagger common-types/privatelinks.json and are used by RPs + "PrivateEndpointConnection", + "PrivateLinkResource", + "PrivateLinkServiceConnectionState", + "PrivateEndpointServiceConnectionStatus", + "PrivateEndpointConnectionProvisioningState", + // not defined in common-types, but common in various RP + "PrivateLinkResourceProperties", + "PrivateLinkServiceConnectionStateProperty", + // internal, but could be public in the future, also make the names more consistent + "PrivateEndpointConnectionListResult", + "PrivateLinkResourceListResult", + "Severity" + }; + + public static void Transform(InputNamespace inputNamespace) + { + foreach (var schemaName in Configuration.MgmtConfiguration.PrependRPPrefix) + { + _schemasToChange.Add(schemaName); + } + foreach (var model in inputNamespace.Models) + { + if (TransformName(model.Name, out var newName)) + { + model.Name = newName; + } + } + + foreach (var inputEnum in inputNamespace.Enums) + { + if (TransformName(inputEnum.Name, out var newName)) + { + inputEnum.Name = newName; + } + } + } + + private static bool TransformName(string name, [NotNullWhen(true)]out string? newName) + { + newName = null; + if (!_schemasToChange.Contains(name)) + { + return false; + } + + string prefix = Configuration.Namespace.Equals(typeof(ArmClient).Namespace) ? "Arm" : MgmtContext.RPName; + string suffix = name.EndsWith("Resource") ? "Data" : string.Empty; + newName = $"{prefix}{name}{suffix}"; + return true; + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputNamespaceTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputNamespaceTransformer.cs new file mode 100644 index 0000000..3343ceb --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputNamespaceTransformer.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Input +{ + internal class InputNamespaceTransformer + { + public static void Transform(InputNamespace input) + { + // TODO: Remove this when we have a better way to remove operations, tracking in https://github.com/Azure/typespec-azure/issues/964 + InputClientTransformer.Transform(input); + InputCommonSingleWordModelTransformer.Transform(input); + InputAcronymTransformer.Transform(input); + InputUrlToUriTransformer.Transform(input); + InputRenameTimeToOnTransformer.Transform(input); + InputPluralEnumTransformer.Transform(input); + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputPluralEnumTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputPluralEnumTransformer.cs new file mode 100644 index 0000000..4e0adbe --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputPluralEnumTransformer.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Utilities; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Input; + +internal class InputPluralEnumTransformer +{ + public static void Transform(InputNamespace inputNamespace) + { + // TODO: Add items to skip this transformation + HashSet enumsToKeepPlural = new HashSet() { }; + + foreach (var inputEnum in inputNamespace.Enums) + { + if (enumsToKeepPlural.Contains(inputEnum.Name)) + { + continue; + } + inputEnum.Name = inputEnum.Name.LastWordToSingular(inputIsKnownToBePlural: false); + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputRenameTimeToOnTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputRenameTimeToOnTransformer.cs new file mode 100644 index 0000000..75a10a2 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputRenameTimeToOnTransformer.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Common.Input +{ + internal class InputRenameTimeToOnTransformer + { + private const string _onSuffix = "On"; + private static readonly Dictionary _nounToVerbMapping = new() + { + {"Creation", "Created"}, + {"Deletion", "Deleted"}, + {"Expiration", "Expire"}, + {"Modification", "Modified"}, + }; + + private static string[] _wordsToSkip = new string[] + { + "From", + "To", + "PointInTime", + }; + + private static string[] _wordsToCut = new string[] + { + "DateTime", + "Time", + "Date", + "At", + }; + + public static void Transform(InputNamespace inputNamespace) + { + foreach (var model in inputNamespace.Models) + { + if (model is not InputModelType inputModel) + continue; + + foreach (var property in inputModel.Properties) + { + var propertyType = property.Type.GetImplementType(); + if ((propertyType is InputPrimitiveType inputPrimitiveType && inputPrimitiveType.Kind == InputPrimitiveTypeKind.PlainDate) || propertyType is InputDateTimeType) + { + var propName = property.CSharpName().AsSpan(); + + foreach (var word in _wordsToSkip) + { + if (propName.StartsWith(word, StringComparison.Ordinal)) + { + continue; + } + } + + var lengthToCut = 0; + foreach (var word in _wordsToCut) + { + if (propName.EndsWith(word, StringComparison.Ordinal)) + { + lengthToCut = word.Length; + break; + } + } + + if (lengthToCut > 0 && lengthToCut < propName.Length) + { + var prefix = propName.Slice(0, propName.Length - lengthToCut).ToString(); + var newName = (_nounToVerbMapping.TryGetValue(prefix, out var verb) ? verb : prefix) + _onSuffix; + property.Name = newName; + } + } + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputUrlToUriTransformer.cs b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputUrlToUriTransformer.cs new file mode 100644 index 0000000..eebbf04 --- /dev/null +++ b/logger/autorest.csharp/common/Input/InputTypes/Transformation/InputUrlToUriTransformer.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AutoRest.CSharp.Common.Input +{ + internal static class InputUrlToUriTransformer + { + public static void Transform(InputNamespace input) + { + foreach (var model in input.Models) + { + if (model is not InputModelType inputModel) + continue; + + if (TryTransformUrlToUri(model.Name, out var newModelName)) + { + model.Name = newModelName; + } + + foreach (var property in inputModel.Properties) + { + if (TryTransformUrlToUri(property.Name, out var newPropertyName)) + { + property.Name = newPropertyName; + } + } + } + } + + internal static bool TryTransformUrlToUri(string name, [MaybeNullWhen(false)] out string newName) + { + const char i = 'i'; + const string UrlSuffix = "Url"; + newName = null; + if (name.Length < 3) + { + return false; + } + + var span = name.AsSpan(); + // check if this ends with `Url` + if (span.EndsWith(UrlSuffix.AsSpan(), StringComparison.Ordinal)) + { + Span newSpan = span.ToArray(); + newSpan[^1] = i; + + newName = new string(newSpan); + return true; + } + + return false; + } + } +} diff --git a/logger/autorest.csharp/common/Input/SchemaUsageProvider.cs b/logger/autorest.csharp/common/Input/SchemaUsageProvider.cs new file mode 100644 index 0000000..fb34735 --- /dev/null +++ b/logger/autorest.csharp/common/Input/SchemaUsageProvider.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Input +{ + internal class SchemaUsageProvider + { + private readonly CodeModel _codeModel; + private readonly Lazy> _usages; + + public SchemaUsageProvider(CodeModel codeModel) + { + _codeModel = codeModel; + _usages = new Lazy>(EnsureUsages); + } + + private Dictionary EnsureUsages() + { + var usages = new Dictionary(); + foreach (var objectSchema in _codeModel.Schemas.Objects) + { + var usage = objectSchema?.Extensions?.Usage; + + if (usage != null) + { + var schemaTypeUsage = (SchemaTypeUsage)Enum.Parse(typeof(SchemaTypeUsage), usage, true); + + if (schemaTypeUsage.HasFlag(SchemaTypeUsage.Converter)) + { + Apply(usages, objectSchema, SchemaTypeUsage.Converter, false); + schemaTypeUsage &= ~SchemaTypeUsage.Converter; + } + + Apply(usages, objectSchema, schemaTypeUsage, true); + } + } + foreach (var operationGroup in _codeModel.OperationGroups) + { + foreach (var operation in operationGroup.Operations) + { + foreach (var operationResponse in operation.Responses) + { + var paging = operation.Language.Default.Paging; + if (paging != null && operationResponse.ResponseSchema is ObjectSchema objectSchema) + { + Apply(usages, operationResponse.ResponseSchema, SchemaTypeUsage.Output); + foreach (var property in objectSchema.Properties) + { + var itemName = paging.ItemName ?? "value"; + if (property.SerializedName == itemName) + { + Apply(usages, property.Schema, SchemaTypeUsage.Model | SchemaTypeUsage.Output); + } + } + } + else + { + Apply(usages, operationResponse.ResponseSchema, SchemaTypeUsage.Model | SchemaTypeUsage.Output); + } + } + + foreach (var operationResponse in operation.Exceptions) + { + Apply(usages, operationResponse.ResponseSchema, SchemaTypeUsage.Error | SchemaTypeUsage.Output); + } + + foreach (var parameter in operation.Parameters) + { + ApplyParameterSchema(usages, parameter); + } + + foreach (var serviceRequest in operation.Requests) + { + foreach (var parameter in serviceRequest.Parameters) + { + ApplyParameterSchema(usages, parameter); + } + } + } + } + + return usages; + } + + private void ApplyParameterSchema(Dictionary usages, RequestParameter parameter) + { + if (parameter.Flattened == true) + { + Apply(usages, parameter.Schema, SchemaTypeUsage.FlattenedParameters | SchemaTypeUsage.Input, recurse: false); + } + else + { + Apply(usages, parameter.Schema, SchemaTypeUsage.Model | SchemaTypeUsage.Input); + } + } + + private void Apply(Dictionary usages, Schema? schema, SchemaTypeUsage usage, bool recurse = true) + { + if (schema == null) + { + return; + } + + usages.TryGetValue(schema, out var currentUsage); + + var newUsage = currentUsage | usage; + if (newUsage == currentUsage) + { + return; + } + + usages[schema] = newUsage; + + if (!recurse) + { + return; + } + + if (schema is ObjectSchema objectSchema) + { + foreach (var parent in objectSchema.Parents!.All) + { + Apply(usages, parent, usage); + } + + foreach (var child in objectSchema.Children!.All) + { + Apply(usages, child, usage); + } + + foreach (var schemaProperty in objectSchema.Properties) + { + var propertyUsage = usage; + + if (schemaProperty.IsReadOnly) + { + propertyUsage &= ~SchemaTypeUsage.Input; + } + + Apply(usages, schemaProperty.Schema, propertyUsage); + } + } + else if (schema is DictionarySchema dictionarySchema) + { + Apply(usages, dictionarySchema.ElementType, usage); + } + else if (schema is ArraySchema arraySchema) + { + Apply(usages, arraySchema.ElementType, usage); + } + else if (schema is ConstantSchema constantSchmea) + { + // the value type of a ConstantSchema might be an choice (transformed in class AutoRest.CSharp.Mgmt.Decorator.Transformer.ConstantSchemaTransformer + Apply(usages, constantSchmea.ValueType, usage); + } + } + + public SchemaTypeUsage GetUsage(Schema schema) + { + _usages.Value.TryGetValue(schema, out var usage); + return usage; + } + } + + [Flags] + internal enum SchemaTypeUsage + { + None = 0, + Input = 1 << 1, + Output = Input << 1, + RoundTrip = Input | Output, + Model = Output << 1, + Error = Model << 1, + FlattenedParameters = Error << 1, + Converter = FlattenedParameters << 1 + } +} diff --git a/logger/autorest.csharp/common/Input/Source/ClientSourceInput.cs b/logger/autorest.csharp/common/Input/Source/ClientSourceInput.cs new file mode 100644 index 0000000..45ca5ac --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/ClientSourceInput.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Input.Source +{ + internal record ClientSourceInput(INamedTypeSymbol? ParentClientType); +} diff --git a/logger/autorest.csharp/common/Input/Source/CodeGenAttributes.cs b/logger/autorest.csharp/common/Input/Source/CodeGenAttributes.cs new file mode 100644 index 0000000..87b9003 --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/CodeGenAttributes.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Azure.Core; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Input.Source +{ + public class CodeGenAttributes + { + public const string CodeGenSuppressAttributeName = "CodeGenSuppressAttribute"; + + public const string CodeGenMemberAttributeName = "CodeGenMemberAttribute"; + + public const string CodeGenTypeAttributeName = "CodeGenTypeAttribute"; + + public const string CodeGenModelAttributeName = "CodeGenModelAttribute"; + + public const string CodeGenClientAttributeName = "CodeGenClientAttribute"; + + public const string CodeGenSerializationAttributeName = "CodeGenSerializationAttribute"; + + public bool TryGetCodeGenMemberAttributeValue(AttributeData attributeData, [MaybeNullWhen(false)] out string name) + { + name = null; + if (attributeData.AttributeClass?.Name != CodeGenMemberAttributeName) + return false; + + name = attributeData.ConstructorArguments.FirstOrDefault().Value as string; + return name != null; + } + + public bool TryGetCodeGenSerializationAttributeValue(AttributeData attributeData, [MaybeNullWhen(false)] out string propertyName, out IReadOnlyList? serializationNames, out string? serializationHook, out string? deserializationHook, out string? bicepSerializationHook) + { + propertyName = null; + serializationNames = null; + serializationHook = null; + deserializationHook = null; + bicepSerializationHook = null; + if (attributeData.AttributeClass?.Name != CodeGenSerializationAttributeName) + { + return false; + } + + var ctorArgs = attributeData.ConstructorArguments; + // this attribute could only at most have one constructor + propertyName = ctorArgs[0].Value as string; + + if (ctorArgs.Length > 1) + { + var namesArg = ctorArgs[1]; + serializationNames = namesArg.Kind switch + { + TypedConstantKind.Array => ToStringArray(namesArg.Values), + _ when namesArg.IsNull => null, + _ => new string[] { namesArg.Value?.ToString()! } + }; + } + + foreach (var (key, namedArgument) in attributeData.NamedArguments) + { + switch (key) + { + case nameof(CodeGenSerializationAttribute.SerializationPath): + serializationNames = ToStringArray(namedArgument.Values); + break; + case nameof(CodeGenSerializationAttribute.SerializationValueHook): + serializationHook = namedArgument.Value as string; + break; + case nameof(CodeGenSerializationAttribute.DeserializationValueHook): + deserializationHook = namedArgument.Value as string; + break; + case nameof(CodeGenSerializationAttribute.BicepSerializationValueHook): + bicepSerializationHook = namedArgument.Value as string; + break; + } + } + + return propertyName != null && (serializationNames != null || serializationHook != null || deserializationHook != null || bicepSerializationHook != null); + } + + public bool TryGetCodeGenModelAttributeValue(AttributeData attributeData, out string[]? usage, out string[]? formats) + { + usage = null; + formats = null; + if (attributeData.AttributeClass?.Name != CodeGenModelAttributeName) + return false; + foreach (var namedArgument in attributeData.NamedArguments) + { + switch (namedArgument.Key) + { + case nameof(CodeGenModelAttribute.Usage): + usage = ToStringArray(namedArgument.Value.Values); + break; + case nameof(CodeGenModelAttribute.Formats): + formats = ToStringArray(namedArgument.Value.Values); + break; + } + } + + return usage != null || formats != null; + } + + private static string[]? ToStringArray(ImmutableArray values) + { + if (values.IsDefaultOrEmpty) + { + return null; + } + + return values + .Select(v => (string?)v.Value) + .OfType() + .ToArray(); + } + } +} diff --git a/logger/autorest.csharp/common/Input/Source/CompilationInput.cs b/logger/autorest.csharp/common/Input/Source/CompilationInput.cs new file mode 100644 index 0000000..b4aab08 --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/CompilationInput.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Input.Source +{ + public abstract class CompilationInput + { + protected Compilation _compilation; + + public CompilationInput(Compilation compilation) + { + _compilation= compilation; + } + + internal abstract IMethodSymbol? FindMethod(string namespaceName, string typeName, string methodName, IEnumerable parameters); + } +} diff --git a/logger/autorest.csharp/common/Input/Source/ModelTypeMapping.cs b/logger/autorest.csharp/common/Input/Source/ModelTypeMapping.cs new file mode 100644 index 0000000..1b3a7af --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/ModelTypeMapping.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Input.Source +{ + public class ModelTypeMapping + { + private readonly Dictionary _propertyMappings; + private readonly Dictionary _codeGenMemberMappings; + private readonly Dictionary _typeSerializationMappings; + + public string[]? Usage { get; } + public string[]? Formats { get; } + public bool UseSystemTextJsonConverter { get; } + + public ModelTypeMapping(CodeGenAttributes codeGenAttributes, INamedTypeSymbol existingType) + { + _propertyMappings = new(); + _codeGenMemberMappings = new(); + _typeSerializationMappings = new(); + + foreach (ISymbol member in GetMembers(existingType)) + { + // If member is defined in both base and derived class, use derived one + if (ShouldIncludeMember(member) && !_propertyMappings.ContainsKey(member.Name)) + { + _propertyMappings[member.Name] = member; + } + + foreach (var attributeData in member.GetAttributes()) + { + // handle CodeGenMember attribute + if (codeGenAttributes.TryGetCodeGenMemberAttributeValue(attributeData, out var schemaMemberName)) + { + _codeGenMemberMappings[schemaMemberName] = member; + } + } + } + + foreach (var attributeData in existingType.GetAttributes()) + { + // handle CodeGenModel attribute + if (codeGenAttributes.TryGetCodeGenModelAttributeValue(attributeData, out var usage, out var formats)) + { + Usage = usage; + Formats = formats; + UseSystemTextJsonConverter = usage != null && usage.Contains(nameof(SchemaTypeUsage.Converter), StringComparer.OrdinalIgnoreCase); + } + + // handle CodeGenSerialization attribute + if (codeGenAttributes.TryGetCodeGenSerializationAttributeValue(attributeData, out var propertyName, out var serializationNames, out var serializationHook, out var deserializationHook, out var bicepSerializationHook) && !_typeSerializationMappings.ContainsKey(propertyName)) + { + _typeSerializationMappings.Add(propertyName, new(propertyName, serializationNames, serializationHook, deserializationHook, bicepSerializationHook)); + } + } + } + + private static bool ShouldIncludeMember(ISymbol member) + { + // here we exclude those "CompilerGenerated" members, such as the backing field of a property which is also a field. + return !member.IsImplicitlyDeclared && member is IPropertySymbol or IFieldSymbol; + } + + public ISymbol? GetMemberByOriginalName(string name) + => _codeGenMemberMappings.TryGetValue(name, out var renamedSymbol) ? + renamedSymbol : + _propertyMappings.TryGetValue(name, out var memberSymbol) ? memberSymbol : null; + + public SourcePropertySerializationMapping? GetForMemberSerialization(string name) + => _typeSerializationMappings.TryGetValue(name, out var serialization) ? serialization : null; + + public IEnumerable GetPropertiesWithSerialization() + { + // only the property with CodeGenSerialization attribute will be emitted into the serialization code. + foreach (var (propertyName, symbol) in _propertyMappings) + { + if (_typeSerializationMappings.ContainsKey(propertyName)) + yield return symbol; + } + } + + private static IEnumerable GetMembers(INamedTypeSymbol? typeSymbol) + { + while (typeSymbol != null) + { + foreach (var symbol in typeSymbol.GetMembers()) + { + yield return symbol; + } + + typeSymbol = typeSymbol.BaseType; + } + } + } +} diff --git a/logger/autorest.csharp/common/Input/Source/ProtocolCompilationInput.cs b/logger/autorest.csharp/common/Input/Source/ProtocolCompilationInput.cs new file mode 100644 index 0000000..8d91c3e --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/ProtocolCompilationInput.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Plugins; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Input.Source +{ + public class ProtocolCompilationInput : CompilationInput + { + private List? _methodSet; + private List MethodSet => _methodSet ??= EnsureMethodSet(); + + public static async Task TryCreate() + { + return Configuration.ExistingProjectFolder != null ? + new ProtocolCompilationInput(await GeneratedCodeWorkspace.CreateExistingCodeProject(Configuration.ExistingProjectFolder).GetCompilationAsync()) : null; + } + + private ProtocolCompilationInput(Compilation compilation) + : base(compilation) { } + + private protected List EnsureMethodSet() + { + var result = new List(); + foreach (IModuleSymbol module in _compilation.Assembly.Modules) + { + foreach (var type in SourceInputHelper.GetSymbols(module.GlobalNamespace)) + { + if (type is INamedTypeSymbol typeSymbol && IsClient(typeSymbol)) + { + foreach (var member in typeSymbol.GetMembers()) + { + if (member is IMethodSymbol methodSymbol && IsProtocolMethod(methodSymbol)) + { + result.Add(methodSymbol); + } + } + } + } + } + return result; + } + + internal override IMethodSymbol? FindMethod(string namespaceName, string typeName, string methodName, IEnumerable parameters) + { + var methods = MethodSet.Where(m => + m.ContainingNamespace.ToString() == namespaceName && + m.ContainingType.Name == typeName && + m.Name == methodName).ToArray(); + if (methods.Length == 0) + { + return null; + } + else if (methods.Length == 1) + { + return methods.First(); + } + else + { + foreach (var method in methods) + { + var existingParameters = method.Parameters; + var parametersCount = parameters.Count(); + if (existingParameters.Length - 1 != parametersCount) + { + continue; + } + + int index = 0; + foreach (var parameter in parameters) + { + if ((existingParameters[index].Type as INamedTypeSymbol)!.IsSameType(parameter)) + { + break; + } + ++index; + } + + if (index == parametersCount) + { + return method; + } + } + return null; + } + } + + private bool IsClient(INamedTypeSymbol type) => type.Name.EndsWith("Client"); + private bool IsProtocolMethod(IMethodSymbol method) => method.Parameters.Length > 0 && (method.Parameters.Last().Type as INamedTypeSymbol)!.IsSameType(KnownParameters.RequestContext.Type); + } +} diff --git a/logger/autorest.csharp/common/Input/Source/SourceInputHelper.cs b/logger/autorest.csharp/common/Input/Source/SourceInputHelper.cs new file mode 100644 index 0000000..a006ae4 --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/SourceInputHelper.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Input.Source +{ + public static class SourceInputHelper + { + internal static IEnumerable GetSymbols(INamespaceSymbol namespaceSymbol) + { + foreach (var childNamespaceSymbol in namespaceSymbol.GetNamespaceMembers()) + { + foreach (var symbol in GetSymbols(childNamespaceSymbol)) + { + yield return symbol; + } + } + + foreach (INamedTypeSymbol symbol in namespaceSymbol.GetTypeMembers()) + { + yield return symbol; + } + } + + internal static bool TryGetExistingMethod(INamedTypeSymbol? type, MethodSignature signature, [MaybeNullWhen(false)] out IMethodSymbol method) + { + if (type is null) + { + method = null; + return false; + } + + foreach (var member in type.GetMembers()) + { + if (member.Name != signature.Name) + { + continue; + } + + if (member is not IMethodSymbol candidate) + { + continue; + } + + if (signature.ExplicitInterface is {}) + { + if (candidate.MethodKind != MethodKind.ExplicitInterfaceImplementation) + { + continue; + } + } + else + { + if (candidate.MethodKind != MethodKind.Ordinary) + { + continue; + } + } + + if (TypesAreEqual(candidate.Parameters, signature.Parameters)) + { + method = candidate; + return true; + } + } + + method = null; + return false; + } + + private static bool TypesAreEqual(ImmutableArray candidateParameters, IReadOnlyList signatureParameters) + { + if (candidateParameters.Length != signatureParameters.Count) + { + return false; + } + + for (var i = 0; i < candidateParameters.Length; i++) + { + var candidateParameterType = candidateParameters[i].Type; + var signatureParameterType = signatureParameters[i].Type; + if (candidateParameterType is not INamedTypeSymbol typeSymbol || !typeSymbol.IsSameType(signatureParameterType)) + { + return false; + } + } + + return true; + } + } +} diff --git a/logger/autorest.csharp/common/Input/Source/SourceInputModel.cs b/logger/autorest.csharp/common/Input/Source/SourceInputModel.cs new file mode 100644 index 0000000..9bd89bf --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/SourceInputModel.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using AutoRest.CSharp.AutoRest.Plugins; +using System.Threading.Tasks; +using AutoRest.CSharp.Generation.Types; +using Azure.Core; +using Microsoft.Build.Construction; +using Microsoft.CodeAnalysis; +using NuGet.Configuration; +using AutoRest.CSharp.Common.Input; + +namespace AutoRest.CSharp.Input.Source +{ + public class SourceInputModel + { + private readonly CompilationInput? _existingCompilation; + private readonly CodeGenAttributes _codeGenAttributes; + private readonly Dictionary _nameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public Compilation Customization { get; } + public Compilation? PreviousContract { get; } + + public SourceInputModel(Compilation customization, CompilationInput? existingCompilation = null) + { + Customization = customization; + PreviousContract = LoadBaselineContract().GetAwaiter().GetResult(); + _existingCompilation = existingCompilation; + + _codeGenAttributes = new CodeGenAttributes(); + + IAssemblySymbol assembly = Customization.Assembly; + + foreach (IModuleSymbol module in assembly.Modules) + { + foreach (var type in SourceInputHelper.GetSymbols(module.GlobalNamespace)) + { + if (type is INamedTypeSymbol namedTypeSymbol && TryGetName(type, out var schemaName)) + { + _nameMap.Add(schemaName, namedTypeSymbol); + } + } + } + } + + public IReadOnlyList? GetServiceVersionOverrides() + { + var osvAttributeType = Customization.GetTypeByMetadataName(typeof(CodeGenOverrideServiceVersionsAttribute).FullName!)!; + var osvAttribute = Customization.Assembly.GetAttributes() + .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, osvAttributeType)); + + return osvAttribute?.ConstructorArguments[0].Values.Select(v => v.Value).OfType().ToList(); + } + + public ModelTypeMapping? CreateForModel(INamedTypeSymbol? symbol) + { + if (symbol == null) + return null; + + return new ModelTypeMapping(_codeGenAttributes, symbol); + } + + internal IMethodSymbol? FindMethod(string namespaceName, string typeName, string methodName, IEnumerable parameters) + { + return _existingCompilation?.FindMethod(namespaceName, typeName, methodName, parameters); + } + + public INamedTypeSymbol? FindForType(string ns, string name, bool includeArmCore = false) + { + var fullyQualifiedMetadataName = $"{ns}.{name}"; + if (!_nameMap.TryGetValue(name, out var type) && + !_nameMap.TryGetValue(fullyQualifiedMetadataName, out type)) + { + type = includeArmCore ? Customization.GetTypeByMetadataName(fullyQualifiedMetadataName) : Customization.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName); + } + + return type; + } + + internal bool TryGetClientSourceInput(INamedTypeSymbol type, [NotNullWhen(true)] out ClientSourceInput? clientSourceInput) + { + foreach (var attribute in type.GetAttributes()) + { + var attributeType = attribute.AttributeClass; + while (attributeType != null) + { + if (attributeType.Name == CodeGenAttributes.CodeGenClientAttributeName) + { + INamedTypeSymbol? parentClientType = null; + foreach ((var argumentName, TypedConstant constant) in attribute.NamedArguments) + { + if (argumentName == nameof(CodeGenClientAttribute.ParentClient)) + { + parentClientType = (INamedTypeSymbol?)constant.Value; + } + } + + clientSourceInput = new ClientSourceInput(parentClientType); + return true; + } + + attributeType = attributeType.BaseType; + } + } + + clientSourceInput = null; + return false; + } + + private bool TryGetName(ISymbol symbol, [NotNullWhen(true)] out string? name) + { + name = null; + + foreach (var attribute in symbol.GetAttributes()) + { + INamedTypeSymbol? type = attribute.AttributeClass; + while (type != null) + { + if (type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) == CodeGenAttributes.CodeGenTypeAttributeName) + { + if (attribute?.ConstructorArguments.Length > 0) + { + name = attribute.ConstructorArguments[0].Value as string; + break; + } + } + + type = type.BaseType; + } + } + + return name != null; + } + + private async Task LoadBaselineContract() + { + // This can only be used for Mgmt now, because there are custom/hand-written code in HLC can't be loaded into CsharpType such as generic methods + if (!Configuration.AzureArm) + return null; + + string fullPath; + string projectFilePath = Path.GetFullPath(Path.Combine(Configuration.AbsoluteProjectFolder, $"{Configuration.Namespace}.csproj")); + if (!File.Exists(projectFilePath)) + return null; + + var baselineVersion = ProjectRootElement.Open(projectFilePath).Properties.SingleOrDefault(p => p.Name == "ApiCompatVersion")?.Value; + + if (baselineVersion is not null) + { + var nugetGlobalPackageFolder = SettingsUtility.GetGlobalPackagesFolder(new NullSettings()); + var nugetFolder = Path.Combine(nugetGlobalPackageFolder, Configuration.Namespace.ToLowerInvariant(), baselineVersion, "lib", "netstandard2.0"); + fullPath = Path.Combine(nugetFolder, $"{Configuration.Namespace}.dll"); + if (File.Exists(fullPath)) + { + return await GeneratedCodeWorkspace.CreatePreviousContractFromDll(Path.Combine(nugetFolder, $"{Configuration.Namespace}.xml"), fullPath); + } + else + { + throw new InvalidOperationException($"Can't find Baseline contract assembly ({Configuration.Namespace}@{baselineVersion}) from Nuget Global Package Folder at {fullPath}. " + + $"Please make sure the baseline nuget package has been installed properly"); + } + } + return null; + } + } +} diff --git a/logger/autorest.csharp/common/Input/Source/SourcePropertySerializationMapping.cs b/logger/autorest.csharp/common/Input/Source/SourcePropertySerializationMapping.cs new file mode 100644 index 0000000..35e6b3f --- /dev/null +++ b/logger/autorest.csharp/common/Input/Source/SourcePropertySerializationMapping.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Input.Source +{ + public class SourcePropertySerializationMapping + { + public SourcePropertySerializationMapping(string propertyName, IReadOnlyList? serializationPath, string? jsonSerializationValueHook, string? jsonDeserializationValueHook, string? bicepSerializationValueHook) + { + PropertyName = propertyName; + SerializationPath = serializationPath; + JsonSerializationValueHook = jsonSerializationValueHook; + JsonDeserializationValueHook = jsonDeserializationValueHook; + BicepSerializationValueHook = bicepSerializationValueHook; + } + + public string PropertyName { get; } + public IReadOnlyList? SerializationPath { get; } + public string? JsonSerializationValueHook { get; } + public string? JsonDeserializationValueHook { get; } + + public string? BicepSerializationValueHook { get; } + } +} diff --git a/logger/autorest.csharp/common/Input/SystemApiTypes.cs b/logger/autorest.csharp/common/Input/SystemApiTypes.cs new file mode 100644 index 0000000..13a95b0 --- /dev/null +++ b/logger/autorest.csharp/common/Input/SystemApiTypes.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Output.Expressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Types.System; +using Azure.Core.Pipeline; //needed because BearerTokenAuthenticationPolicy doesn't exist in System.ServiceModel.Rest yet +using BinaryContent = System.ClientModel.BinaryContent; + +namespace AutoRest.CSharp.Common.Input +{ + internal class SystemApiTypes : ApiTypes + { + public override Type ResponseType => typeof(ClientResult); + public override Type ResponseOfTType => typeof(ClientResult<>); + public override string ResponseParameterName => "result"; + public override string ContentStreamName => $"{GetRawResponseName}().{nameof(PipelineResponse.ContentStream)}"; + public override string StatusName => $"{GetRawResponseName}().{nameof(PipelineResponse.Status)}"; + public override string GetRawResponseName => nameof(ClientResult.GetRawResponse); + + public override Type HttpPipelineType => typeof(ClientPipeline); + public override CSharpType PipelineExtensionsType => ClientPipelineExtensionsProvider.Instance.Type; + public override string HttpPipelineCreateMessageName => nameof(ClientPipeline.CreateMessage); + + public override Type HttpMessageType => typeof(PipelineMessage); + public override string HttpMessageResponseName => nameof(PipelineMessage.Response); + + public override Type ClientOptionsType => typeof(ClientPipelineOptions); + + public override Type RequestContextType => typeof(RequestOptions); + + public override string RequestContextName => "options"; + public override string RequestContextDescription => "The request options, which can override default behaviors of the client pipeline on a per-call basis."; + + public override Type BearerAuthenticationPolicyType => typeof(BearerTokenAuthenticationPolicy); + public override Type KeyCredentialType => typeof(ApiKeyCredential); + public override Type HttpPipelineBuilderType => typeof(ClientPipeline); + public override Type KeyCredentialPolicyType => typeof(ApiKeyAuthenticationPolicy); + public override FormattableString GetHttpPipelineClassifierString(string pipelineField, string optionsVariable, FormattableString perCallPolicies, FormattableString perRetryPolicies, FormattableString beforeTransportPolicies) + => $"{pipelineField:I} = {typeof(ClientPipeline)}.{nameof(ClientPipeline.Create)}({optionsVariable:I}, {perCallPolicies}, {perRetryPolicies}, {beforeTransportPolicies});"; + + public override FormattableString CredentialDescription => $"A credential used to authenticate to the service."; + + public override Type HttpPipelinePolicyType => typeof(PipelinePolicy); + + public override string HttpMessageRequestName => nameof(PipelineMessage.Request); + + public override FormattableString GetSetMethodString(string requestName, string method) + { + return $"{requestName}.{nameof(PipelineRequest.Method)} = \"{method}\";"; + } + + private string GetHttpMethodName(string method) + { + return $"{method[0]}{method.Substring(1).ToLowerInvariant()}"; + } + + public override FormattableString GetSetUriString(string requestName, string uriName) + => $"{requestName}.Uri = {uriName}.ToUri();"; + + public override Action WriteHeaderMethod => RequestWriterHelpers.WriteHeaderSystem; + + public override FormattableString GetSetContentString(string requestName, string contentName) + => $"{requestName}.Content = {contentName};"; + + public override CSharpType RequestUriType => ClientUriBuilderProvider.Instance.Type; + public override string MultipartRequestContentTypeName => "MultipartFormDataBinaryContent"; + public override Type RequestContentType => typeof(BinaryContent); + public override string ToRequestContentName => "ToBinaryContent"; + public override string ToMultipartRequestContentName => "ToMultipartBinaryBody"; + public override string RequestContentCreateName => nameof(BinaryContent.Create); + + public override Type IXmlSerializableType => throw new NotSupportedException("Xml serialization is not supported in non-branded libraries yet"); + + public override Type RequestFailedExceptionType => typeof(ClientResultException); + + public override Type ResponseClassifierType => typeof(PipelineMessageClassifier); + public override Type StatusCodeClassifierType => typeof(PipelineMessageClassifier); + + public override ValueExpression GetCreateFromStreamSampleExpression(ValueExpression freeFormObjectExpression) + => new InvokeStaticMethodExpression(Configuration.ApiTypes.RequestContentType, Configuration.ApiTypes.RequestContentCreateName, new[] { BinaryDataExpression.FromObjectAsJson(freeFormObjectExpression).ToStream() }); + + public override string EndPointSampleValue => "https://my-service.com"; + + public override string JsonElementVariableName => "element"; + + public override ValueExpression GetKeySampleExpression(string clientName) + => new InvokeStaticMethodExpression(typeof(Environment), nameof(Environment.GetEnvironmentVariable), new[] { new StringLiteralExpression($"{clientName}_KEY", false) }); + + public override ExtensibleSnippets ExtensibleSnippets { get; } = new SystemExtensibleSnippets(); + + public override string LicenseString => Configuration.CustomHeader ?? string.Empty; + + public override string ResponseClassifierIsErrorResponseName => nameof(PipelineMessageClassifier.TryClassify); + } +} diff --git a/logger/autorest.csharp/common/Input/Utf8JsonReaderExtensions.cs b/logger/autorest.csharp/common/Input/Utf8JsonReaderExtensions.cs new file mode 100644 index 0000000..75e5d1d --- /dev/null +++ b/logger/autorest.csharp/common/Input/Utf8JsonReaderExtensions.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AutoRest.CSharp.Common.Input +{ + internal static class Utf8JsonReaderExtensions + { + public static bool TryReadReferenceId(this ref Utf8JsonReader reader, ref bool isFirstProperty, ref string? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != "$id") + { + return false; + } + + if (!isFirstProperty) + { + throw new JsonException("$id should be the first defined property"); + } + + isFirstProperty = false; + + reader.Read(); + value = reader.GetString() ?? throw new JsonException(); + reader.Read(); + return true; + } + + public static bool TryReadBoolean(this ref Utf8JsonReader reader, string propertyName, ref bool value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + value = reader.GetBoolean(); + reader.Read(); + return true; + } + + public static bool TryReadString(this ref Utf8JsonReader reader, string propertyName, ref string? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + value = reader.GetString() ?? throw new JsonException(); + reader.Read(); + return true; + } + + public static bool TryReadEnumValue(this ref Utf8JsonReader reader, string propertyName, ref object? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + switch (reader.TokenType) + { + case JsonTokenType.String: + value = reader.GetString() ?? throw new JsonException("Enum value cannot be empty"); + break; + case JsonTokenType.Number: + if (reader.TryGetInt32(out int intValue)) + { + value = intValue; + } + else if (reader.TryGetSingle(out float floatValue)) + { + value = floatValue; + } + else + { + throw new JsonException($"Unsupported enum value type: {reader.TokenType}"); + } + break; + default: + throw new JsonException($"Unsupported enum value type: {reader.TokenType}"); + } + + reader.Read(); + return true; + } + + public static bool TryReadWithConverter(this ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref T? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + value = reader.ReadWithConverter(options); + return true; + } + + public static bool TryReadWithConverter(this ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref IReadOnlyList? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + reader.Read(); + + var result = new List(); + while (reader.TokenType != JsonTokenType.EndArray) + { + var item = reader.ReadWithConverter(options); + result.Add(item ?? throw new JsonException($"null value in list is not allowed")); + } + reader.Read(); + value = result; + return true; + } + + public static T? ReadWithConverter(this ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var converter = (JsonConverter)options.GetConverter(typeof(T)); + var value = converter.Read(ref reader, typeof(T), options); + reader.Read(); + return value; + } + + public static T? ReadReferenceAndResolve(this ref Utf8JsonReader reader, ReferenceResolver resolver) where T : class + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + reader.Read(); + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != "$ref") + { + return null; + } + + reader.Read(); + var idRef = reader.GetString() ?? throw new JsonException("$ref can't be null"); + var result = (T)resolver.ResolveReference(idRef ?? throw new JsonException()); + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("$ref should be the only property"); + } + + return result; + } + + public static bool TryReadStringArray(this ref Utf8JsonReader reader, string propertyName, ref IReadOnlyList? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + reader.Read(); + + var result = new List(); + while (reader.TokenType != JsonTokenType.EndArray) + { + result.Add(reader.GetString() ?? throw new JsonException()); + reader.Read(); + } + reader.Read(); + value = result; + return true; + } + + public static bool TryReadStringBinaryDataDictionary(this ref Utf8JsonReader reader, string propertyName, ref IReadOnlyDictionary? value) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + if (reader.GetString() != propertyName) + { + return false; + } + + reader.Read(); + using var document = JsonDocument.ParseValue(ref reader); + var result = new Dictionary(); + foreach (JsonProperty property in document.RootElement.EnumerateObject()) + { + result.Add(property.Name, BinaryData.FromString(property.Value.GetRawText())); + } + value = result; + return true; + } + + public static void SkipProperty(this ref Utf8JsonReader reader) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + reader.Read(); + reader.SkipValue(); + } + + private static void SkipValue(this ref Utf8JsonReader reader) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + reader.Read(); + while (reader.TokenType != JsonTokenType.EndObject) + { + reader.SkipProperty(); + } + reader.Read(); + break; + case JsonTokenType.StartArray: + reader.Read(); + while (reader.TokenType != JsonTokenType.EndArray) + { + reader.SkipValue(); + } + reader.Read(); + break; + case JsonTokenType.String: + case JsonTokenType.Number: + case JsonTokenType.True: + case JsonTokenType.False: + case JsonTokenType.Null: + reader.Read(); + break; + case JsonTokenType.Comment: + case JsonTokenType.None: + case JsonTokenType.EndObject: + case JsonTokenType.EndArray: + case JsonTokenType.PropertyName: + throw new InvalidOperationException("Unexpected token type"); + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/BicepSerializationMethodsBuilder.cs b/logger/autorest.csharp/common/Output/Builders/BicepSerializationMethodsBuilder.cs new file mode 100644 index 0000000..0565089 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/BicepSerializationMethodsBuilder.cs @@ -0,0 +1,682 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; +using Azure.ResourceManager; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using Constant = AutoRest.CSharp.Output.Models.Shared.Constant; +using ConstantExpression = AutoRest.CSharp.Common.Output.Expressions.ValueExpressions.ConstantExpression; +using Parameter = AutoRest.CSharp.Output.Models.Shared.Parameter; + +namespace AutoRest.CSharp.Common.Output.Builders +{ + internal static class BicepSerializationMethodsBuilder + { + private const string SerializeBicepMethodName = "SerializeBicep"; + + public static IEnumerable BuildPerTypeBicepSerializationMethods(BicepObjectSerialization objectSerialization) + { + yield return new Method( + new MethodSignature( + SerializeBicepMethodName, + null, + null, + MethodSignatureModifiers.Private, + typeof(BinaryData), + null, + new Parameter[] { KnownParameters.Serializations.Options }), + WriteSerializeBicep(objectSerialization).AsStatement()); + } + + private static MethodBodyStatement WriteFlattenedPropertiesWithOverrides(string wirePath, StringBuilderExpression stringBuilder, ValueExpression propertyOverride, int spaces) + { + var parts = wirePath.Split('.'); + + var body = new List(); + + // opening brace + body.Add(stringBuilder.AppendLine("{")); + + // nested property names and opening braces + for (int i = 1; i < parts.Length; i++) + { + spaces += 2; + var indent = new string(' ', spaces); + if (i == parts.Length - 1) + { + body.Add(stringBuilder.Append($"{indent}{parts[i]}: ")); + } + else + { + body.Add(stringBuilder.AppendLine($"{indent}{parts[i]}: {{")); + } + } + + // value + body.Add(stringBuilder.AppendLine(propertyOverride)); + + // closing braces + for (int i = parts.Length - 1; i >= 1; i--) + { + spaces -= 2; + var indent = new string(' ', spaces); + body.Add(stringBuilder.AppendLine($"{indent}}}")); + } + + return body; + } + + public static SwitchCase BuildBicepWriteSwitchCase(BicepObjectSerialization bicep, ModelReaderWriterOptionsExpression options) + { + return new SwitchCase( + Serializations.BicepFormat, + Return( + new InvokeInstanceMethodExpression( + null, + new MethodSignature( + $"SerializeBicep", + null, + null, + MethodSignatureModifiers.Private, + typeof(BinaryData), + null, + new[] + { + KnownParameters.Serializations.Options + }), + new ValueExpression[] + { + options + }))); + } + + private static List WriteSerializeBicep(BicepObjectSerialization objectSerialization) + { + var statements = new List(); + VariableReference stringBuilder = new VariableReference(typeof(StringBuilder), "builder"); + + statements.Add(Declare(stringBuilder, New.Instance(typeof(StringBuilder)))); + + VariableReference bicepOptions = new VariableReference(typeof(BicepModelReaderWriterOptions), "bicepOptions"); + statements.Add(Declare(bicepOptions, new AsExpression(KnownParameters.Serializations.Options, typeof(BicepModelReaderWriterOptions)))); + + VariableReference hasObjectOverride = new VariableReference(typeof(bool), "hasObjectOverride"); + VariableReference propertyOverrides = new VariableReference(typeof(IDictionary), "propertyOverrides"); + statements.Add(Declare(propertyOverrides, Null)); + statements.Add(Declare( + hasObjectOverride, + And( + new BoolExpression(NotEqual(bicepOptions, Null)), + new BoolExpression(bicepOptions.Property(nameof(BicepModelReaderWriterOptions.PropertyOverrides)) + .Invoke("TryGetValue", This, new KeywordExpression("out", propertyOverrides)))))); + var hasPropertyOverride = new VariableReference(typeof(bool), "hasPropertyOverride"); + statements.Add(Declare(hasPropertyOverride, BoolExpression.False)); + var propertyOverride = new VariableReference(typeof(string), "propertyOverride"); + statements.Add(Declare(propertyOverride, Null)); + + statements.Add(EmptyLine); + + var propertyOverrideVariables = new PropertyOverrideVariables( + bicepOptions, + propertyOverrides, + hasObjectOverride, + hasPropertyOverride, + propertyOverride, + objectSerialization); + + statements.Add(EmptyLine); + + var stringBuilderExpression = new StringBuilderExpression(stringBuilder); + statements.Add(stringBuilderExpression.AppendLine("{")); + statements.Add(EmptyLine); + foreach (MethodBodyStatement methodBodyStatement in WriteProperties(objectSerialization.Serializations, stringBuilderExpression, 2, objectSerialization.IsResourceData, propertyOverrideVariables)) + { + statements.Add(methodBodyStatement); + } + + statements.Add(stringBuilderExpression.AppendLine("}")); + statements.Add(Return(BinaryDataExpression.FromString(stringBuilder.Invoke(nameof(StringBuilder.ToString))))); + + return statements; + } + + private static IEnumerable WriteProperties( + IEnumerable properties, + StringBuilderExpression stringBuilder, + int spaces, + bool isResourceData, + PropertyOverrideVariables propertyOverrideVariables) + { + var indent = new string(' ', spaces); + var propertyList = properties.ToList(); + BicepPropertySerialization? name = null; + BicepPropertySerialization? location = null; + BicepPropertySerialization? tags = null; + BicepPropertySerialization? type = null; + + if (isResourceData) + { + name = propertyList.FirstOrDefault(p => p.SerializedName == "name"); + location = propertyList.FirstOrDefault(p => p.SerializedName == "location"); + tags = propertyList.FirstOrDefault(p => p.SerializedName == "tags"); + type = propertyList.FirstOrDefault(p => p.SerializedName == "type"); + } + + // The top level ResourceData properties should be written first in the payload. Type should not be included + // as it will be put in the outer envelope. + if (name != null) + { + foreach (MethodBodyStatement methodBodyStatement in WriteProperty(stringBuilder, spaces, name, indent, propertyOverrideVariables)) + { + yield return methodBodyStatement; + }; + } + + if (location != null) + { + foreach (MethodBodyStatement methodBodyStatement in WriteProperty(stringBuilder, spaces, location, indent, propertyOverrideVariables)) + { + yield return methodBodyStatement; + }; + } + + if (tags != null) + { + foreach (MethodBodyStatement methodBodyStatement in WriteProperty(stringBuilder, spaces, tags, indent, propertyOverrideVariables)) + { + yield return methodBodyStatement; + }; + } + + foreach (BicepPropertySerialization property in propertyList) + { + if (property == name || property == location || property == tags || property == type) + { + continue; + } + foreach (MethodBodyStatement methodBodyStatement in WriteProperty(stringBuilder, spaces, property, indent, propertyOverrideVariables)) + { + yield return methodBodyStatement; + } + } + } + + private static IEnumerable WriteProperty( + StringBuilderExpression stringBuilder, + int spaces, + BicepPropertySerialization property, + string indent, + PropertyOverrideVariables propertyOverrideVariables) + { + if (property.ValueSerialization == null) + { + // Flattened property - this is different than the other flattening scenario where models with a single property are flattened + yield return new[] + { + stringBuilder.Append($"{indent}{property.SerializedName}:"), + stringBuilder.AppendLine(" {"), + WriteProperties(property.PropertySerializations!, stringBuilder, spaces + 2, false, propertyOverrideVariables).ToArray(), + stringBuilder.AppendLine($"{indent}}}") + }; + } + else + { + foreach (MethodBodyStatement statement in SerializeProperty(stringBuilder, property, spaces, propertyOverrideVariables)) + { + yield return statement; + } + } + } + + private static IEnumerable SerializeProperty( + StringBuilderExpression stringBuilder, + BicepPropertySerialization property, + int spaces, + PropertyOverrideVariables propertyOverrideVariables) + { + var indent = new string(' ', spaces); + bool isFlattened = false; + bool wasFlattened = false; + string? wirePath = null; + string? childPropertyName = null; + + // is the property that we are trying to serialize a flattened property? If so, we need to use the name of the childmost property for overrides. + ValueExpression overridePropertyName = Nameof(property.Value); + string? previouslyFlattenedProperty = null; + if (property.Property!.FlattenedProperty != null) + { + overridePropertyName = Literal(property.Property!.FlattenedProperty.Declaration.Name); + isFlattened = true; + } + else if (WasPreviouslyFlattened(propertyOverrideVariables.serialization, property.Property, out previouslyFlattenedProperty, out wirePath)) + { + wasFlattened = true; + childPropertyName = ((SerializableObjectType)property.Property.ValueType.Implementation).Properties + .Single( + // find the property of the child object that corresponds to the next piece of the wirepath + p => p.GetWirePath() == string.Join(".", wirePath!.Split('.').Skip(1))).Declaration.Name; + } + + yield return Assign( + propertyOverrideVariables.HasPropertyOverride, + new BoolExpression( + And( + new BoolExpression(propertyOverrideVariables.HasObjectOverride), + new BoolExpression(propertyOverrideVariables.PropertyOverrides.Invoke("TryGetValue", overridePropertyName, + new KeywordExpression("out", propertyOverrideVariables.PropertyOverride)))))); + + var formattedPropertyName = $"{indent}{property.SerializedName}: "; + var propertyDictionary = new VariableReference(typeof(Dictionary), "propertyDictionary"); + + // we write the properties if there is a value or an override for that property + + wirePath = isFlattened ? property.Property.FlattenedProperty!.GetWirePath() : wirePath; + + + yield return new IfElseStatement( + new IfStatement(new BoolExpression(propertyOverrideVariables.HasPropertyOverride)) + { + stringBuilder.Append(formattedPropertyName), + new[] + { + isFlattened + ? WriteFlattenedPropertiesWithOverrides(wirePath!, stringBuilder, propertyOverrideVariables.PropertyOverride, spaces) + : stringBuilder.AppendLine(propertyOverrideVariables.PropertyOverride) + } + }, + ConstructElseStatement()); + + MethodBodyStatement ConstructElseStatement() + { + if (wasFlattened) + { + return new IfElseStatement( + new IfStatement( + new BoolExpression( + And( + new BoolExpression(propertyOverrideVariables.HasObjectOverride), + new BoolExpression(propertyOverrideVariables.PropertyOverrides.Invoke("TryGetValue", + Literal(previouslyFlattenedProperty), + new KeywordExpression("out", propertyOverrideVariables.PropertyOverride)))))) + { + stringBuilder.Append(formattedPropertyName), + new IfElseStatement( + Equal(property.Value, Null), + WriteFlattenedPropertiesWithOverrides(wirePath!, stringBuilder, + propertyOverrideVariables.PropertyOverride, spaces), + new[] + { + Declare(propertyDictionary, New.Instance(typeof(Dictionary))), + propertyDictionary.Invoke( + "Add", + Literal(childPropertyName), + propertyOverrideVariables.PropertyOverride).ToStatement(), + propertyOverrideVariables.BicepOptions + .Property(nameof(BicepModelReaderWriterOptions.PropertyOverrides)).Invoke( + "Add", + property.Value, + propertyDictionary).ToStatement(), + InvokeAppendChildObject(stringBuilder, property, spaces, formattedPropertyName) + }) + }, + WrapInIsDefined(property, + new[] + { + WrapInIsNotEmpty(property, + new[] + { + stringBuilder.Append(formattedPropertyName), + InvokeAppendChildObject(stringBuilder, property, spaces, + formattedPropertyName) + }) + }) + ); + } + + return WrapInIsDefined(property, + new[] + { + WrapInIsNotEmpty(property, + new[] + { + stringBuilder.Append(formattedPropertyName), + InvokeAppendChildObject(stringBuilder, property, spaces, + formattedPropertyName) + }) + }); + } + + yield return EmptyLine; + } + + private static MethodBodyStatement InvokeAppendChildObject(StringBuilderExpression stringBuilder, BicepPropertySerialization property, int spaces, string formattedPropertyName) + { + return property.CustomSerializationMethodName is {} serializationMethodName + ? InvokeCustomBicepSerializationMethod(serializationMethodName, stringBuilder) + : SerializeExpression(stringBuilder, formattedPropertyName, property.ValueSerialization!, property.Value, spaces); + } + + + private static bool WasPreviouslyFlattened(BicepObjectSerialization serialization, ObjectTypeProperty property, out string? previouslyFlattenedProperty, out string? wirePath) + { + previouslyFlattenedProperty = null; + wirePath = null; + + if (serialization.CustomizationType == null) + { + return false; + } + + string? path = null; + var previous = serialization.CustomizationType.GetMembers().SingleOrDefault( + m => m.Kind == SymbolKind.Property && m.GetAttributes() + .Any(a => + { + if (a.AttributeClass?.Name == "WirePath" + && a.ApplicationSyntaxReference != null) + { + // Get the path from the attribute. We can't use ConstructorArguments because the WirePath attribute is internal + // so Roslyn doesn't populate them. + path = + a.ApplicationSyntaxReference.SyntaxTree.ToString() + .AsSpan() + .Slice(a.ApplicationSyntaxReference.Span.Start, + a.ApplicationSyntaxReference.Span.Length).ToString().Split('(', ')')[1]; + + // strip enclosing quotes + path = path.Substring(1, path.Length - 2); + if (path.Contains($"{property.GetWirePath()}.")) + { + return true; + } + } + + return false; + })); + + wirePath = path; + previouslyFlattenedProperty = previous?.Name; + return previouslyFlattenedProperty != null; + } + + private static MethodBodyStatement SerializeExpression( + StringBuilderExpression stringBuilder, + string formattedPropertyName, + BicepSerialization valueSerialization, + ValueExpression expression, + int spaces, + bool isArrayElement = false) + { + return valueSerialization switch + { + BicepArraySerialization array => SerializeArray( + stringBuilder, + formattedPropertyName, + array, + new EnumerableExpression(array.ImplementationType.ElementType, expression), + spaces), + BicepDictionarySerialization dictionary => SerializeDictionary( + stringBuilder, + formattedPropertyName, + dictionary, + new DictionaryExpression(dictionary.Type.Arguments[0], dictionary.Type.Arguments[1], expression), + spaces), + BicepValueSerialization value => SerializeValue( + stringBuilder, + formattedPropertyName, + value, + expression, + spaces, + isArrayElement), + _ => throw new NotSupportedException() + }; + } + + private static MethodBodyStatement SerializeArray( + StringBuilderExpression stringBuilder, + string propertyName, + BicepArraySerialization arraySerialization, + EnumerableExpression array, + int spaces) + { + string indent = new string(' ', spaces); + return new[] + { + stringBuilder.AppendLine("["), + new ForeachStatement("item", array, out var item) + { + CheckCollectionItemForNull(stringBuilder, arraySerialization.ValueSerialization, item), + SerializeExpression(stringBuilder, propertyName, arraySerialization.ValueSerialization, item, spaces, true) + }, + stringBuilder.AppendLine($"{indent}]"), + }; + } + + private static MethodBodyStatement SerializeDictionary( + StringBuilderExpression stringBuilder, + string propertyName, + BicepDictionarySerialization dictionarySerialization, + DictionaryExpression dictionary, + int spaces) + { + var indent = new string(' ', spaces); + + return new[] + { + stringBuilder.AppendLine("{"), + new ForeachStatement("item", dictionary, out KeyValuePairExpression keyValuePair) + { + stringBuilder.Append( + new FormattableStringExpression( + $"{indent}{indent}'{{0}}': ", keyValuePair.Key)), + CheckCollectionItemForNull(stringBuilder, dictionarySerialization.ValueSerialization, keyValuePair.Value), + SerializeExpression(stringBuilder, propertyName, dictionarySerialization.ValueSerialization, keyValuePair.Value, spaces + 2) + }, + stringBuilder.AppendLine($"{indent}}}") + }; + } + + private static MethodBodyStatement SerializeValue( + StringBuilderExpression stringBuilder, + string formattedPropertyName, + BicepValueSerialization valueSerialization, + ValueExpression expression, + int spaces, + bool isArrayElement = false) + { + + var indent = isArrayElement ? new string(' ', spaces + 2) : new string(' ', 0); + + if (valueSerialization.Type.IsFrameworkType) + { + return SerializeFrameworkTypeValue(stringBuilder, valueSerialization, expression, indent); + } + + if (valueSerialization.Type.IsValueType) + { + switch (valueSerialization.Type.Implementation) + { + case EnumType { IsNumericValueType: true } enumType: + return stringBuilder.AppendLine( + new EnumExpression( + enumType, + expression.NullableStructValue(valueSerialization.Type)) + .Invoke(nameof(ToString))); + case EnumType enumType: + return stringBuilder.AppendLine( + new FormattableStringExpression( + $"{indent}'{{0}}'", + new EnumExpression( + enumType, + expression.NullableStructValue(valueSerialization.Type)) + .ToSerial())); + default: + return stringBuilder.AppendLine(new FormattableStringExpression("{0}{1}", + expression.Invoke(nameof(ToString)))); + } + } + + return new MethodBodyStatement[] + { + BicepSerializationTypeProvider.Instance.AppendChildObject( + stringBuilder, + expression, + new ConstantExpression(new Constant(isArrayElement ? spaces + 2 : spaces, typeof(int))), + isArrayElement ? BoolExpression.True : BoolExpression.False, + Literal(formattedPropertyName)) + }; + } + + private static MethodBodyStatement SerializeFrameworkTypeValue( + StringBuilderExpression stringBuilder, + BicepValueSerialization valueSerialization, + ValueExpression expression, + string indent) + { + var frameworkType = valueSerialization.Type.FrameworkType; + if (frameworkType == typeof(Nullable<>)) + { + frameworkType = valueSerialization.Type.Arguments[0].FrameworkType; + } + + expression = expression.NullableStructValue(valueSerialization.Type); + + if (frameworkType == typeof(Uri)) + { + return stringBuilder.AppendLine( + new FormattableStringExpression($"{indent}'{{0}}'", + expression.Property(nameof(Uri.AbsoluteUri)))); + } + + if (frameworkType == typeof(string)) + { + return new IfElseStatement( + new BoolExpression( + expression.Invoke(nameof(string.Contains), + new TypeReference(typeof(Environment)).Property(nameof(Environment.NewLine)))), + new[] + { + stringBuilder.AppendLine($"{indent}'''"), + stringBuilder.AppendLine(new FormattableStringExpression("{0}'''", expression)) + }, + stringBuilder.AppendLine(new FormattableStringExpression($"{indent}'{{0}}'", expression))); + } + + if (frameworkType == typeof(int)) + { + return stringBuilder.AppendLine(new FormattableStringExpression($"{indent}{{0}}", expression)); + } + + if (frameworkType == typeof(TimeSpan)) + { + return new[] + { + Var( + "formattedTimeSpan", + new StringExpression(new InvokeStaticMethodExpression(typeof(TypeFormatters), + nameof(TypeFormatters.ToString), new[] { expression, Literal("P") })), + out var timeSpanVariable), + stringBuilder.AppendLine(new FormattableStringExpression( + $"{indent}'{{0}}'", + timeSpanVariable)) + }; + } + + if (frameworkType == typeof(DateTimeOffset) || frameworkType == typeof(DateTime)) + { + return new[] + { + Var( + "formattedDateTimeString", + new StringExpression(new InvokeStaticMethodExpression(typeof(TypeFormatters), + nameof(TypeFormatters.ToString), new[] { expression, Literal("o") })), + out var dateTimeStringVariable), + stringBuilder.AppendLine(new FormattableStringExpression( + $"{indent}'{{0}}'", + dateTimeStringVariable)) + }; + } + + if (frameworkType == typeof(bool)) + { + return new[] + { + Var( + "boolValue", + new StringExpression(new TernaryConditionalOperator( + Equal(expression, BoolExpression.True), + Literal("true"), Literal("false"))), + out var boolVariable), + stringBuilder.AppendLine(new FormattableStringExpression($"{indent}{{0}}", boolVariable)) + }; + } + + return stringBuilder.AppendLine(new FormattableStringExpression($"{indent}'{{0}}'", expression.Invoke(nameof(ToString)))); + } + + private static MethodBodyStatement CheckCollectionItemForNull( + StringBuilderExpression stringBuilder, + BicepSerialization valueSerialization, + ValueExpression value) + => CollectionItemRequiresNullCheckInSerialization(valueSerialization) + ? new IfStatement(Equal(value, Null)) { stringBuilder.Append("null"), Continue } + : EmptyStatement; + + private static bool CollectionItemRequiresNullCheckInSerialization(BicepSerialization serialization) => + // nullable value type, like int? + serialization is { IsNullable: true } and BicepValueSerialization { Type.IsValueType: true } || + // list or dictionary + serialization is BicepArraySerialization or BicepDictionarySerialization || + // framework reference type, e.g. byte[] + serialization is BicepValueSerialization { Type: { IsValueType: false, IsFrameworkType: true } }; + + public static MethodBodyStatement WrapInIsDefined(BicepPropertySerialization serialization, MethodBodyStatement statement) + { + if (serialization.Value.Type is { IsNullable: false, IsValueType: true }) + { + return statement; + } + + return serialization.Value.Type.IsCollection && !serialization.Value.Type.IsReadOnlyMemory + ? new IfStatement(InvokeOptional.IsCollectionDefined(serialization.Value)) { statement } + : new IfStatement(OptionalTypeProvider.Instance.IsDefined(serialization.Value)) { statement }; + } + + public static MethodBodyStatement WrapInIsNotEmpty(BicepPropertySerialization serialization, MethodBodyStatement statement) + { + return serialization.Value.Type.IsCollection && !serialization.Value.Type.IsReadOnlyMemory + ? new IfStatement( + new BoolExpression(InvokeStaticMethodExpression.Extension(typeof(Enumerable), nameof(Enumerable.Any), serialization.Value))) + { + statement + } + : statement; + } + + private record struct PropertyOverrideVariables( + ValueExpression BicepOptions, + ValueExpression PropertyOverrides, + ValueExpression HasObjectOverride, + ValueExpression HasPropertyOverride, + ValueExpression PropertyOverride, + BicepObjectSerialization serialization) + { + } + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/BuilderHelpers.cs b/logger/autorest.csharp/common/Output/Builders/BuilderHelpers.cs new file mode 100644 index 0000000..c39723b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/BuilderHelpers.cs @@ -0,0 +1,443 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using SerializationFormat = AutoRest.CSharp.Output.Models.Serialization.SerializationFormat; + +namespace AutoRest.CSharp.Output.Builders +{ + internal static class BuilderHelpers + { + public static Constant StringConstant(string? s) => ParseConstant(s, new CSharpType(typeof(string), s == null)); + + public static Constant ParseConstant(object? value, CSharpType type) + { + object? normalizedValue; + + if (!type.IsFrameworkType && type.Implementation is EnumType enumType) + { + if (value == null) + { + return Constant.Default(type); + } + + var stringValue = Convert.ToString(value); + var enumTypeValue = enumType.Values.SingleOrDefault(v => v.Value.Value?.ToString() == stringValue); + + // Fallback to the string value if we can't find an appropriate enum member (would work only for extensible enums) + return new Constant((object?)enumTypeValue ?? stringValue, type); + } + + Type? frameworkType = type.FrameworkType; + if (frameworkType == null) + { + throw new InvalidOperationException("Only constants of framework type and enums are allowed"); + } + + if (frameworkType == typeof(byte[]) && value is string base64String) + normalizedValue = Convert.FromBase64String(base64String); + else if (frameworkType == typeof(BinaryData) && value is string base64String2) + normalizedValue = BinaryData.FromBytes(Convert.FromBase64String(base64String2)); + else if (frameworkType == typeof(DateTimeOffset) && value is string dateTimeString) + normalizedValue = DateTimeOffset.Parse(dateTimeString, styles: DateTimeStyles.AssumeUniversal); + else if (frameworkType == typeof(ResourceType) && value is string resourceTypeString) + normalizedValue = new ResourceType(resourceTypeString); + else + normalizedValue = Convert.ChangeType(value, frameworkType); + + return new Constant(normalizedValue, type); + } + + public static SerializationFormat GetSerializationFormat(Schema schema) => schema switch + { + ConstantSchema constantSchema => GetSerializationFormat(constantSchema.ValueType), // forward the constantSchema to its underlying type + + ByteArraySchema byteArraySchema => byteArraySchema.Format switch + { + ByteArraySchemaFormat.Base64url => SerializationFormat.Bytes_Base64Url, + ByteArraySchemaFormat.Byte => SerializationFormat.Bytes_Base64, + _ => SerializationFormat.Default + }, + + UnixTimeSchema => SerializationFormat.DateTime_Unix, + DateTimeSchema dateTimeSchema => dateTimeSchema.Format switch + { + DateTimeSchemaFormat.DateTime => SerializationFormat.DateTime_ISO8601, + DateTimeSchemaFormat.DateTimeRfc1123 => SerializationFormat.DateTime_RFC1123, + _ => SerializationFormat.Default + }, + + DateSchema _ => SerializationFormat.Date_ISO8601, + TimeSchema _ => SerializationFormat.Time_ISO8601, + + DurationSchema _ => schema.Extensions?.Format switch + { + XMsFormat.DurationConstant => SerializationFormat.Duration_Constant, + _ => SerializationFormat.Duration_ISO8601 + }, + + _ when schema.Type == AllSchemaTypes.Duration => SerializationFormat.Duration_ISO8601, + _ when schema.Type == AllSchemaTypes.DateTime => SerializationFormat.DateTime_ISO8601, + _ when schema.Type == AllSchemaTypes.Date => SerializationFormat.DateTime_ISO8601, + _ when schema.Type == AllSchemaTypes.Time => SerializationFormat.DateTime_ISO8601, + + _ => schema.Extensions?.Format switch + { + XMsFormat.DateTime => SerializationFormat.DateTime_ISO8601, + XMsFormat.DateTimeRFC1123 => SerializationFormat.DateTime_RFC1123, + XMsFormat.DateTimeUnix => SerializationFormat.DateTime_Unix, + XMsFormat.DurationConstant => SerializationFormat.Duration_Constant, + _ => SerializationFormat.Default + } + }; + + private const string EscapedAmpersand = "&"; + private const string EscapedLessThan = "<"; + private const string EscapedGreaterThan = ">"; + private const string EscapedAppostrophe = "'"; + private const string EscapedQuote = """; + public static string EscapeXmlDocDescription(string s) + { + if (string.IsNullOrEmpty(s)) + return s; + + var span = s.AsSpan(); + Dictionary replacements = new Dictionary(); + for (int i = 0; i < span.Length; i++) + { + switch (span[i]) + { + case '&': + if (IsAlreadyEscaped(ref span, i, out int escapeLength)) + { + i += escapeLength; + } + else + { + replacements.Add(i, EscapedAmpersand); + } + break; + case '<': + replacements.Add(i, EscapedLessThan); + break; + case '>': + replacements.Add(i, EscapedGreaterThan); + break; + } + } + if (replacements.Count > 0) + { + StringBuilder sb = new StringBuilder(); + int lastStart = 0; + foreach (var kv in replacements) + { + sb.Append(span.Slice(lastStart, kv.Key - lastStart)); + sb.Append(kv.Value); + lastStart = kv.Key + 1; + } + sb.Append(span.Slice(lastStart)); + return sb.ToString(); + } + return s; + } + + private static bool IsAlreadyEscaped(ref ReadOnlySpan span, int i, out int escapeLength) + { + return IsEscapedMatch(ref span, i, EscapedAmpersand, out escapeLength) || + IsEscapedMatch(ref span, i, EscapedLessThan, out escapeLength) || + IsEscapedMatch(ref span, i, EscapedGreaterThan, out escapeLength) || + IsEscapedMatch(ref span, i, EscapedAppostrophe, out escapeLength) || + IsEscapedMatch(ref span, i, EscapedQuote, out escapeLength); + } + + private static bool IsEscapedMatch(ref ReadOnlySpan span, int i, string escapedChar, out int escapeLength) + { + escapeLength = 0; + if (span.Length < i + escapedChar.Length) + return false; + + var slice = span.Slice(i, escapedChar.Length); + var isMatch = slice.Equals(escapedChar.AsSpan(), StringComparison.Ordinal); + if (isMatch) + escapeLength = slice.Length; + return isMatch; + } + + // TODO: Clean up these helper methods in https://github.com/Azure/autorest.csharp/issues/4767 + public static string CSharpName(this InputParameter parameter) => parameter.Name.ToVariableName(); + + public static string CSharpName(this RequestParameter parameter) => parameter.Language.Default.Name.ToVariableName(); + + public static string CSharpName(this ChoiceValue choice) => choice.Language.Default.Name.ToCleanName(); + + public static string CSharpName(this Property property) => + (property.Language.Default.Name == null || property.Language.Default.Name == "null") ? "NullProperty" : property.Language.Default.Name.ToCleanName(); + + public static string CSharpName(this InputModelProperty property) => + (property.Name == null || property.Name == "null") ? "NullProperty" : property.Name.ToCleanName(); + + public static string CSharpName(this InputOperation operation) => + operation.Name.ToCleanName(); + + public static string CSharpName(this InputType inputType) => + inputType.Name.ToCleanName(); + + public static string CSharpName(this Schema operation) => + operation.Language.Default.Name.ToCleanName(); + public static string CSharpName(this HttpResponseHeader header) => + header.Language!.Default.Name.ToCleanName(); + + public static TypeDeclarationOptions CreateTypeAttributes(string defaultName, string defaultNamespace, string defaultAccessibility, INamedTypeSymbol? existingType = null, bool existingTypeOverrides = false, bool isAbstract = false) + { + if (existingType != null) + { + return new TypeDeclarationOptions(existingType.Name, + existingType.ContainingNamespace.ToDisplayString(), + SyntaxFacts.GetText(existingType.DeclaredAccessibility), + existingType.IsAbstract || isAbstract, + existingTypeOverrides + ); + } + + return new TypeDeclarationOptions(defaultName, defaultNamespace, defaultAccessibility, isAbstract, false); + } + + public static CSharpType GetTypeFromExisting(ISymbol existingMember, CSharpType defaultType, TypeFactory typeFactory) + { + var newType = existingMember switch + { + IFieldSymbol { Type: INamedTypeSymbol { EnumUnderlyingType: { } } } => defaultType, // Special case for enums + IFieldSymbol fieldSymbol => typeFactory.CreateType(fieldSymbol.Type), + IPropertySymbol propertySymbol => typeFactory.CreateType(propertySymbol.Type), + _ => defaultType + }; + + return PromoteNullabilityInformation(newType, defaultType); + } + + private static bool IsExistingSettable(ISymbol existingMember) => existingMember switch + { + IPropertySymbol propertySymbol => propertySymbol.SetMethod != null, + IFieldSymbol fieldSymbol => !fieldSymbol.IsReadOnly, + _ => throw new NotSupportedException($"'{existingMember.ContainingType.Name}.{existingMember.Name}' must be either field or property.") + }; + + public static bool IsReadOnly(ISymbol existingMember, CSharpType type) + { + var hasSetter = IsExistingSettable(existingMember); + if (hasSetter) + { + return false; + } + + if (type.IsCollection) + { + return type.IsReadOnlyDictionary || type.IsReadOnlyList; + } + + return !hasSetter; + } + + public static MemberDeclarationOptions CreateMemberDeclaration(string defaultName, CSharpType defaultType, string defaultAccessibility, ISymbol? existingMember, TypeFactory typeFactory) + { + return existingMember != null ? + new MemberDeclarationOptions( + SyntaxFacts.GetText(existingMember.DeclaredAccessibility), + existingMember.Name, + GetTypeFromExisting(existingMember, defaultType, typeFactory) + ) : + new MemberDeclarationOptions( + defaultAccessibility, + defaultName, + defaultType + ); + } + + + // Because most of our libraries don't use C# nullable reference types we are losing nullability information + // for reference types when members are remapped + // Try to copy it back where possible from the original type + private static CSharpType PromoteNullabilityInformation(CSharpType newType, CSharpType defaultType) + { + if (newType.IsValueType) + { + return newType; + } + + if (newType.Arguments.Count != defaultType.Arguments.Count) + { + return newType.WithNullable(defaultType.IsNullable); + } + + if ((newType.IsList && defaultType.IsList) || + (newType.IsDictionary && defaultType.IsDictionary)) + { + var arguments = new CSharpType[newType.Arguments.Count]; + for (var i = 0; i < newType.Arguments.Count; i++) + { + arguments[i] = PromoteNullabilityInformation(newType.Arguments[i], defaultType.Arguments[i]); + } + + return new CSharpType(newType.FrameworkType, defaultType.IsNullable, arguments); + } + + return newType.WithNullable(defaultType.IsNullable); + } + + public static FormattableString CreateDerivedTypesDescription(CSharpType type) + { + if (type.IsCollection) + { + type = type.ElementType; + } + + if (type is { IsFrameworkType: false, Implementation: ObjectType objectType }) + { + return objectType.CreateExtraDescriptionWithDiscriminator(); + } + + return $""; + } + + public static string CreateDescription(this Schema schema) + => EscapeXmlDocDescription(schema.Language.Default.Description); + + public static string DisambiguateName(string typeName, string name, string suffix) + { + if (name == typeName || name is nameof(GetHashCode) or nameof(Equals) or nameof(ToString)) + { + return name + suffix; + } + + return name; + } + + public static string DisambiguateName(CSharpType type, string name, string suffix = "Value") + { + if (name == type.Name || + name == nameof(GetHashCode) || + name == nameof(Equals) || + name == nameof(ToString)) + { + return name + suffix; + } + + return name; + } + + public static MethodSignatureModifiers MapModifiers(ISymbol symbol) + { + var modifiers = MethodSignatureModifiers.None; + switch (symbol.DeclaredAccessibility) + { + case Accessibility.Public: + modifiers |= MethodSignatureModifiers.Public; + break; + case Accessibility.Internal: + modifiers |= MethodSignatureModifiers.Internal; + break; + case Accessibility.Private: + modifiers |= MethodSignatureModifiers.Private; + break; + case Accessibility.Protected: + modifiers |= MethodSignatureModifiers.Protected; + break; + case Accessibility.ProtectedAndInternal: + modifiers |= MethodSignatureModifiers.Protected | MethodSignatureModifiers.Internal; + break; + } + if (symbol.IsStatic) + { + modifiers |= MethodSignatureModifiers.Static; + } + if (symbol is IMethodSymbol methodSymbol && methodSymbol.IsAsync) + { + modifiers |= MethodSignatureModifiers.Async; + } + if (symbol.IsVirtual) + { + modifiers |= MethodSignatureModifiers.Virtual; + } + if (symbol.IsOverride) + { + modifiers |= MethodSignatureModifiers.Override; + } + return modifiers; + } + + public static CSharpType CreateAdditionalPropertiesPropertyType(CSharpType originalType, CSharpType unknownType) + { + // TODO -- we only construct additional properties when the type is verifiable, because we always need the property to fall into the bucket of serialized additional raw data field when it does not fit the additional properties. + var arguments = originalType.Arguments; + var keyType = arguments[0]; + var valueType = arguments[1]; + + return originalType.MakeGenericType(new[] { ReplaceUnverifiableType(keyType, unknownType), ReplaceUnverifiableType(valueType, unknownType) }); + } + + private static CSharpType ReplaceUnverifiableType(CSharpType type, CSharpType unknownType) + { + // when the type is System.Object Or BinaryData + if (type.EqualsIgnoreNullable(unknownType)) + { + return type; + } + + // when the type is a verifiable type + if (IsVerifiableType(type)) + { + return type; + } + + // when the type is a union + if (type.IsUnion) + { + return type; + } + + // otherwise the type is not a verifiable type + // replace for list + if (type.IsList) + { + return type.MakeGenericType(new[] { ReplaceUnverifiableType(type.Arguments[0], unknownType) }); + } + // replace for dictionary + if (type.IsDictionary) + { + return type.MakeGenericType(new[] { ReplaceUnverifiableType(type.Arguments[0], unknownType), ReplaceUnverifiableType(type.Arguments[1], unknownType) }); + } + // for the other cases, wrap them in a union + return CSharpType.FromUnion(new[] { type }); + } + + private static readonly HashSet _verifiableTypes = new HashSet + { + // The following types are constructed by the `TryGetXXX` methods on `JsonElement`. + typeof(byte), typeof(byte[]), typeof(sbyte), + typeof(DateTime), typeof(DateTimeOffset), + typeof(decimal), typeof(double), typeof(short), typeof(int), typeof(long), typeof(float), + typeof(ushort), typeof(uint), typeof(ulong), + typeof(Guid), + // The following types have a firm JsonValueKind to verify + typeof(string), typeof(bool) + }; + + public static bool IsVerifiableType(Type type) => _verifiableTypes.Contains(type); + + public static bool IsVerifiableType(CSharpType type) => type is { IsFrameworkType: true, FrameworkType: { } frameworkType } && IsVerifiableType(frameworkType); + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/ClientBuilder.cs b/logger/autorest.csharp/common/Output/Builders/ClientBuilder.cs new file mode 100644 index 0000000..f29d059 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/ClientBuilder.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Common.Output.Builders +{ + internal static class ClientBuilder + { + private const string ClientSuffixValue = "Client"; + private const string OperationsSuffixValue = "Operations"; + + public static string GetClientSuffix() => Configuration.AzureArm ? OperationsSuffixValue : ClientSuffixValue; + + public static string CreateDescription(OperationGroup operationGroup, string clientPrefix) + => CreateDescription(operationGroup.Language.Default.Description, clientPrefix); + + public static string CreateDescription(string description, string clientPrefix) + => string.IsNullOrWhiteSpace(description) + ? $"The {clientPrefix} service client." + : BuilderHelpers.EscapeXmlDocDescription(description); + + private const string AzurePackageNamespacePrefix = "Azure."; + private const string AzureMgmtPackageNamespacePrefix = "Azure.ResourceManager."; + + /// + /// Returns a the name of the RP from the namespace by the following rule: + /// If the namespace starts with `Azure.ResourceManager` and it is a management plane package, returns every segment concating after the `Azure.ResourceManager` prefix. + /// If the namespace starts with `Azure`, returns every segment concating together after the `Azure` prefix + /// Returns the namespace as the RP name if nothing matches. + /// + /// + /// + public static string GetRPName(string namespaceName) + { + var segments = namespaceName.Split('.'); + if (namespaceName.StartsWith(AzurePackageNamespacePrefix)) + { + if (Configuration.AzureArm && Configuration.MgmtConfiguration.IsArmCore) + { + return "ResourceManager"; + } + + if (Configuration.AzureArm && namespaceName.StartsWith(AzureMgmtPackageNamespacePrefix)) + { + return string.Join("", segments.Skip(2)); // skips "Azure" and "ResourceManager" + } + + return string.Join("", segments.Skip(1)); + } + return string.Join("", segments); + } + + public static string GetClientPrefix(string name, BuildContext context) + => GetClientPrefix(name, context.InputNamespace.Name); + + public static string GetClientPrefix(string? name, string namespaceName) + { + name = string.IsNullOrEmpty(name) ? namespaceName : name.ToCleanName(); + + if (name.EndsWith(OperationsSuffixValue) && name.Length > OperationsSuffixValue.Length) + { + name = name.Substring(0, name.Length - OperationsSuffixValue.Length); + } + + if (name.EndsWith(ClientSuffixValue) && name.Length >= ClientSuffixValue.Length) + { + name = name.Substring(0, name.Length - ClientSuffixValue.Length); + } + + return name; + } + + /// + /// This function builds an enumerable of from an and a + /// + /// The InputClient to build methods from + /// The corresponding RestClient to the operation group + /// The type declaration options + /// An enumerable of + public static IEnumerable BuildMethods(InputClient inputClient, RestClient restClient, TypeDeclarationOptions declaration) + { + foreach (var operation in inputClient.Operations) + { + if (operation.LongRunning != null || operation.Paging != null) + { + continue; + } + + RestClientMethod startMethod = restClient.GetOperationMethod(operation); + var name = operation.CleanName; + + yield return new ClientMethod( + name, + startMethod, + BuilderHelpers.EscapeXmlDocDescription(operation.Description), + new Diagnostic($"{declaration.Name}.{name}", Array.Empty()), + operation.Accessibility ?? "public"); + } + } + + /// + /// This function builds an enumerable of from an and a + /// + /// The InputClient to build methods from + /// The corresponding RestClient to the operation group + /// The type declaration options + /// An enumerable of + public static IEnumerable BuildPagingMethods(InputClient inputClient, RestClient restClient, TypeDeclarationOptions declaration) + { + foreach (var operation in inputClient.Operations) + { + var paging = operation.Paging; + if (paging == null || operation.LongRunning != null) + { + continue; + } + + RestClientMethod method = restClient.GetOperationMethod(operation); + RestClientMethod? nextPageMethod = restClient.GetNextOperationMethod(operation); + + yield return BuildPagingMethod(method.Name, paging.NextLinkName, paging.ItemName, method, nextPageMethod, declaration); + } + } + + public static PagingMethod BuildPagingMethod(string methodName, string? nextLinkName, string? itemName, RestClientMethod method, RestClientMethod? nextPageMethod, TypeDeclarationOptions declaration) + { + if (!(method.Responses.SingleOrDefault(r => r.ResponseBody != null)?.ResponseBody is ObjectResponseBody objectResponseBody)) + { + throw new InvalidOperationException($"Method {method.Name} has to have a return value"); + } + + return new PagingMethod( + method, + nextPageMethod, + methodName, + new Diagnostic($"{declaration.Name}.{methodName}"), + new PagingResponseInfo(nextLinkName, itemName, objectResponseBody.Type)); + } + + /// + /// This function builds an enumerable of from an and a + /// + /// The OperationGroup to build methods from + /// The corresponding RestClient to the operation group + /// The type declaration options + /// A delegate used for overriding the name of output + /// An enumerable of + public static IEnumerable BuildPagingMethods(InputClient inputClient, CmcRestClient restClient, TypeDeclarationOptions Declaration, + Func? nameOverrider = default) + { + foreach (var operation in inputClient.Operations) + { + OperationPaging? paging = operation.Paging; + if (paging == null || operation.IsLongRunning) + { + continue; + } + + RestClientMethod method = restClient.GetOperationMethod(operation); + RestClientMethod? nextPageMethod = restClient.GetNextOperationMethod(operation); + + if (!(method.Responses.SingleOrDefault(r => r.ResponseBody != null)?.ResponseBody is ObjectResponseBody objectResponseBody)) + { + throw new InvalidOperationException($"Method {method.Name} has to have a return value"); + } + + var name = nameOverrider?.Invoke(inputClient, operation, method) ?? method.Name; + + yield return new PagingMethod( + method, + nextPageMethod, + name, + new Diagnostic($"{Declaration.Name}.{name}"), + new PagingResponseInfo(paging.NextLinkName, paging.ItemName, objectResponseBody.Type)); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/JsonSerializationMethodsBuilder.cs b/logger/autorest.csharp/common/Output/Builders/JsonSerializationMethodsBuilder.cs new file mode 100644 index 0000000..a44969d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/JsonSerializationMethodsBuilder.cs @@ -0,0 +1,1305 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using Azure.ResourceManager.Resources.Models; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using MemberExpression = AutoRest.CSharp.Common.Output.Expressions.ValueExpressions.MemberExpression; +using SerializationFormat = AutoRest.CSharp.Output.Models.Serialization.SerializationFormat; +using SwitchCase = AutoRest.CSharp.Common.Output.Expressions.Statements.SwitchCase; + +namespace AutoRest.CSharp.Common.Output.Builders +{ + internal static class JsonSerializationMethodsBuilder + { + private const string _jsonModelWriteCoreMethodName = "JsonModelWriteCore"; + + public static IEnumerable BuildResourceJsonSerializationMethods(Resource resource) + { + var resourceDataType = resource.ResourceData.Type; + var jsonModelInterface = new CSharpType(typeof(IJsonModel<>), resourceDataType); + var options = new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.Options); + var modelReaderWriter = new CSharpType(typeof(ModelReaderWriter)); + var iModelTInterface = new CSharpType(typeof(IPersistableModel<>), resourceDataType); + var data = new BinaryDataExpression(KnownParameters.Serializations.Data); + + // void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + var writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + yield return new Method( + new MethodSignature(nameof(IJsonModel.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.Utf8JsonWriter, KnownParameters.Serializations.Options }, ExplicitInterface: jsonModelInterface), + // => ((IJsonModel)Data).Write(writer, options); + new MemberExpression(This, "Data").CastTo(jsonModelInterface).Invoke(nameof(IJsonModel.Write), writer, options)); + + // T IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + var reader = KnownParameters.Serializations.Utf8JsonReader; + yield return new Method( + new MethodSignature(nameof(IJsonModel.Create), null, null, MethodSignatureModifiers.None, resourceDataType, null, new[] { reader, KnownParameters.Serializations.Options }, ExplicitInterface: jsonModelInterface), + // => ((IJsonModel)Data).Create(reader, options); + new MemberExpression(This, "Data").CastTo(jsonModelInterface).Invoke(nameof(IJsonModel.Create), reader, options)); + + // BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + yield return new Method( + new MethodSignature(nameof(IPersistableModel.Write), null, null, MethodSignatureModifiers.None, typeof(BinaryData), null, new[] { KnownParameters.Serializations.Options }, ExplicitInterface: iModelTInterface), + // => ModelReaderWriter.Write(Data, options); + new InvokeStaticMethodExpression(modelReaderWriter, "Write", new List { new MemberExpression(This, "Data"), options }, new List { resourceDataType })); + + // T IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + yield return new Method( + new MethodSignature(nameof(IPersistableModel.Create), null, null, MethodSignatureModifiers.None, resourceDataType, null, new[] { KnownParameters.Serializations.Data, KnownParameters.Serializations.Options }, ExplicitInterface: iModelTInterface), + // => ModelReaderWriter.Read(new BinaryData(reader.ValueSequence)); + new InvokeStaticMethodExpression(modelReaderWriter, "Read", new List { data, options }, new List { resourceDataType })); + + // ModelReaderWriterFormat IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + yield return new Method( + new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { KnownParameters.Serializations.Options }, ExplicitInterface: iModelTInterface), + // => Data.GetFormatFromOptions(options); + new MemberExpression(This, "Data").CastTo(iModelTInterface).Invoke(nameof(IPersistableModel.GetFormatFromOptions), options)); + } + + public static IEnumerable BuildJsonSerializationMethods(JsonObjectSerialization json, SerializationInterfaces? interfaces, bool hasInherits, bool isSealed) + { + var useModelReaderWriter = Configuration.UseModelReaderWriter; + + var iJsonInterface = interfaces?.IJsonInterface; + var iJsonModelInterface = interfaces?.IJsonModelTInterface; + var iPersistableModelTInterface = interfaces?.IPersistableModelTInterface; + var iJsonModelObjectInterface = interfaces?.IJsonModelObjectInterface; + var writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + if (iJsonInterface is not null) + { + // void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) + if (iJsonModelInterface is not null) + { + yield return new + ( + new MethodSignature(nameof(IUtf8JsonSerializable.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.Utf8JsonWriter }, ExplicitInterface: iJsonInterface), + This.CastTo(iJsonModelInterface).Invoke(nameof(IJsonModel.Write), writer, ModelReaderWriterOptionsExpression.Wire) + ); + } + else + { + yield return new + ( + new MethodSignature(nameof(IUtf8JsonSerializable.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.Utf8JsonWriter }, ExplicitInterface: iJsonInterface), + WriteObject(json, writer, null, null) + ); + } + } + + if (interfaces is null && Configuration.UseModelReaderWriter) + { + // void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) + yield return BuildJsonModelWriteCoreMethod(json, null, hasInherits, isSealed); + } + + if (iJsonModelInterface is not null && iPersistableModelTInterface is not null) + { + var typeOfT = iJsonModelInterface.Arguments[0]; + var model = typeOfT.Implementation as SerializableObjectType; + Debug.Assert(model != null, $"{typeOfT} should be a SerializableObjectType"); + + // void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) + var jsonModelWriteCore = BuildJsonModelWriteCoreMethod(json, iPersistableModelTInterface, hasInherits, isSealed); + + // void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + var options = new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.Options); + yield return new + ( + new MethodSignature(nameof(IJsonModel.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.Utf8JsonWriter, KnownParameters.Serializations.Options }, ExplicitInterface: iJsonModelInterface), + BuildJsonModelWriteMethodBody(jsonModelWriteCore, writer) + ); + yield return jsonModelWriteCore; + // T IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + var reader = KnownParameters.Serializations.Utf8JsonReader; + yield return new + ( + new MethodSignature(nameof(IJsonModel.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { KnownParameters.Serializations.Utf8JsonReader, KnownParameters.Serializations.Options }, ExplicitInterface: iJsonModelInterface), + new[] + { + Serializations.ValidateJsonFormat(options, iPersistableModelTInterface, Serializations.ValidationType.Read), + // using var document = JsonDocument.ParseValue(ref reader); + UsingDeclare("document", JsonDocumentExpression.ParseValue(reader), out var docVariable), + // return DeserializeXXX(doc.RootElement, options); + Return(SerializableObjectTypeExpression.Deserialize(model, docVariable.RootElement, options)) + } + ); + + // if the model is a struct, it needs to implement IJsonModel as well which leads to another 2 methods + if (iJsonModelObjectInterface is not null) + { + // void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IJsonModel.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.Utf8JsonWriter, KnownParameters.Serializations.Options }, ExplicitInterface: iJsonModelObjectInterface), + This.CastTo(iJsonModelInterface).Invoke(nameof(IJsonModel.Write), writer, options) + ); + + // object IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IJsonModel.Create), null, null, MethodSignatureModifiers.None, typeof(object), null, new[] { KnownParameters.Serializations.Utf8JsonReader, KnownParameters.Serializations.Options }, ExplicitInterface: iJsonModelObjectInterface), + This.CastTo(iJsonModelInterface).Invoke(nameof(IJsonModel.Create), reader, options) + ); + } + } + } + + public static SwitchCase BuildJsonWriteSwitchCase(JsonObjectSerialization json, ModelReaderWriterOptionsExpression options) + { + return new SwitchCase(Serializations.JsonFormat, + Return(new InvokeStaticMethodExpression(typeof(ModelReaderWriter), nameof(ModelReaderWriter.Write), new[] { This, options }))); + } + + public static SwitchCase BuildJsonCreateSwitchCase(SerializableObjectType model, BinaryDataExpression data, ModelReaderWriterOptionsExpression options) + { + /* using var document = JsonDocument.ParseValue(ref reader); + * return DeserializeXXX(doc.RootElement, options); + */ + return new SwitchCase(Serializations.JsonFormat, + new MethodBodyStatement[] + { + UsingDeclare("document", JsonDocumentExpression.Parse(data), out var docVariable), + Return(SerializableObjectTypeExpression.Deserialize(model, docVariable.RootElement, options)) + }, addScope: true); // using statement must have a scope, if we do not have the addScope parameter here, the generated code will not compile + } + + // TODO -- make the options and iPersistableModelTInterface parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static MethodBodyStatement[] WriteObject(JsonObjectSerialization serialization, Utf8JsonWriterExpression utf8JsonWriter, ModelReaderWriterOptionsExpression? options, CSharpType? iPersistableModelTInterface) + => new[] + { + Serializations.ValidateJsonFormat(options, iPersistableModelTInterface, Serializations.ValidationType.Write), + utf8JsonWriter.WriteStartObject(), + WriteProperties(utf8JsonWriter, serialization.Properties, serialization.RawDataField?.Value, options).ToArray(), + SerializeAdditionalProperties(utf8JsonWriter, options, serialization.AdditionalProperties, false), + SerializeAdditionalProperties(utf8JsonWriter, options, serialization.RawDataField, true), + utf8JsonWriter.WriteEndObject() + }; + + private static MethodBodyStatement[] BuildJsonModelWriteMethodBody(Method jsonModelWriteCoreMethod, Utf8JsonWriterExpression utf8JsonWriter) + { + var coreMethodSignature = jsonModelWriteCoreMethod.Signature; + + return new[] + { + utf8JsonWriter.WriteStartObject(), + This.Invoke((MethodSignature)coreMethodSignature).ToStatement(), + utf8JsonWriter.WriteEndObject(), + }; + } + + // void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) + private static Method BuildJsonModelWriteCoreMethod(JsonObjectSerialization serialization, CSharpType? iPersistableModelTInterface, bool hasInherits, bool isSealed) + { + MethodSignatureModifiers modifiers = hasInherits ? MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; + if (serialization.Type.IsValueType || isSealed) + modifiers = MethodSignatureModifiers.Private; + + var utf8JsonWriter = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriterWithDescription); + var options = new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.OptionsWithDescription); + + return new Method + ( + new MethodSignature(_jsonModelWriteCoreMethodName, null, null, modifiers, null, null, new[] { KnownParameters.Serializations.Utf8JsonWriterWithDescription, KnownParameters.Serializations.OptionsWithDescription }), + BuildJsonModelWriteCoreMethodBody(serialization, utf8JsonWriter, options, iPersistableModelTInterface, hasInherits) + ); + } + + private static MethodBodyStatement[] BuildJsonModelWriteCoreMethodBody(JsonObjectSerialization serialization, Utf8JsonWriterExpression utf8JsonWriter, ModelReaderWriterOptionsExpression options, CSharpType? iPersistableModelTInterface, bool hasInherits) + { + return new[] + { + Serializations.ValidateJsonFormat(options, iPersistableModelTInterface, Serializations.ValidationType.Write), + CallBaseJsonModelWriteCore(utf8JsonWriter, options, hasInherits), + WriteProperties(utf8JsonWriter, serialization.SelfProperties, serialization.RawDataField?.Value, options).ToArray(), + SerializeAdditionalProperties(utf8JsonWriter, options, serialization.AdditionalProperties, false), + CallSerializeAdditionalPropertiesForRawData(serialization, utf8JsonWriter, options, hasInherits) + }; + } + + private static MethodBodyStatement CallSerializeAdditionalPropertiesForRawData(JsonObjectSerialization serialization, Utf8JsonWriterExpression utf8JsonWriter, ModelReaderWriterOptionsExpression options, bool hasInherits) + { + return hasInherits ? + EmptyStatement + : SerializeAdditionalProperties(utf8JsonWriter, options, serialization.RawDataField, true); + } + + private static MethodBodyStatement CallBaseJsonModelWriteCore(Utf8JsonWriterExpression utf8JsonWriter, ModelReaderWriterOptionsExpression options, bool hasInherits) + { + // base.() + return hasInherits ? + Base.Invoke(_jsonModelWriteCoreMethodName, utf8JsonWriter, options).ToStatement() + : EmptyStatement; + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable WriteProperties(Utf8JsonWriterExpression utf8JsonWriter, IEnumerable properties, ValueExpression? rawData, ModelReaderWriterOptionsExpression? options) + { + // TODO -- update this so that we could decide if we want to write the extra check of checking if this thing is in the raw data field dictionary + foreach (JsonPropertySerialization property in properties) + { + if (property.ValueSerialization == null) + { + // Flattened property + yield return Serializations.WrapInCheckInRawData(rawData, property.SerializedName, Serializations.WrapInCheckNotWire( + property.ShouldExcludeInWireSerialization, + options?.Format, + new[] + { + utf8JsonWriter.WritePropertyName(property.SerializedName), + utf8JsonWriter.WriteStartObject(), + WriteProperties(utf8JsonWriter, property.PropertySerializations!, null, options).ToArray(), // the raw data should not pass through to the flattened properties + utf8JsonWriter.WriteEndObject(), + })); + } + else if (property.SerializedType is { IsNullable: true }) + { + var checkPropertyIsInitialized = property.Value.Type.IsCollection && !property.Value.Type.IsReadOnlyMemory && property.IsRequired + ? And(NotEqual(property.Value, Null), InvokeOptional.IsCollectionDefined(property.Value)) + : NotEqual(property.Value, Null); + + yield return Serializations.WrapInCheckInRawData(rawData, property.SerializedName, Serializations.WrapInCheckNotWire( + property.ShouldExcludeInWireSerialization, + options?.Format, + InvokeOptional.WrapInIsDefined( + property, + new IfElseStatement(checkPropertyIsInitialized, + WritePropertySerialization(utf8JsonWriter, property, options), + utf8JsonWriter.WriteNull(property.SerializedName) + )) + )); + } + else + { + yield return Serializations.WrapInCheckInRawData(rawData, property.SerializedName, Serializations.WrapInCheckNotWire( + property.ShouldExcludeInWireSerialization, + options?.Format, + InvokeOptional.WrapInIsDefined(property, WritePropertySerialization(utf8JsonWriter, property, options)))); + } + } + } + + private static MethodBodyStatement WritePropertySerialization(Utf8JsonWriterExpression utf8JsonWriter, JsonPropertySerialization serialization, ModelReaderWriterOptionsExpression? options) + { + return new[] + { + utf8JsonWriter.WritePropertyName(serialization.SerializedName), + serialization.CustomSerializationMethodName is {} serializationMethodName + ? InvokeCustomSerializationMethod(serializationMethodName, utf8JsonWriter, options) + : SerializeExpression(utf8JsonWriter, serialization.ValueSerialization, serialization.EnumerableValue ?? serialization.Value, options) + }; + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static MethodBodyStatement SerializeAdditionalProperties(Utf8JsonWriterExpression utf8JsonWriter, ModelReaderWriterOptionsExpression? options, JsonAdditionalPropertiesSerialization? additionalProperties, bool isRawData) + { + if (additionalProperties is null) + { + return EmptyStatement; + } + + var additionalPropertiesExpression = new DictionaryExpression(additionalProperties.ImplementationType.Arguments[0], additionalProperties.ImplementationType.Arguments[1], additionalProperties.Value); + MethodBodyStatement statement = new ForeachStatement("item", additionalPropertiesExpression, out KeyValuePairExpression item) + { + SerializeForRawData(utf8JsonWriter, additionalProperties.ValueSerialization, item, options, isRawData) + }; + + // if it should be excluded in wire serialization, it is a raw data field and we need to check if it is null + // otherwise it is the public AdditionalProperties property, we always instantiate it therefore we do not need to check null. + statement = isRawData ? + new IfStatement(NotEqual(additionalPropertiesExpression, Null)) + { + statement + } : statement; + + return Serializations.WrapInCheckNotWire( + additionalProperties.ShouldExcludeInWireSerialization, + options?.Format, + statement); + + static MethodBodyStatement SerializeForRawData(Utf8JsonWriterExpression utf8JsonWriter, JsonSerialization? valueSerialization, KeyValuePairExpression item, ModelReaderWriterOptionsExpression? options, bool isRawData) + { + MethodBodyStatement statement = new MethodBodyStatement[] + { + utf8JsonWriter.WritePropertyName(item.Key), + SerializeExpression(utf8JsonWriter, valueSerialization, item.Value, options) + }; + if (!isRawData) + { + return statement; + } + + if (Configuration.EnableInternalRawData && options != null) + { + statement = new MethodBodyStatement[] + { + new IfStatement(ModelSerializationExtensionsProvider.Instance.IsSentinelValue(item.Value)) + { + Continue + }, + statement + }; + } + + return statement; + } + } + + public static MethodBodyStatement SerializeExpression(Utf8JsonWriterExpression utf8JsonWriter, JsonSerialization? serialization, TypedValueExpression expression, ModelReaderWriterOptionsExpression? options) + => serialization switch + { + JsonArraySerialization array => SerializeArray(utf8JsonWriter, array, GetEnumerableExpression(expression, array), options), + JsonDictionarySerialization dictionary => SerializeDictionary(utf8JsonWriter, dictionary, new DictionaryExpression(dictionary.Type.Arguments[0], dictionary.Type.Arguments[1], expression), options), + JsonValueSerialization value => SerializeValue(utf8JsonWriter, value, expression, options), + _ => throw new NotSupportedException() + }; + + private static EnumerableExpression GetEnumerableExpression(ValueExpression expression, JsonArraySerialization array) + { + if (expression is TypedValueExpression typed && typed.Type.IsReadOnlyMemory) + { + expression = typed + .NullableStructValue() + .Property(nameof(ReadOnlyMemory.Span), new CSharpType(typeof(ReadOnlySpan<>), typed.Type.Arguments[0])); + } + + return new EnumerableExpression(array.Type.ElementType, expression); + } + + private static MethodBodyStatement SerializeArray(Utf8JsonWriterExpression utf8JsonWriter, JsonArraySerialization arraySerialization, EnumerableExpression array, ModelReaderWriterOptionsExpression? options) + { + return new[] + { + utf8JsonWriter.WriteStartArray(), + new ForeachStatement("item", array, out var item) + { + CheckCollectionItemForNull(utf8JsonWriter, arraySerialization.ValueSerialization, item), + SerializeExpression(utf8JsonWriter, arraySerialization.ValueSerialization, item, options) + }, + utf8JsonWriter.WriteEndArray() + }; + } + + private static MethodBodyStatement SerializeDictionary(Utf8JsonWriterExpression utf8JsonWriter, JsonDictionarySerialization dictionarySerialization, DictionaryExpression dictionary, ModelReaderWriterOptionsExpression? options) + { + return new[] + { + utf8JsonWriter.WriteStartObject(), + new ForeachStatement("item", dictionary, out KeyValuePairExpression keyValuePair) + { + utf8JsonWriter.WritePropertyName(keyValuePair.Key), + CheckCollectionItemForNull(utf8JsonWriter, dictionarySerialization.ValueSerialization, keyValuePair.Value), + SerializeExpression(utf8JsonWriter, dictionarySerialization.ValueSerialization, keyValuePair.Value, options) + }, + utf8JsonWriter.WriteEndObject() + }; + } + + private static MethodBodyStatement SerializeValue(Utf8JsonWriterExpression utf8JsonWriter, JsonValueSerialization valueSerialization, TypedValueExpression value, ModelReaderWriterOptionsExpression? options) + { + if (valueSerialization.Type.SerializeAs is not null) + { + return SerializeFrameworkTypeValue(utf8JsonWriter, valueSerialization, value, valueSerialization.Type.SerializeAs, options); + } + + if (valueSerialization.Type.IsFrameworkType) + { + return SerializeFrameworkTypeValue(utf8JsonWriter, valueSerialization, value, valueSerialization.Type.FrameworkType, options); + } + + switch (valueSerialization.Type.Implementation) + { + case SystemObjectType systemObjectType when IsCustomJsonConverterAdded(systemObjectType.SystemType): + if (valueSerialization.Options == JsonSerializationOptions.UseManagedServiceIdentityV3) + { + return new[] + { + Var("serializeOptions", New.JsonSerializerOptions(), out var serializeOptions), + JsonSerializerExpression.Serialize(utf8JsonWriter, value, serializeOptions).ToStatement() + }; + } + + return JsonSerializerExpression.Serialize(utf8JsonWriter, value).ToStatement(); + + case ObjectType: + return utf8JsonWriter.WriteObjectValue(value, options: options); + + case EnumType { IsIntValueType: true, IsExtensible: false } enumType: + return utf8JsonWriter.WriteNumberValue(new CastExpression(value.NullableStructValue(valueSerialization.Type), enumType.ValueType)); + + case EnumType { IsNumericValueType: true } enumType: + return utf8JsonWriter.WriteNumberValue(new EnumExpression(enumType, value.NullableStructValue(valueSerialization.Type)).ToSerial()); + + case EnumType enumType: + return utf8JsonWriter.WriteStringValue(new EnumExpression(enumType, value.NullableStructValue(valueSerialization.Type)).ToSerial()); + + default: + throw new NotSupportedException($"Cannot build serialization expression for type {valueSerialization.Type}, please add `CodeGenMemberSerializationHooks` to specify the serialization of this type with the customized property"); + } + } + + private static MethodBodyStatement SerializeFrameworkTypeValue(Utf8JsonWriterExpression utf8JsonWriter, JsonValueSerialization valueSerialization, ValueExpression value, Type valueType, ModelReaderWriterOptionsExpression? options) + { + if (valueType == typeof(JsonElement)) + { + return new JsonElementExpression(value).WriteTo(utf8JsonWriter); + } + + if (valueType == typeof(Nullable<>)) + { + valueType = valueSerialization.Type.Arguments[0].FrameworkType; + } + + value = value.NullableStructValue(valueSerialization.Type); + + if (valueType == typeof(decimal) || + valueType == typeof(double) || + valueType == typeof(float) || + IsIntType(valueType)) + { + if (valueSerialization.Format is SerializationFormat.Int_String && + IsIntType(valueType)) + { + return utf8JsonWriter.WriteStringValue(value.InvokeToString()); + } + return utf8JsonWriter.WriteNumberValue(value); + } + + if (valueType == typeof(object)) + { + return utf8JsonWriter.WriteObjectValue(new TypedValueExpression(valueType, value), options); + } + + // These are string-like types that could implicitly convert to string type + if (valueType == typeof(string) || valueType == typeof(char) || valueType == typeof(Guid) || valueType == typeof(ResourceIdentifier) || valueType == typeof(ResourceType) || valueType == typeof(AzureLocation)) + { + return utf8JsonWriter.WriteStringValue(value); + } + + if (valueType == typeof(bool)) + { + return utf8JsonWriter.WriteBooleanValue(value); + } + + if (valueType == typeof(byte[])) + { + return utf8JsonWriter.WriteBase64StringValue(value, valueSerialization.Format.ToFormatSpecifier()); + } + + if (valueType == typeof(DateTimeOffset) || valueType == typeof(DateTime) || valueType == typeof(TimeSpan)) + { + var format = valueSerialization.Format.ToFormatSpecifier(); + + if (valueSerialization.Format is SerializationFormat.Duration_Seconds) + { + return utf8JsonWriter.WriteNumberValue(InvokeConvert.ToInt32(new TimeSpanExpression(value).InvokeToString(format))); + } + + if (valueSerialization.Format is SerializationFormat.Duration_Seconds_Float or SerializationFormat.Duration_Seconds_Double) + { + return utf8JsonWriter.WriteNumberValue(InvokeConvert.ToDouble(new TimeSpanExpression(value).InvokeToString(format))); + } + + if (valueSerialization.Format is SerializationFormat.DateTime_Unix) + { + return utf8JsonWriter.WriteNumberValue(value, format); + } + return format is not null + ? utf8JsonWriter.WriteStringValue(value, format) + : utf8JsonWriter.WriteStringValue(value); + } + + // These are string-like types that cannot implicitly convert to string type, therefore we need to call ToString on them + if (valueType == typeof(ETag) || valueType == typeof(ContentType) || valueType == typeof(IPAddress) || valueType == typeof(RequestMethod) || valueType == typeof(ExtendedLocationType)) + { + return utf8JsonWriter.WriteStringValue(value.InvokeToString()); + } + + if (valueType == typeof(Uri)) + { + return utf8JsonWriter.WriteStringValue(new MemberExpression(value, nameof(Uri.AbsoluteUri))); + } + + if (valueType == typeof(BinaryData)) + { + var binaryDataValue = new BinaryDataExpression(value); + if (valueSerialization.Format is SerializationFormat.Bytes_Base64 or SerializationFormat.Bytes_Base64Url) + { + return utf8JsonWriter.WriteBase64StringValue(new BinaryDataExpression(value).ToArray(), valueSerialization.Format.ToFormatSpecifier()); + } + + return utf8JsonWriter.WriteBinaryData(binaryDataValue); + } + if (valueType == typeof(Stream)) + { + return utf8JsonWriter.WriteBinaryData(BinaryDataExpression.FromStream(value, false)); + } + + if (IsCustomJsonConverterAdded(valueType)) + { + return JsonSerializerExpression.Serialize(utf8JsonWriter, value).ToStatement(); + } + + throw new NotSupportedException($"Framework type {valueType} serialization not supported, please add `CodeGenMemberSerializationHooks` to specify the serialization of this type with the customized property"); + } + + private static MethodBodyStatement CheckCollectionItemForNull(Utf8JsonWriterExpression utf8JsonWriter, JsonSerialization valueSerialization, ValueExpression value) + => CollectionItemRequiresNullCheckInSerialization(valueSerialization) + ? new IfStatement(Equal(value, Null)) { utf8JsonWriter.WriteNullValue(), Continue } + : EmptyStatement; + + public static Method? BuildDeserialize(TypeDeclarationOptions declaration, JsonObjectSerialization serialization, INamedTypeSymbol? existingType) + { + var methodName = $"Deserialize{declaration.Name}"; + var signature = Configuration.UseModelReaderWriter ? + new MethodSignature(methodName, null, null, MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static, serialization.Type, null, new[] { KnownParameters.Serializations.JsonElement, KnownParameters.Serializations.OptionalOptions }) : + new MethodSignature(methodName, null, null, MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static, serialization.Type, null, new[] { KnownParameters.Serializations.JsonElement }); + if (SourceInputHelper.TryGetExistingMethod(existingType, signature, out _)) + { + return null; + } + + return Configuration.UseModelReaderWriter ? + new Method(signature, BuildDeserializeBody(serialization, new JsonElementExpression(KnownParameters.Serializations.JsonElement), new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.OptionalOptions)).ToArray()) : + new Method(signature, BuildDeserializeBody(serialization, new JsonElementExpression(KnownParameters.Serializations.JsonElement), null).ToArray()); + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable BuildDeserializeBody(JsonObjectSerialization serialization, JsonElementExpression jsonElement, ModelReaderWriterOptionsExpression? options) + { + // fallback to Default options if it is null + if (options != null) + { + yield return AssignIfNull(options, ModelReaderWriterOptionsExpression.Wire); + + yield return EmptyLine; + } + + if (!serialization.Type.IsValueType) // only return null for reference type (e.g. no enum) + { + yield return new IfStatement(jsonElement.ValueKindEqualsNull()) + { + Return(Null) + }; + } + + var discriminator = serialization.Discriminator; + if (discriminator is not null && discriminator.HasDescendants) + { + yield return new IfStatement(jsonElement.TryGetProperty(discriminator.SerializedName, out var discriminatorElement)) + { + new SwitchStatement(discriminatorElement.GetString(), GetDiscriminatorCases(jsonElement, discriminator, options).ToArray()) + }; + } + // we redirect the deserialization to the `DefaultObjectType` (the unknown version of the discriminated set) if possible. + // We could only do this when there is a discriminator, and the discriminator does not have a value (having a value indicating it is the child instead of base), and there is an unknown default object type to fall back, and I am not that fallback type. + if (discriminator is { Value: null, DefaultObjectType: { } defaultObjectType } && !serialization.Type.Equals(defaultObjectType.Type)) + { + yield return Return(GetDeserializeImplementation(discriminator.DefaultObjectType.Type.Implementation, jsonElement, options, null)); + } + else + { + yield return WriteObjectInitialization(serialization, jsonElement, options).ToArray(); + } + } + + // TODO -- make the options parameter non-nullable again when we remove the `use-model-reader-writer` flag + private static IEnumerable GetDiscriminatorCases(JsonElementExpression element, ObjectTypeDiscriminator discriminator, ModelReaderWriterOptionsExpression? options) + { + foreach (var implementation in discriminator.Implementations) + { + yield return new SwitchCase(Literal(implementation.Key), Return(GetDeserializeImplementation(implementation.Type.Implementation, element, options, null)), true); + } + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable WriteObjectInitialization(JsonObjectSerialization serialization, JsonElementExpression element, ModelReaderWriterOptionsExpression? options) + { + // this is the first level of object hierarchy + // collect all properties and initialize the dictionary + var propertyVariables = new Dictionary(); + + CollectPropertiesForDeserialization(propertyVariables, serialization); + + bool isThisTheDefaultDerivedType = serialization.Type.Equals(serialization.Discriminator?.DefaultObjectType?.Type); + + foreach (var variable in propertyVariables) + { + if (serialization.Discriminator?.SerializedName == variable.Key.SerializedName && + isThisTheDefaultDerivedType && + serialization.Discriminator.Value is not null && + (!serialization.Discriminator.Property.ValueType.IsEnum || serialization.Discriminator.Property.ValueType.Implementation is EnumType { IsExtensible: true })) + { + var defaultValue = serialization.Discriminator.Value.Value.Value?.ToString(); + yield return Declare(variable.Value, Literal(defaultValue)); + } + else + { + yield return Declare(variable.Value, Default); + } + } + + var shouldTreatEmptyStringAsNull = Configuration.ModelsToTreatEmptyStringAsNull.Contains(serialization.Type.Name); + var additionalProperties = serialization.AdditionalProperties; + DictionaryExpression? additionalPropertiesDictionary = null; + var rawDataField = serialization.RawDataField; + DictionaryExpression? rawDataFieldDictionary = null; + // if there is additional properties, we need to declare the dictionary first + if (additionalProperties != null) + { + yield return Declare("additionalPropertiesDictionary", + new DictionaryExpression(additionalProperties.ImplementationType.Arguments[0], additionalProperties.ImplementationType.Arguments[1], New.Instance(additionalProperties.ImplementationType)), + out additionalPropertiesDictionary); + } + // if there is raw data field, we need to declare the dictionary first + if (rawDataField != null) + { + yield return Declare("rawDataDictionary", + new DictionaryExpression(rawDataField.ImplementationType.Arguments[0], rawDataField.ImplementationType.Arguments[1], New.Instance(rawDataField.ImplementationType)), + out rawDataFieldDictionary); + } + + yield return new ForeachStatement("property", element.EnumerateObject(), out var property) + { + DeserializeIntoObjectProperties(serialization.Properties, new JsonPropertyExpression(property), additionalProperties, additionalPropertiesDictionary, rawDataField, rawDataFieldDictionary, propertyVariables, shouldTreatEmptyStringAsNull, options).ToArray() + }; + + if (additionalProperties != null && additionalPropertiesDictionary != null) + { + yield return Assign(propertyVariables[additionalProperties], additionalPropertiesDictionary); + } + if (rawDataField != null && rawDataFieldDictionary != null) + { + yield return Assign(propertyVariables[rawDataField], rawDataFieldDictionary); + } + + var parameterValues = propertyVariables.ToDictionary(v => v.Key.SerializationConstructorParameterName, v => GetOptional(v.Key, v.Value)); + var parameters = serialization.ConstructorParameters + .Select(p => parameterValues[p.Name]) + .ToArray(); + + yield return Return(New.Instance(serialization.Type, parameters)); + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable DeserializeIntoObjectProperties( + IEnumerable propertySerializations, JsonPropertyExpression jsonProperty, + JsonAdditionalPropertiesSerialization? additionalProperties, DictionaryExpression? additionalPropertiesDictionary, + JsonAdditionalPropertiesSerialization? rawDataField, DictionaryExpression? rawDataFieldDictionary, + IReadOnlyDictionary propertyVariables, + bool shouldTreatEmptyStringAsNull, + ModelReaderWriterOptionsExpression? options) + { + yield return DeserializeIntoObjectProperties(propertySerializations, jsonProperty, propertyVariables, shouldTreatEmptyStringAsNull, options); + + if (additionalProperties != null && additionalPropertiesDictionary != null) + { + var valueSerialization = additionalProperties.ValueSerialization ?? throw new InvalidOperationException("ValueSerialization of AdditionalProperties property should never be null"); + var deserializationStatement = TryDeserializeValue(valueSerialization, jsonProperty.Value, options, out var value); + + var additionalPropertiesStatement = additionalPropertiesDictionary.Add(jsonProperty.Name, value); + additionalPropertiesStatement = Serializations.WrapInCheckNotWire( + additionalProperties.ShouldExcludeInWireDeserialization, + options?.Format, + additionalPropertiesStatement); + + if (deserializationStatement is IfStatement tryDeserializationStatement) + { + // when we have a verifier, the statement is a if statement, the following value processing statements should be wrapped by this if statement + tryDeserializationStatement.Add(additionalPropertiesStatement); + // we also need to add "continue" to let the raw data field be skipped + tryDeserializationStatement.Add(Continue); + yield return tryDeserializationStatement; + } + else + { + // when we do not have a verifier, the statement is not a if statement + yield return deserializationStatement; + yield return additionalPropertiesStatement; + } + } + + if (rawDataField != null && rawDataFieldDictionary != null) + { + var valueSerialization = rawDataField.ValueSerialization ?? throw new InvalidOperationException("ValueSerialization of raw data field should never be null"); + yield return DeserializeValue(valueSerialization, jsonProperty.Value, options, out var value); + var rawDataFieldStatement = rawDataFieldDictionary.Add(jsonProperty.Name, value); + + if (Configuration.EnableInternalRawData) + { + rawDataFieldStatement = new MethodBodyStatement[] + { + AssignIfNull(rawDataFieldDictionary, New.Instance(rawDataField.ImplementationType)), + rawDataFieldStatement + }; + } + + yield return Serializations.WrapInCheckNotWire( + rawDataField.ShouldExcludeInWireDeserialization, + options?.Format, + rawDataFieldStatement); + } + } + + private static MethodBodyStatement DeserializeIntoObjectProperties(IEnumerable propertySerializations, JsonPropertyExpression jsonProperty, IReadOnlyDictionary propertyVariables, bool shouldTreatEmptyStringAsNull, ModelReaderWriterOptionsExpression? options) + => propertySerializations + .Select(p => new IfStatement(jsonProperty.NameEquals(p.SerializedName)) + { + DeserializeIntoObjectProperty(p, jsonProperty, propertyVariables, shouldTreatEmptyStringAsNull, options) + }) + .ToArray(); + + private static MethodBodyStatement DeserializeIntoObjectProperty(JsonPropertySerialization jsonPropertySerialization, JsonPropertyExpression jsonProperty, IReadOnlyDictionary propertyVariables, bool shouldTreatEmptyStringAsNull, ModelReaderWriterOptionsExpression? options) + { + // write the deserialization hook + if (jsonPropertySerialization.CustomDeserializationMethodName is { } deserializationMethodName) + { + return new[] + { + InvokeCustomDeserializationMethod(deserializationMethodName, jsonProperty, propertyVariables[jsonPropertySerialization].Declaration), + Continue + }; + } + + // Reading a property value + if (jsonPropertySerialization.ValueSerialization is not null) + { + return new[] + { + CreatePropertyNullCheckStatement(jsonPropertySerialization, jsonProperty, propertyVariables, shouldTreatEmptyStringAsNull), + DeserializeValue(jsonPropertySerialization.ValueSerialization, jsonProperty.Value, options, out var value), + Assign(propertyVariables[jsonPropertySerialization], value), + Continue + }; + } + + // Reading a nested object + if (jsonPropertySerialization.PropertySerializations is not null) + { + return new[] + { + CreatePropertyNullCheckStatement(jsonPropertySerialization, jsonProperty, propertyVariables, shouldTreatEmptyStringAsNull), + new ForeachStatement("property", jsonProperty.Value.EnumerateObject(), out var nestedItemVariable) + { + DeserializeIntoObjectProperties(jsonPropertySerialization.PropertySerializations, new JsonPropertyExpression(nestedItemVariable), propertyVariables, shouldTreatEmptyStringAsNull, options) + }, + Continue + }; + } + + throw new InvalidOperationException($"Either {nameof(JsonPropertySerialization.ValueSerialization)} must not be null or {nameof(JsonPropertySerialization.PropertySerializations)} must not be null."); + } + + private static MethodBodyStatement CreatePropertyNullCheckStatement(JsonPropertySerialization jsonPropertySerialization, JsonPropertyExpression jsonProperty, IReadOnlyDictionary propertyVariables, bool shouldTreatEmptyStringAsNull) + { + var checkEmptyProperty = GetCheckEmptyPropertyValueExpression(jsonProperty, jsonPropertySerialization, shouldTreatEmptyStringAsNull); + var serializedType = jsonPropertySerialization.SerializedType; + if (serializedType?.IsNullable == true) + { + var propertyVariable = propertyVariables[jsonPropertySerialization]; + + // we only assign null when it is not a collection if we have DeserializeNullCollectionAsNullValue configuration is off + // specially when it is required, we assign ChangeTrackingList because for optional lists we are already doing that + if (!serializedType.IsCollection || Configuration.DeserializeNullCollectionAsNullValue) + { + return new IfStatement(checkEmptyProperty) + { + Assign(propertyVariable, Null), + Continue + }; + } + + if (jsonPropertySerialization.IsRequired && !propertyVariable.Type.IsValueType) + { + return new IfStatement(checkEmptyProperty) + { + Assign(propertyVariable, New.Instance(propertyVariable.Type.PropertyInitializationType)), + Continue + }; + } + + return new IfStatement(checkEmptyProperty) + { + Continue + }; + } + + var propertyType = jsonPropertySerialization.ValueSerialization?.Type; + // even if ReadOnlyMemory is required we leave the list empty if the payload doesn't have it + if (jsonPropertySerialization.IsRequired && (propertyType is null || !propertyType.IsReadOnlyMemory)) + { + return EmptyStatement; + } + + if (propertyType?.Equals(typeof(JsonElement)) == true || // JsonElement handles nulls internally + propertyType?.Equals(typeof(string)) == true) //https://github.com/Azure/autorest.csharp/issues/922 + { + return EmptyStatement; + } + + if (jsonPropertySerialization.PropertySerializations is null) + { + return new IfStatement(checkEmptyProperty) + { + Continue + }; + } + + return new IfStatement(checkEmptyProperty) + { + jsonProperty.ThrowNonNullablePropertyIsNull(), + Continue + }; + } + + private static BoolExpression GetCheckEmptyPropertyValueExpression(JsonPropertyExpression jsonProperty, JsonPropertySerialization jsonPropertySerialization, bool shouldTreatEmptyStringAsNull) + { + var jsonElement = jsonProperty.Value; + if (!shouldTreatEmptyStringAsNull) + { + return jsonElement.ValueKindEqualsNull(); + } + + if (jsonPropertySerialization.ValueSerialization is not JsonValueSerialization { Type.IsFrameworkType: true } valueSerialization) + { + return jsonElement.ValueKindEqualsNull(); + } + + if (!Configuration.IntrinsicTypesToTreatEmptyStringAsNull.Contains(valueSerialization.Type.FrameworkType.Name)) + { + return jsonElement.ValueKindEqualsNull(); + } + + return Or(jsonElement.ValueKindEqualsNull(), And(jsonElement.ValueKindEqualsString(), Equal(jsonElement.GetString().Length, Int(0)))); + + } + + /// + /// Collects all the properties, additional properties property, raw data field for deserialization + /// + /// + /// + private static void CollectPropertiesForDeserialization(IDictionary propertyVariables, JsonObjectSerialization serialization) + { + CollectPropertiesForDeserialization(propertyVariables, serialization.Properties); + + if (serialization.AdditionalProperties is { } additionalProperties) + { + propertyVariables.Add(additionalProperties, new VariableReference(additionalProperties.Value.Type, additionalProperties.SerializationConstructorParameterName)); + } + + if (serialization.RawDataField is { } rawDataField) + { + propertyVariables.Add(rawDataField, new VariableReference(rawDataField.Value.Type, rawDataField.SerializationConstructorParameterName)); + } + } + + /// + /// Collects a list of properties being read from all level of object hierarchy + /// + /// + /// + private static void CollectPropertiesForDeserialization(IDictionary propertyVariables, IEnumerable jsonProperties) + { + foreach (JsonPropertySerialization jsonProperty in jsonProperties) + { + if (jsonProperty.ValueSerialization?.Type is { } variableType) + { + var propertyDeclaration = new CodeWriterDeclaration(jsonProperty.SerializedName.ToVariableName()); + propertyVariables.Add(jsonProperty, new VariableReference(variableType, propertyDeclaration)); + } + else if (jsonProperty.PropertySerializations != null) + { + CollectPropertiesForDeserialization(propertyVariables, jsonProperty.PropertySerializations); + } + } + } + + public static MethodBodyStatement BuildDeserializationForMethods(JsonSerialization serialization, bool async, ValueExpression? variable, StreamExpression stream, bool isBinaryData, ModelReaderWriterOptionsExpression? options) + { + if (isBinaryData) + { + var callFromStream = BinaryDataExpression.FromStream(stream, async); + var variableExpression = variable is not null ? new BinaryDataExpression(variable) : null; + return AssignOrReturn(variableExpression, callFromStream); + } + + var declareDocument = UsingVar("document", JsonDocumentExpression.Parse(stream, async), out var document); + var deserializeValueBlock = DeserializeValue(serialization, document.RootElement, options, out var value); + + if (!serialization.IsNullable) + { + return new[] { declareDocument, deserializeValueBlock, AssignOrReturn(variable, value) }; + } + + return new MethodBodyStatement[] + { + declareDocument, + new IfElseStatement + ( + document.RootElement.ValueKindEqualsNull(), + AssignOrReturn(variable, Null), + new[]{deserializeValueBlock, AssignOrReturn(variable, value)} + ) + }; + } + + // TODO -- make options parameter non-nullable again when we remove the `use-model-reader-writer` flag + public static MethodBodyStatement TryDeserializeValue(JsonSerialization serialization, JsonElementExpression element, ModelReaderWriterOptionsExpression? options, out ValueExpression value) + { + if (serialization.Type is { IsFrameworkType: true, FrameworkType: { } frameworkType } && BuilderHelpers.IsVerifiableType(frameworkType)) + { + if (frameworkType == typeof(string)) + { + DeserializeValue(serialization, element, options, out value); + return new IfStatement(Or(element.ValueKindEqualsString(), element.ValueKindEqualsNull())); + } + else if (frameworkType == typeof(bool)) + { + DeserializeValue(serialization, element, options, out value); + var valueKind = element.ValueKind; + return new IfStatement(Or(Equal(valueKind, JsonValueKindExpression.True), Equal(valueKind, JsonValueKindExpression.False))); + } + else + { + var declarationExpression = new DeclarationExpression(frameworkType, "value", out var variable, isOut: true); + value = variable; + return new IfStatement(new BoolExpression(element.Invoke($"TryGet{frameworkType.Name}", declarationExpression))); + } + } + else if (serialization.Type.IsList) + { + return new IfStatement(Equal(element.ValueKind, JsonValueKindExpression.Array)) + { + DeserializeValue(serialization, element, options, out value) + }; + } + else if (serialization.Type.IsDictionary) + { + return new IfStatement(Equal(element.ValueKind, JsonValueKindExpression.Object)) + { + DeserializeValue(serialization, element, options, out value) + }; + } + else + { + return DeserializeValue(serialization, element, options, out value); + } + } + + // TODO -- make options parameter non-nullable again when we remove the `use-model-reader-writer` flag + public static MethodBodyStatement DeserializeValue(JsonSerialization serialization, JsonElementExpression element, ModelReaderWriterOptionsExpression? options, out ValueExpression value) + { + switch (serialization) + { + case JsonArraySerialization jsonReadOnlyMemory when jsonReadOnlyMemory.Type.IsReadOnlyMemory: + var array = new VariableReference(jsonReadOnlyMemory.Type.InitializationType, "array"); + var index = new VariableReference(typeof(int), "index"); + var deserializeReadOnlyMemory = new MethodBodyStatement[] + { + Declare(index, Int(0)), + Declare(array, New.Array(jsonReadOnlyMemory.ValueSerialization.Type, element.GetArrayLength())), + new ForeachStatement("item", element.EnumerateArray(), out var item) + { + DeserializeArrayItemIntoArray(jsonReadOnlyMemory.ValueSerialization, new ArrayElementExpression(array, index), new JsonElementExpression(item), options), + Increment(index) + } + }; + value = New.Instance(jsonReadOnlyMemory.Type, array); + return deserializeReadOnlyMemory; + + case JsonArraySerialization arraySerialization: + var deserializeArrayStatement = new MethodBodyStatement[] + { + Declare("array", New.List(arraySerialization.ValueSerialization.Type), out var list), + new ForeachStatement("item", element.EnumerateArray(), out var arrayItem) + { + DeserializeArrayItemIntoList(arraySerialization.ValueSerialization, list, new JsonElementExpression(arrayItem), options) + } + }; + value = list; + return deserializeArrayStatement; + + case JsonDictionarySerialization jsonDictionary: + var deserializeDictionaryStatement = new MethodBodyStatement[] + { + Declare("dictionary", New.Dictionary(jsonDictionary.Type.Arguments[0], jsonDictionary.Type.Arguments[1]), out var dictionary), + new ForeachStatement("property", element.EnumerateObject(), out var property) + { + DeserializeDictionaryValue(jsonDictionary.ValueSerialization, dictionary, new JsonPropertyExpression(property), options) + } + }; + value = dictionary; + return deserializeDictionaryStatement; + + case JsonValueSerialization { Options: JsonSerializationOptions.UseManagedServiceIdentityV3 } valueSerialization: + var declareSerializeOptions = Var("serializeOptions", New.JsonSerializerOptions(), out var serializeOptions); + value = GetDeserializeValueExpression(element, valueSerialization.Type, options, valueSerialization.Format, serializeOptions); + return declareSerializeOptions; + + case JsonValueSerialization valueSerialization: + value = GetDeserializeValueExpression(element, valueSerialization.Type, options, valueSerialization.Format); + return EmptyStatement; + + default: + throw new InvalidOperationException($"{serialization.GetType()} is not supported."); + } + } + + private static MethodBodyStatement DeserializeArrayItemIntoList(JsonSerialization serialization, ListExpression listVariable, JsonElementExpression arrayItemVariable, ModelReaderWriterOptionsExpression? options) + => NullCheckCollectionItemIfRequired(serialization, arrayItemVariable, listVariable.Add(Null), new[] + { + DeserializeValue(serialization, arrayItemVariable, options, out var value), + listVariable.Add(value), + }); + + private static MethodBodyStatement DeserializeArrayItemIntoArray(JsonSerialization serialization, ArrayElementExpression arrayElement, JsonElementExpression arrayItemVariable, ModelReaderWriterOptionsExpression? options) + => NullCheckCollectionItemIfRequired(serialization, arrayItemVariable, Assign(arrayElement, Null), new[] + { + DeserializeValue(serialization, arrayItemVariable, options, out var value), + Assign(arrayElement, value), + }); + + private static MethodBodyStatement NullCheckCollectionItemIfRequired(JsonSerialization serialization, JsonElementExpression arrayItemVariable, MethodBodyStatement assignNull, MethodBodyStatement deserializeValue) + => CollectionItemRequiresNullCheckInSerialization(serialization) + ? new IfElseStatement(arrayItemVariable.ValueKindEqualsNull(), assignNull, deserializeValue) + : deserializeValue; + + private static MethodBodyStatement DeserializeDictionaryValue(JsonSerialization serialization, DictionaryExpression dictionary, JsonPropertyExpression property, ModelReaderWriterOptionsExpression? options) + { + var deserializeValueBlock = new[] + { + DeserializeValue(serialization, property.Value, options, out var value), + dictionary.Add(property.Name, value) + }; + + if (CollectionItemRequiresNullCheckInSerialization(serialization)) + { + return new IfElseStatement + ( + property.Value.ValueKindEqualsNull(), + dictionary.Add(property.Name, Null), + deserializeValueBlock + ); + } + + return deserializeValueBlock; + } + + public static ValueExpression GetDeserializeValueExpression(JsonElementExpression element, CSharpType serializationType, ModelReaderWriterOptionsExpression? options, SerializationFormat serializationFormat = SerializationFormat.Default, ValueExpression? serializerOptions = null) + { + if (serializationType.SerializeAs != null) + { + return new CastExpression(GetFrameworkTypeValueExpression(serializationType.SerializeAs, element, serializationFormat, serializationType), serializationType); + } + + if (serializationType.IsFrameworkType) + { + var frameworkType = serializationType.FrameworkType; + if (frameworkType == typeof(Nullable<>)) + { + frameworkType = serializationType.Arguments[0].FrameworkType; + } + + return GetFrameworkTypeValueExpression(frameworkType, element, serializationFormat, serializationType); + } + + return GetDeserializeImplementation(serializationType.Implementation, element, options, serializerOptions); + } + + private static ValueExpression GetDeserializeImplementation(TypeProvider implementation, JsonElementExpression element, ModelReaderWriterOptionsExpression? options, ValueExpression? serializerOptions) + { + switch (implementation) + { + case SystemObjectType systemObjectType when IsCustomJsonConverterAdded(systemObjectType.SystemType): + return JsonSerializerExpression.Deserialize(element, implementation.Type, serializerOptions); + + case Resource { ResourceData: SerializableObjectType resourceDataType } resource: + return New.Instance(resource.Type, new MemberExpression(null, "Client"), SerializableObjectTypeExpression.Deserialize(resourceDataType, element)); + + case MgmtObjectType mgmtObjectType when TypeReferenceTypeChooser.HasMatch(mgmtObjectType.InputModel): + return JsonSerializerExpression.Deserialize(element, implementation.Type); + + case SerializableObjectType type: + return SerializableObjectTypeExpression.Deserialize(type, element, options); + + case EnumType clientEnum: + var value = GetFrameworkTypeValueExpression(clientEnum.ValueType.FrameworkType, element, SerializationFormat.Default, null); + return EnumExpression.ToEnum(clientEnum, value); + + default: + throw new NotSupportedException($"No deserialization logic exists for {implementation.Declaration.Name}"); + } + } + + private static ValueExpression GetOptional(PropertySerialization jsonPropertySerialization, TypedValueExpression variable) + { + var sourceType = variable.Type; + if (!sourceType.IsFrameworkType || jsonPropertySerialization.SerializationConstructorParameterName == "serializedAdditionalRawData") + { + return variable; + } + else if (!jsonPropertySerialization.IsRequired) + { + return InvokeOptional.FallBackToChangeTrackingCollection(variable, jsonPropertySerialization.SerializedType); + } + + return variable; + } + + public static ValueExpression GetFrameworkTypeValueExpression(Type frameworkType, JsonElementExpression element, SerializationFormat format, CSharpType? serializationType) + { + if (frameworkType == typeof(ETag) || + frameworkType == typeof(Uri) || + frameworkType == typeof(ResourceIdentifier) || + frameworkType == typeof(ResourceType) || + frameworkType == typeof(ContentType) || + frameworkType == typeof(RequestMethod) || + frameworkType == typeof(AzureLocation) || + frameworkType == typeof(ExtendedLocationType)) + { + return New.Instance(frameworkType, element.GetString()); + } + + if (frameworkType == typeof(IPAddress)) + { + return new InvokeStaticMethodExpression(typeof(IPAddress), nameof(IPAddress.Parse), new[] { element.GetString() }); + } + + if (frameworkType == typeof(BinaryData)) + { + return format is SerializationFormat.Bytes_Base64 or SerializationFormat.Bytes_Base64Url + ? BinaryDataExpression.FromBytes(element.GetBytesFromBase64(format.ToFormatSpecifier())) + : BinaryDataExpression.FromString(element.GetRawText()); + } + + if (frameworkType == typeof(Stream)) + { + /* BinaryData.FromString(property.Value).ToStream() */ + return new BinaryDataExpression(BinaryDataExpression.FromString(element.GetRawText())).ToStream(); + } + + if (IsCustomJsonConverterAdded(frameworkType) && serializationType is not null) + { + return JsonSerializerExpression.Deserialize(element, serializationType); + } + + if (frameworkType == typeof(JsonElement)) + return element.InvokeClone(); + if (frameworkType == typeof(object)) + return element.GetObject(); + if (frameworkType == typeof(bool)) + return element.GetBoolean(); + if (frameworkType == typeof(char)) + return element.GetChar(); + if (IsIntType(frameworkType)) + return GetIntTypeDeserializationValueExpression(element, frameworkType, format); + if (frameworkType == typeof(float)) + return element.GetSingle(); + if (frameworkType == typeof(double)) + return element.GetDouble(); + if (frameworkType == typeof(decimal)) + return element.GetDecimal(); + if (frameworkType == typeof(string)) + return element.GetString(); + if (frameworkType == typeof(Guid)) + return element.GetGuid(); + if (frameworkType == typeof(byte[])) + return element.GetBytesFromBase64(format.ToFormatSpecifier()); + + if (frameworkType == typeof(DateTimeOffset)) + { + return format == SerializationFormat.DateTime_Unix + ? DateTimeOffsetExpression.FromUnixTimeSeconds(element.GetInt64()) + : element.GetDateTimeOffset(format.ToFormatSpecifier()); + } + + if (frameworkType == typeof(DateTime)) + return element.GetDateTime(); + if (frameworkType == typeof(TimeSpan)) + { + if (format is SerializationFormat.Duration_Seconds) + { + return TimeSpanExpression.FromSeconds(element.GetInt32()); + } + + if (format is SerializationFormat.Duration_Seconds_Float or SerializationFormat.Duration_Seconds_Double) + { + return TimeSpanExpression.FromSeconds(element.GetDouble()); + } + + return element.GetTimeSpan(format.ToFormatSpecifier()); + } + + throw new NotSupportedException($"Framework type {frameworkType} is not supported, please add `CodeGenMemberSerializationHooks` to specify the serialization of this type with the customized property"); + } + + private static ValueExpression GetIntTypeDeserializationValueExpression(JsonElementExpression element, Type type, SerializationFormat format) => format switch + { + SerializationFormat.Int_String => new TypeReference(type).Invoke(nameof(int.Parse), new List { element.GetString() }), + _ => type switch + { + Type t when t == typeof(sbyte) => element.GetSByte(), + Type t when t == typeof(byte) => element.GetByte(), + Type t when t == typeof(short) => element.GetInt16(), + Type t when t == typeof(int) => element.GetInt32(), + Type t when t == typeof(long) => element.GetInt64(), + _ => throw new NotSupportedException($"Framework type {type} is not int.") + } + }; + + private static bool IsIntType(Type frameworkType) => + frameworkType == typeof(sbyte) || + frameworkType == typeof(byte) || + frameworkType == typeof(short) || + frameworkType == typeof(int) || + frameworkType == typeof(long); + + private static MethodBodyStatement InvokeListAdd(ValueExpression list, ValueExpression value) + => new InvokeInstanceMethodStatement(list, nameof(List.Add), value); + + private static MethodBodyStatement InvokeArrayElementAssignment(ValueExpression array, ValueExpression index, ValueExpression value) + => Assign(new ArrayElementExpression(array, index), value); + + private static bool IsCustomJsonConverterAdded(Type type) + => type.GetCustomAttributes().Any(a => a.GetType() == typeof(JsonConverterAttribute)); + + public static bool CollectionItemRequiresNullCheckInSerialization(JsonSerialization serialization) => + serialization is { IsNullable: true } and JsonValueSerialization { Type: { IsValueType: true } } || // nullable value type, like int? + serialization is JsonArraySerialization or JsonDictionarySerialization || // list or dictionary + serialization is JsonValueSerialization jsonValueSerialization && + jsonValueSerialization is { Type: { IsValueType: false, IsFrameworkType: true } } && // framework reference type, e.g. byte[] + jsonValueSerialization.Type.FrameworkType != typeof(string) && // excluding string, because JsonElement.GetString() can handle null + jsonValueSerialization.Type.FrameworkType != typeof(byte[]); // excluding byte[], because JsonElement.GetBytesFromBase64() can handle null + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/MultipartSerializationMethodsBuilder.cs b/logger/autorest.csharp/common/Output/Builders/MultipartSerializationMethodsBuilder.cs new file mode 100644 index 0000000..e19b705 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/MultipartSerializationMethodsBuilder.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Serialization.Multipart; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Common.Output.Models.Types.HelperTypeProviders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Builders +{ + internal class MultipartSerializationMethodsBuilder + { + public static IEnumerable BuildMultipartSerializationMethods(MultipartObjectSerialization multipart) + { + /* private BinaryData SerializeMultipart(ModelReaderWriterOptions options) */ + yield return new Method( + new MethodSignature( + "SerializeMultipart", + null, + null, + MethodSignatureModifiers.Private, + typeof(BinaryData), + null, + new Parameter[] { KnownParameters.Serializations.Options }), + BuildMultipartSerializationMethodBody().ToArray()); + + /* internal virtual MultipartFormDataRequestContent ToMultipartRequestContent() */ + yield return new Method( + new MethodSignature( + Configuration.ApiTypes.ToMultipartRequestContentName, + null, + null, + MethodSignatureModifiers.Internal | MethodSignatureModifiers.Virtual, + MultipartFormDataRequestContentProvider.Instance.Type, + null, + Array.Empty()), + BuildToMultipartRequestContentMethodBody(multipart).ToArray()); + } + public static SwitchCase BuildMultipartWriteSwitchCase(MultipartObjectSerialization multipart, ModelReaderWriterOptionsExpression options) + { + return new SwitchCase( + Serializations.MultipartFormat, + Return(new InvokeInstanceMethodExpression(null, "SerializeMultipart", new[] { options }, null, false))); + } + + public static IEnumerable BuildMultipartSerializationMethodBody() + { + var serializeCallExpression = new InvokeInstanceMethodExpression(null, Configuration.ApiTypes.ToMultipartRequestContentName, Array.Empty(), null, false); + return new MethodBodyStatement[] + { + UsingDeclare("content", MultipartFormDataRequestContentProvider.Instance.Type, serializeCallExpression, out var content), + /*using MemoryStream stream = new MemoryStream();*/ + UsingDeclare("stream", typeof(MemoryStream), New.Instance(typeof(MemoryStream), Array.Empty()), out var stream), + /*content.WriteTo(stream, cancellationToken);*/ + MultipartFormDataRequestContentProvider.Instance.WriteTo(content, stream, null), + new IfElseStatement(GreaterThan(stream.Property("Position"), IntExpression.MaxValue), + /*return BinaryData.FromStream(stream);*/ + Return(BinaryDataExpression.FromStream(stream, false)), + /*return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position));*/ + Return(New.Instance(typeof(BinaryData), new[]{ new InvokeInstanceMethodExpression((new StreamExpression(stream)).GetBuffer, "AsMemory", new[] { Literal(0), new CastExpression(stream.Property("Position"), typeof(int)) }, null, false) })) + ), + }; + } + + public static IEnumerable BuildToMultipartRequestContentMethodBody(MultipartObjectSerialization? multipart) + { + return new MethodBodyStatement[] + { + Declare(MultipartFormDataRequestContentProvider.Instance.Type, "content", New.Instance(MultipartFormDataRequestContentProvider.Instance.Type), out var content), + WriteMultiParts(content, multipart!.Properties).ToArray(), + SerializeAdditionalProperties((VariableReference)content, multipart.AdditionalProperties), + Return(content) + }; + } + + private static IEnumerable WriteMultiParts(ValueExpression multipartContent, IEnumerable properties) + { + foreach (MultipartPropertySerialization property in properties) + { + if (property.SerializedType is { IsNullable: true }) + { + var checkPropertyIsInitialized = property.SerializedType.IsCollection && !property.SerializedType.IsReadOnlyMemory && property.IsRequired + ? And(NotEqual(property.Value, Null), InvokeOptional.IsCollectionDefined(property.Value)) + : NotEqual(property.Value, Null); + + yield return Serializations.WrapInCheckNotWire( + property.ShouldExcludeInWireSerialization, + Serializations.MultipartFormat, + InvokeOptional.WrapInIsDefined( + property, + new IfElseStatement(checkPropertyIsInitialized, + WritePropertySerialization(multipartContent, property), null) + )); + } + else + { + yield return Serializations.WrapInCheckNotWire( + property.ShouldExcludeInWireSerialization, + Serializations.MultipartFormat, + InvokeOptional.WrapInIsDefined(property, WritePropertySerialization(multipartContent, property))); + } + } + } + + private static MethodBodyStatement SerializeAdditionalProperties(ValueExpression multipartContent, MultipartAdditionalPropertiesSerialization? additionalProperties) + { + if (additionalProperties is null || additionalProperties.ShouldExcludeInWireSerialization) + { + return EmptyStatement; + } + var additionalPropertiesExpression = new DictionaryExpression(additionalProperties.Type.Arguments[0], additionalProperties.Type.Arguments[1], additionalProperties.Value); + MethodBodyStatement statement = new ForeachStatement("item", additionalPropertiesExpression, out KeyValuePairExpression item) + { + SerializationExression(multipartContent, additionalProperties.ValueSerialization, item.Value, new StringExpression(item.Key)) + }; + return statement; + } + + private static MethodBodyStatement WritePropertySerialization(ValueExpression mulitpartContent, MultipartPropertySerialization serialization) + { + return new[] + { + SerializationExression(mulitpartContent, serialization.ValueSerialization,serialization.Value, Literal(serialization.SerializedName)) + }; + } + + private static MethodBodyStatement SerializationExression(ValueExpression mulitpartContent, ObjectSerialization serialization, TypedValueExpression value, StringExpression name) => serialization switch + { + MultipartArraySerialization arraySerialization => SerializeArray(mulitpartContent, arraySerialization, value, name), + MultipartValueSerialization valueSerialization => SerializeValue(mulitpartContent, valueSerialization, value.NullableStructValue(), name), + MultipartDictionarySerialization dictionarySerialization => SerializeDictionary(mulitpartContent, dictionarySerialization, value.NullableStructValue(), name), + _ => throw new NotImplementedException($"{serialization.GetType()}") + }; + + private static MethodBodyStatement SerializeArray(ValueExpression mulitpartContent, MultipartArraySerialization serialization, ValueExpression value, StringExpression name) + { + return new[] + { + new ForeachStatement(serialization.Type.ElementType, "item", value, false, out var item) + { + SerializationExression(mulitpartContent, serialization.ValueSerialization, item, name) + } + }; + } + + private static MethodBodyStatement SerializeDictionary(ValueExpression mulitpartContent, MultipartDictionarySerialization serialization, ValueExpression value, StringExpression name) + { + var dictionaryExpression = new DictionaryExpression(serialization.Type.Arguments[0], serialization.Type.Arguments[1], value); + return new[] + { + new ForeachStatement("item", dictionaryExpression, out KeyValuePairExpression keyValuePair) + { + SerializationExression(mulitpartContent, serialization.ValueSerialization, keyValuePair.Value, new StringExpression(keyValuePair.Key)) + } + }; + } + + private static MethodBodyStatement SerializeValue(ValueExpression mulitpartContent, MultipartValueSerialization valueSerialization, ValueExpression valueExpression, StringExpression name) + { + ValueExpression? contentExpression = null; + ValueExpression? fileNameExpression = null; + ValueExpression? contentTypeExpression = null; + if (valueSerialization.Type.IsFrameworkType && valueSerialization.Type.FrameworkType == typeof(Stream)) + { + contentExpression = BuildValueSerizationExpression(valueSerialization.Type, valueExpression); + if (valueSerialization.FileName != null) + { + fileNameExpression = Literal(valueSerialization.FileName); + } + else + { + fileNameExpression = name; + } + if (valueSerialization.ContentType != null) + { + contentTypeExpression = Literal(valueSerialization.ContentType); + } + } + if (valueSerialization.Type.IsFrameworkType && valueSerialization.Type.FrameworkType == typeof(BinaryData)) + { + contentExpression = BuildValueSerizationExpression(valueSerialization.Type, valueExpression); + if (valueSerialization.FileName != null) + { + fileNameExpression = Literal(valueSerialization.FileName); + } + else + { + fileNameExpression = name; + } + if (valueSerialization.ContentType != null) + { + contentTypeExpression = Literal(valueSerialization.ContentType); + } + } + if (valueSerialization.Type.IsFrameworkType) + { + contentExpression = BuildValueSerizationExpression(valueSerialization.Type, valueExpression); + } + if (contentExpression != null) + { + return MultipartFormDataRequestContentProvider.Instance.Add(mulitpartContent, contentExpression, name, fileNameExpression, contentTypeExpression); + } + + switch (valueSerialization.Type.Implementation) + { + case SerializableObjectType serializableObjectType: + contentExpression = BuildValueSerizationExpression(serializableObjectType.Type, valueExpression); + break; + case EnumType { IsIntValueType: true, IsExtensible: false } enumType: + contentExpression = BuildValueSerizationExpression(typeof(int), new CastExpression(valueExpression, enumType.ValueType)); + break; + case EnumType enumType: + contentExpression = BuildValueSerizationExpression(enumType.ValueType, new EnumExpression(enumType, valueExpression).ToSerial()); + break; + default: + throw new NotSupportedException($"Cannot build serialization expression for type {valueSerialization.Type}, please add `CodeGenMemberSerializationHooks` to specify the serialization of this type with the customized property"); + } + return MultipartFormDataRequestContentProvider.Instance.Add(mulitpartContent, contentExpression, name, fileNameExpression, contentTypeExpression); + } + + private static ValueExpression BuildValueSerizationExpression(CSharpType valueType, ValueExpression valueExpression) => valueType switch + { + { IsFrameworkType: true } => valueExpression, + { IsFrameworkType: false, Implementation: SerializableObjectType serializableObjectType } => new InvokeStaticMethodExpression(typeof(ModelReaderWriter), nameof(ModelReaderWriter.Write), new[] { valueExpression, ModelReaderWriterOptionsExpression.Wire }, new[] { valueType }), + _ => BinaryDataExpression.FromObjectAsJson(valueExpression) + }; + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/SerializationBuilder.cs b/logger/autorest.csharp/common/Output/Builders/SerializationBuilder.cs new file mode 100644 index 0000000..36b39c6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/SerializationBuilder.cs @@ -0,0 +1,755 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Serialization.Multipart; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.ResourceManager.Models; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Builders +{ + internal class SerializationBuilder + { + public static SerializationFormat GetDefaultSerializationFormat(CSharpType type) + { + if (type.EqualsIgnoreNullable(typeof(byte[]))) + { + return SerializationFormat.Bytes_Base64; + } + + if (type.EqualsIgnoreNullable(typeof(DateTimeOffset))) + { + return SerializationFormat.DateTime_ISO8601; + } + + if (type.EqualsIgnoreNullable(typeof(TimeSpan))) + { + return SerializationFormat.Duration_ISO8601; + } + + return SerializationFormat.Default; + } + + public static SerializationFormat GetSerializationFormat(InputType type) => type switch + { + InputLiteralType literalType => GetSerializationFormat(literalType.ValueType), + InputListType listType => GetSerializationFormat(listType.ValueType), + InputDictionaryType dictionaryType => GetSerializationFormat(dictionaryType.ValueType), + InputNullableType nullableType => GetSerializationFormat(nullableType.Type), + InputDateTimeType dateTimeType => dateTimeType.Encode switch + { + DateTimeKnownEncoding.Rfc3339 => SerializationFormat.DateTime_RFC3339, + DateTimeKnownEncoding.Rfc7231 => SerializationFormat.DateTime_RFC7231, + DateTimeKnownEncoding.UnixTimestamp => SerializationFormat.DateTime_Unix, + _ => throw new IndexOutOfRangeException($"unknown encode {dateTimeType.Encode}"), + }, + InputDurationType durationType => durationType.Encode switch + { + // there is no such thing as `DurationConstant` + DurationKnownEncoding.Iso8601 => SerializationFormat.Duration_ISO8601, + DurationKnownEncoding.Seconds => durationType.WireType.Kind switch + { + InputPrimitiveTypeKind.Int32 => SerializationFormat.Duration_Seconds, + InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => SerializationFormat.Duration_Seconds_Float, + _ => SerializationFormat.Duration_Seconds_Double + }, + DurationKnownEncoding.Constant => SerializationFormat.Duration_Constant, + _ => throw new IndexOutOfRangeException($"unknown encode {durationType.Encode}") + }, + InputPrimitiveType primitiveType => primitiveType.Kind switch + { + InputPrimitiveTypeKind.PlainDate => SerializationFormat.Date_ISO8601, + InputPrimitiveTypeKind.PlainTime => SerializationFormat.Time_ISO8601, + InputPrimitiveTypeKind.Bytes => primitiveType.Encode switch + { + BytesKnownEncoding.Base64 => SerializationFormat.Bytes_Base64, + BytesKnownEncoding.Base64Url => SerializationFormat.Bytes_Base64Url, + null => SerializationFormat.Default, + _ => throw new IndexOutOfRangeException($"unknown encode {primitiveType.Encode}") + }, + InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.Int32 or InputPrimitiveTypeKind.Int16 or InputPrimitiveTypeKind.Int8 + or InputPrimitiveTypeKind.UInt64 or InputPrimitiveTypeKind.UInt32 or InputPrimitiveTypeKind.UInt16 or InputPrimitiveTypeKind.UInt8 + or InputPrimitiveTypeKind.Integer or InputPrimitiveTypeKind.SafeInt when primitiveType.Encode is "string" => SerializationFormat.Int_String, + _ => SerializationFormat.Default + }, + _ => SerializationFormat.Default + }; + + internal static SerializationFormat GetSerializationFormat(InputType inputType, CSharpType valueType) + { + var serializationFormat = GetSerializationFormat(inputType); + return serializationFormat != SerializationFormat.Default ? serializationFormat : GetDefaultSerializationFormat(valueType); + } + + public static ObjectSerialization Build(BodyMediaType bodyMediaType, InputType inputType, CSharpType type, SerializationFormat? serializationFormat) => bodyMediaType switch + { + BodyMediaType.Xml => BuildXmlElementSerialization(inputType, type, null, true), + BodyMediaType.Json => BuildJsonSerialization(inputType, type, false, serializationFormat ?? GetSerializationFormat(inputType, type)), + _ => throw new NotImplementedException(bodyMediaType.ToString()) + }; + + private static XmlElementSerialization BuildXmlElementSerialization(InputType inputType, CSharpType type, string? name, bool isRoot) + { + string xmlName = inputType.Serialization.Xml?.Name ?? name ?? inputType.Name; + switch (inputType) + { + case InputListType listType: + var wrapped = isRoot || listType.Serialization.Xml?.IsWrapped == true; + var arrayElement = BuildXmlElementSerialization(listType.ValueType, type.ElementType, null, false); + return new XmlArraySerialization(type.InitializationType, arrayElement, xmlName, wrapped); + case InputDictionaryType dictionaryType: + var valueElement = BuildXmlElementSerialization(dictionaryType.ValueType, type.ElementType, null, false); + return new XmlDictionarySerialization(type.InitializationType, valueElement, xmlName); + case InputNullableType nullableType: + return BuildXmlElementSerialization(nullableType.Type, type, name, isRoot); + default: + return new XmlElementValueSerialization(xmlName, new XmlValueSerialization(type, GetSerializationFormat(inputType))); + } + } + + private static bool IsManagedServiceIdentityV3(InputType schema, CSharpType type) + { + // If the type is the common type ManagedServiceIdentity and the schema contains a type property with sealed enum or extensible enum schema which has a choice of v3 "SystemAssigned,UserAssigned" value, + // then this is a v3 version of ManagedServiceIdentity. + if (type is not { IsFrameworkType: false, Implementation: SystemObjectType systemObjectType } || systemObjectType.SystemType != typeof(ManagedServiceIdentity) || schema is not InputModelType objectSchema) + { + return false; + } + + foreach (var m in objectSchema.GetSelfAndBaseModels()) + { + foreach (var property in m.Properties) + { + if (property is { SerializedName: "type", Type: InputEnumType choice } && choice.Values.Any(c => c.Value.ToString() == ManagedServiceIdentityTypeV3Converter.SystemAssignedUserAssignedV3Value)) + { + return true; + } + } + } + + return false; + } + + public static JsonSerialization BuildJsonSerialization(InputType inputType, CSharpType valueType, bool isCollectionElement, SerializationFormat serializationFormat) + { + if (valueType.IsFrameworkType && valueType.FrameworkType == typeof(JsonElement)) + { + return new JsonValueSerialization(valueType, serializationFormat, valueType.IsNullable || isCollectionElement); + } + + return inputType switch + { + InputListType listType => new JsonArraySerialization(valueType, BuildJsonSerialization(listType.ValueType, valueType.ElementType, true, serializationFormat), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)), + InputDictionaryType dictionaryType => new JsonDictionarySerialization(valueType, BuildJsonSerialization(dictionaryType.ValueType, valueType.ElementType, true, serializationFormat), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)), + InputNullableType nullableType => BuildJsonSerialization(nullableType.Type, valueType, isCollectionElement, serializationFormat), + _ => + Configuration.AzureArm + ? new JsonValueSerialization(valueType, serializationFormat, valueType.IsNullable || (isCollectionElement && !valueType.IsValueType), IsManagedServiceIdentityV3(inputType, valueType) ? JsonSerializationOptions.UseManagedServiceIdentityV3 : JsonSerializationOptions.None) + : new JsonValueSerialization(valueType, serializationFormat, valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)) // nullable CSharp type like int?, Etag?, and reference type in collection + }; + } + + public static JsonSerialization BuildJsonSerialization(InputType inputType, CSharpType valueType, bool isCollectionElement) + => BuildJsonSerialization(inputType, valueType, isCollectionElement, GetSerializationFormat(inputType, valueType)); + + private static JsonSerialization BuildJsonSerializationFromValue(CSharpType valueType, bool isCollectionElement) + { + if (valueType.IsList) + { + return new JsonArraySerialization(valueType, BuildJsonSerializationFromValue(valueType.ElementType, true), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)); + } + + if (valueType.IsDictionary) + { + var dictionaryValueType = valueType.Arguments[1]; + return new JsonDictionarySerialization(valueType, BuildJsonSerializationFromValue(dictionaryValueType, true), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)); + } + + return new JsonValueSerialization(valueType, GetDefaultSerializationFormat(valueType), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)); + } + + public static XmlObjectSerialization BuildXmlObjectSerialization(string serializationName, SerializableObjectType model, TypeFactory typeFactory) + { + var elements = new List(); + var attributes = new List(); + var embeddedArrays = new List(); + XmlObjectContentSerialization? contentSerialization = null; + + foreach (var objectTypeLevel in model.EnumerateHierarchy()) + { + foreach (ObjectTypeProperty objectProperty in objectTypeLevel.Properties) + { + if (IsSerializable(objectProperty, typeFactory, out var isAttribute, out var isContent, out var format, out var serializedName, out var serializedType)) + { + if (isContent) + { + contentSerialization = new XmlObjectContentSerialization(serializedName, serializedType, objectProperty, new XmlValueSerialization(objectProperty.Declaration.Type, format)); + } + else if (isAttribute) + { + attributes.Add(new XmlObjectAttributeSerialization(serializedName, serializedType, objectProperty, new XmlValueSerialization(objectProperty.Declaration.Type, format))); + } + else + { + var valueSerialization = objectProperty.InputModelProperty is { } inputModelProperty + ? BuildXmlElementSerialization(inputModelProperty.Type, objectProperty.Declaration.Type, serializedName, false) + : BuildXmlElementSerialization(objectProperty.InputModelProperty!.Type, objectProperty.Declaration.Type, serializedName, false); + + if (valueSerialization is XmlArraySerialization arraySerialization) + { + embeddedArrays.Add(new XmlObjectArraySerialization(serializedName, serializedType, objectProperty, arraySerialization)); + } + else + { + elements.Add(new XmlObjectElementSerialization(serializedName, serializedType, objectProperty, valueSerialization)); + } + } + } + } + } + + return new XmlObjectSerialization(serializationName, model, elements.ToArray(), attributes.ToArray(), embeddedArrays.ToArray(), contentSerialization); + + static bool IsSerializable(ObjectTypeProperty objectProperty, TypeFactory typeFactory, out bool isAttribute, out bool isContent, out SerializationFormat format, out string serializedName, out CSharpType serializedType) + { + if (objectProperty.InputModelProperty is { } inputModelProperty) + { + isAttribute = inputModelProperty.Type.Serialization.Xml?.IsAttribute == true; + isContent = inputModelProperty.Type.Serialization.Xml?.IsContent == true; + format = GetSerializationFormat(inputModelProperty.Type); + serializedName = inputModelProperty.SerializedName; + serializedType = typeFactory.CreateType(inputModelProperty.Type); + return true; + } + + if (objectProperty.InputModelProperty is { } property) + { + isAttribute = property.Type.Serialization?.Xml?.IsAttribute == true; + isContent = property.Type.Serialization?.Xml?.IsContent == true; + format = GetSerializationFormat(property.Type); + serializedName = property.SerializedName; + serializedType = objectProperty.ValueType; + return true; + } + + isAttribute = false; + isContent = false; + format = default; + serializedName = string.Empty; + serializedType = typeof(object); + return false; + } + } + + public BicepObjectSerialization? BuildBicepObjectSerialization(SerializableObjectType objectType, JsonObjectSerialization jsonObjectSerialization) + => new BicepObjectSerialization(objectType, jsonObjectSerialization); + + private static JsonPropertySerialization? CreateJsonPropertySerializationFromInputModelProperty(SerializableObjectType objectType, ObjectTypeProperty property, TypeFactory typeFactory) + { + var declaredName = property.Declaration.Name; + var propertyType = property.Declaration.Type; + var name = declaredName.ToVariableName(); + var serializationMapping = objectType.GetForMemberSerialization(declaredName); + + if (property.InputModelProperty is not { } inputModelProperty) + { + // Property is not part of specification, + return new JsonPropertySerialization( + name, + new TypedMemberExpression(null, declaredName, propertyType), + serializationMapping?.SerializationPath?[^1] ?? name, + propertyType, + BuildJsonSerializationFromValue(propertyType, false), + property.IsRequired, + property.IsReadOnly, + property, + serializationHooks: new CustomSerializationHooks( + serializationMapping?.JsonSerializationValueHook, + serializationMapping?.JsonDeserializationValueHook, + serializationMapping?.BicepSerializationValueHook)); + } + + var valueSerialization = BuildJsonSerialization(inputModelProperty.Type, propertyType, false); + var serializedName = serializationMapping?.SerializationPath?[^1] ?? inputModelProperty.SerializedName; + var serializedType = typeFactory.CreateType(inputModelProperty.Type); + var memberValueExpression = new TypedMemberExpression(null, declaredName, propertyType); + + return new JsonPropertySerialization( + name, + memberValueExpression, + serializedName, + propertyType.WithNullable(propertyType.IsNullable ? serializedType.IsNullable : false), + valueSerialization, + property.IsRequired, + ShouldExcludeInWireSerialization(property, inputModelProperty), + property, + serializationHooks: new CustomSerializationHooks( + serializationMapping?.JsonSerializationValueHook, + serializationMapping?.JsonDeserializationValueHook, + serializationMapping?.BicepSerializationValueHook), + enumerableExpression: null); + } + + private static bool ShouldExcludeInWireSerialization(ObjectTypeProperty property, InputModelProperty inputProperty) + { + if (inputProperty.IsDiscriminator) + { + return false; + } + + if (property.InitializationValue is not null) + { + return false; + } + + return inputProperty.IsReadOnly; + } + + private static IEnumerable GetPropertySerializationsFromBag(PropertyBag propertyBag, Func jsonPropertySerializationFactory) + { + foreach (var property in propertyBag.Properties) + { + if (jsonPropertySerializationFactory(property) is { } serialization) + { + yield return serialization; + } + } + + foreach (var (name, innerBag) in propertyBag.Bag) + { + JsonPropertySerialization[] serializationProperties = GetPropertySerializationsFromBag(innerBag, jsonPropertySerializationFactory).ToArray(); + yield return new JsonPropertySerialization(name, serializationProperties); + } + } + + private IEnumerable GetPropertySerializationsFromBag(SerializationPropertyBag propertyBag, SchemaObjectType objectType) + { + foreach (var (property, serializationMapping) in propertyBag.Properties) + { + if (property.InputModelProperty == null) + { + // Property is not part of specification, + var declaredName = property.Declaration.Name; + var propertyType = property.Declaration.Type; + var varName = declaredName.ToVariableName(); + yield return new JsonPropertySerialization( + varName, + new TypedMemberExpression(null, property.Declaration.Name, propertyType), + serializationMapping?.SerializationPath?[^1] ?? varName, + propertyType, + BuildJsonSerializationFromValue(propertyType, false), + property.IsRequired, + property.IsReadOnly, + property, + serializationHooks: new CustomSerializationHooks( + serializationMapping?.JsonSerializationValueHook, + serializationMapping?.JsonDeserializationValueHook, + serializationMapping?.BicepSerializationValueHook)); + } + else + { + var schemaProperty = property.InputModelProperty; + var parameter = objectType.SerializationConstructor.FindParameterByInitializedProperty(property); + if (parameter is null) + { + throw new InvalidOperationException( + $"Serialization constructor of the type {objectType.Declaration.Name} has no parameter for {schemaProperty.SerializedName} input property"); + } + + var serializedName = serializationMapping?.SerializationPath?[^1] ?? schemaProperty.SerializedName; + var isRequired = schemaProperty.IsRequired; + var shouldExcludeInWireSerialization = !schemaProperty.IsDiscriminator && property.InitializationValue is null && schemaProperty.IsReadOnly; + var serialization = BuildJsonSerialization(schemaProperty.Type, property.Declaration.Type, false); + + var memberValueExpression = + new TypedMemberExpression(null, property.Declaration.Name, property.Declaration.Type); + TypedMemberExpression? enumerableExpression = null; + + yield return new JsonPropertySerialization( + parameter.Name, + memberValueExpression, + serializedName, + property.ValueType, + serialization, + isRequired, + shouldExcludeInWireSerialization, + property, + serializationHooks: new CustomSerializationHooks( + serializationMapping?.JsonSerializationValueHook, + serializationMapping?.JsonDeserializationValueHook, + serializationMapping?.BicepSerializationValueHook), + enumerableExpression: enumerableExpression); + } + } + + foreach ((string name, SerializationPropertyBag innerBag) in propertyBag.Bag) + { + JsonPropertySerialization[] serializationProperties = GetPropertySerializationsFromBag(innerBag, objectType).ToArray(); + yield return new JsonPropertySerialization(name, serializationProperties); + } + } + + public JsonObjectSerialization BuildJsonObjectSerialization(InputModelType inputModel, SchemaObjectType objectType) + { + var propertyBag = new SerializationPropertyBag(); + var selfPropertyBag = new SerializationPropertyBag(); + foreach (var objectTypeLevel in objectType.EnumerateHierarchy()) + { + foreach (var objectTypeProperty in objectTypeLevel.Properties) + { + if (objectTypeProperty == objectTypeLevel.AdditionalPropertiesProperty) + continue; + propertyBag.Properties.Add(objectTypeProperty, objectType.GetForMemberSerialization(objectTypeProperty.Declaration.Name)); + + if (objectTypeLevel == objectType) + { + selfPropertyBag.Properties.Add(objectTypeProperty, objectType.GetForMemberSerialization(objectTypeProperty.Declaration.Name)); + } + } + } + PopulatePropertyBag(propertyBag, 0); + PopulatePropertyBag(selfPropertyBag, 0); + + // properties: all the properties containing the properties from base, these are needed to build the constructor + // selfProperties: the properties not containing properties from base, to do the serialization we only need the properties owned by itself + var properties = GetPropertySerializationsFromBag(propertyBag, objectType).ToArray(); + var selfProperties = GetPropertySerializationsFromBag(selfPropertyBag, objectType).ToArray(); + var (additionalProperties, rawDataField) = CreateAdditionalPropertiesSerialization(inputModel, objectType); + return new JsonObjectSerialization(objectType, objectType.SerializationConstructor.Signature.Parameters, properties, selfProperties, additionalProperties, rawDataField, objectType.Discriminator, objectType.JsonConverter); + } + + public static IReadOnlyList GetPropertySerializations(ModelTypeProvider model, TypeFactory typeFactory, bool onlySelf = false) + => GetPropertySerializationsFromBag(PopulatePropertyBag(model, onlySelf), p => CreateJsonPropertySerializationFromInputModelProperty(model, p, typeFactory)).ToArray(); + + private class SerializationPropertyBag + { + public Dictionary Bag { get; } = new(); + public Dictionary Properties { get; } = new(); + } + + private static void PopulatePropertyBag(SerializationPropertyBag propertyBag, int depthIndex) + { + foreach (var (property, serializationMapping) in propertyBag.Properties.ToArray()) + { + IReadOnlyList? flattenedNames = serializationMapping?.SerializationPath ?? property.InputModelProperty!.FlattenedNames; + if (depthIndex >= (flattenedNames?.Count ?? 0) - 1) + { + continue; + } + + string name = flattenedNames!.ElementAt(depthIndex); + if (!propertyBag.Bag.TryGetValue(name, out SerializationPropertyBag? namedBag)) + { + namedBag = new SerializationPropertyBag(); + propertyBag.Bag.Add(name, namedBag); + } + + namedBag.Properties.Add(property, serializationMapping); + propertyBag.Properties.Remove(property); + } + + foreach (SerializationPropertyBag innerBag in propertyBag.Bag.Values) + { + PopulatePropertyBag(innerBag, depthIndex + 1); + } + } + + private class PropertyBag + { + public Dictionary> Bag { get; } = new(); + public List Properties { get; } = new(); + } + + private static PropertyBag PopulatePropertyBag(SerializableObjectType objectType, bool onlySelf = false) + { + var propertyBag = new PropertyBag(); + var objectTypeLevels = onlySelf ? objectType.AsIEnumerable() : objectType.EnumerateHierarchy(); + foreach (var objectTypeLevel in objectTypeLevels) + { + foreach (var objectTypeProperty in objectTypeLevel.Properties) + { + if (objectTypeProperty == objectTypeLevel.AdditionalPropertiesProperty) + { + continue; + } + + if (objectTypeLevel is SerializableObjectType serializableObjectType && objectTypeProperty == serializableObjectType.RawDataField) + { + continue; + } + + propertyBag.Properties.Add(objectTypeProperty); + } + } + + PopulatePropertyBag(propertyBag, (p, i) => GetPropertyNameAtDepth(objectType, p, i), 0); + return propertyBag; + } + + private static void PopulatePropertyBag(PropertyBag propertyBag, Func getPropertyNameAtDepth, int depthIndex) where T : class + { + var propertiesCopy = propertyBag.Properties.ToArray(); + foreach (var property in propertiesCopy) + { + var name = getPropertyNameAtDepth(property, depthIndex); + + if (name is null) + { + continue; + } + + if (!propertyBag.Bag.TryGetValue(name, out PropertyBag? namedBag)) + { + namedBag = new PropertyBag(); + propertyBag.Bag.Add(name, namedBag); + } + + namedBag.Properties.Add(property); + propertyBag.Properties.Remove(property); + } + + foreach (var innerBag in propertyBag.Bag.Values) + { + PopulatePropertyBag(innerBag, getPropertyNameAtDepth, depthIndex + 1); + } + } + + private static string? GetPropertyNameAtDepth(SerializableObjectType objectType, ObjectTypeProperty property, int depthIndex) + { + if (objectType.GetForMemberSerialization(property.Declaration.Name) is { SerializationPath: { } serializationPath } && serializationPath.Count > depthIndex + 1) + { + return serializationPath[depthIndex]; + } + + if (property.InputModelProperty is { FlattenedNames: { } schemaFlattenedNames } && schemaFlattenedNames.Count > depthIndex + 1) + { + return schemaFlattenedNames.ElementAt(depthIndex); + } + + if (property.InputModelProperty is { } inputModelProperty) + { + return GetPropertyNameAtDepth(inputModelProperty, depthIndex); + } + + return null; + } + + private static string? GetPropertyNameAtDepth(InputModelProperty property, int depthIndex) + => property is { FlattenedNames: { } inputFlattenedNames } && inputFlattenedNames.Count > depthIndex + 1 + ? inputFlattenedNames[depthIndex] + : null; + + private (JsonAdditionalPropertiesSerialization? AdditionalPropertiesSerialization, JsonAdditionalPropertiesSerialization? RawDataFieldSerialization) CreateAdditionalPropertiesSerialization(InputModelType inputModel, ObjectType objectType) + { + // collect additional properties and raw data field + ObjectTypeProperty? additionalPropertiesProperty = null; + ObjectTypeProperty? rawDataField = null; + + InputType? additionalPropertiesValueType = null; + foreach (var model in inputModel.GetSelfAndBaseModels()) + { + additionalPropertiesValueType = model.AdditionalProperties; + if (additionalPropertiesValueType != null) + break; + } + foreach (var obj in objectType.EnumerateHierarchy()) + { + additionalPropertiesProperty ??= obj.AdditionalPropertiesProperty; + rawDataField ??= (obj as SerializableObjectType)?.RawDataField; + } + + if (additionalPropertiesProperty == null && rawDataField == null) + { + return (null, null); + } + + // build serialization for additional properties property (if any) + var additionalPropertiesSerialization = BuildSerializationForAdditionalProperties(additionalPropertiesProperty, additionalPropertiesValueType, false); + // build serialization for raw data field (if any) + var rawDataFieldSerialization = BuildSerializationForAdditionalProperties(rawDataField, null, true); + + return (additionalPropertiesSerialization, rawDataFieldSerialization); + + static JsonAdditionalPropertiesSerialization? BuildSerializationForAdditionalProperties(ObjectTypeProperty? additionalPropertiesProperty, InputType? inputAdditionalPropertiesValueType, bool shouldExcludeInWireSerialization) + { + if (additionalPropertiesProperty == null) + { + return null; + } + + var additionalPropertyValueType = additionalPropertiesProperty.Declaration.Type.Arguments[1]; + JsonSerialization valueSerialization; + if (inputAdditionalPropertiesValueType is not null) + { + valueSerialization = BuildJsonSerialization(inputAdditionalPropertiesValueType, additionalPropertyValueType, false); + } + else + { + valueSerialization = new JsonValueSerialization(additionalPropertyValueType, SerializationFormat.Default, true); + } + + return new JsonAdditionalPropertiesSerialization( + additionalPropertiesProperty, + valueSerialization, + new CSharpType(typeof(Dictionary<,>), additionalPropertiesProperty.Declaration.Type.Arguments), + shouldExcludeInWireSerialization); + } + } + + public MultipartObjectSerialization BuildMultipartObjectSerialization(InputModelType inputModel, SchemaObjectType objectType) + { + /*TODO: This is a temporary implementation. We need to revisit this and make it more robust. + *Need to consider the polymorphism and the base class properties. + **/ + var properties = new List(); + foreach (ObjectTypeProperty property in objectType.Properties.ToArray()) + { + var schemaProperty = property.InputModelProperty!; // we ensure this is not null when we build the array + var parameter = objectType.SerializationConstructor.FindParameterByInitializedProperty(property); + if (parameter is null) + { + throw new InvalidOperationException($"Serialization constructor of the type {objectType.Declaration.Name} has no parameter for {schemaProperty.SerializedName} input property"); + } + + var serializedName = schemaProperty.SerializedName; + var isRequired = schemaProperty.IsRequired; + var shouldExcludeInWireSerialization = schemaProperty.IsReadOnly; + var memberValueExpression = new TypedMemberExpression(null, property.Declaration.Name, property.Declaration.Type); + MultipartSerialization valueSerialization = BuildMultipartSerialization(schemaProperty.Type, property.Declaration.Type, false, property.SerializationFormat, new TypedMemberExpression(null, serializedName, property.Declaration.Type).NullableStructValue()); + var propertySerialization = new MultipartPropertySerialization( + parameter.Name, + memberValueExpression, + serializedName, + property.ValueType, + valueSerialization, + isRequired, + shouldExcludeInWireSerialization); + properties.Add(propertySerialization); + } + + /*build serialization for additional properties*/ + var additionalProperties = CreateMultipartAdditionalPropertiesSerialization(inputModel, objectType); + return new MultipartObjectSerialization(objectType, + objectType.SerializationConstructor.Signature.Parameters, + properties, + additionalProperties, + objectType.Discriminator, + false); + } + private MultipartAdditionalPropertiesSerialization? CreateMultipartAdditionalPropertiesSerialization(InputModelType objectSchema, ObjectType objectType) + { + var additionalPropertiesValueType = objectSchema.AdditionalProperties; + bool shouldExcludeInWireSerialization = false; + ObjectTypeProperty? additionalPropertiesProperty = null; + foreach (var obj in objectType.EnumerateHierarchy()) + { + additionalPropertiesProperty = obj.AdditionalPropertiesProperty ?? (obj as SerializableObjectType)?.RawDataField; + if (additionalPropertiesProperty != null) + { + // if this is a real "AdditionalProperties", we should NOT exclude it in wire + shouldExcludeInWireSerialization = additionalPropertiesProperty != obj.AdditionalPropertiesProperty; + break; + } + } + + if (additionalPropertiesProperty == null) + { + return null; + } + + var dictionaryValueType = additionalPropertiesProperty.Declaration.Type.Arguments[1]; + Debug.Assert(!dictionaryValueType.IsNullable, $"{typeof(JsonCodeWriterExtensions)} implicitly relies on {additionalPropertiesProperty.Declaration.Name} dictionary value being non-nullable"); + MultipartSerialization valueSerialization; + if (additionalPropertiesValueType is not null) + { + valueSerialization = BuildMultipartSerialization(InputPrimitiveType.String, dictionaryValueType, false, additionalPropertiesProperty.SerializationFormat, new TypedMemberExpression(null, additionalPropertiesProperty.Declaration.Name, additionalPropertiesProperty.Declaration.Type).NullableStructValue()); + } + else + { + valueSerialization = new MultipartValueSerialization(dictionaryValueType, SerializationFormat.Default, true); + } + + return new MultipartAdditionalPropertiesSerialization( + additionalPropertiesProperty, + new CSharpType(typeof(Dictionary<,>), additionalPropertiesProperty.Declaration.Type.Arguments), + valueSerialization, + shouldExcludeInWireSerialization); + } + public static MultipartSerialization BuildMultipartSerialization(InputType? inputType, CSharpType valueType, bool isCollectionElement, SerializationFormat serializationFormat, ValueExpression memberValueExpression) + { + /*TODO: need to update to use InputType to identify if it is a Multipart File or not. Current we will set contentType for Bytes and Stream*/ + if (inputType != null && inputType is InputPrimitiveType { Kind: InputPrimitiveTypeKind.Bytes } && valueType.IsFrameworkType && valueType.FrameworkType == typeof(BinaryData)) + { + var valueSerialization = new MultipartValueSerialization(valueType, serializationFormat, valueType.IsNullable || isCollectionElement); + valueSerialization.ContentType = "application/octet-stream"; //TODO: need to set the right content type from InputType + return valueSerialization; + } + if (inputType != null && inputType.Name == InputPrimitiveType.Stream.Name && valueType.IsFrameworkType && valueType.FrameworkType == typeof(Stream)) + { + var valueSerialization = new MultipartValueSerialization(valueType, serializationFormat, valueType.IsNullable || isCollectionElement); + valueSerialization.ContentType = "application/octet-stream"; //TODO: need to set the right content type from InputType + return valueSerialization; + } + return inputType switch + { + InputListType listType => new MultipartArraySerialization(valueType, BuildMultipartSerialization(listType.ValueType, valueType.ElementType, true, serializationFormat, new VariableReference(valueType.ElementType, "item")), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)), + InputDictionaryType dictionaryType => new MultipartDictionarySerialization(valueType, BuildMultipartSerialization(dictionaryType.ValueType, valueType.ElementType, true, serializationFormat, memberValueExpression), valueType.IsNullable || (isCollectionElement && !valueType.IsValueType)), + InputNullableType nullableType => BuildMultipartSerialization(nullableType.Type, valueType, isCollectionElement, serializationFormat, memberValueExpression), + _ => new MultipartValueSerialization(valueType, serializationFormat, valueType.IsNullable || isCollectionElement)// nullable CSharp type like int?, Etag?, and reference type in collection + }; + } + public static IEnumerable CreateMultipartPropertySerializations(ModelTypeProvider model) + { + foreach (var objType in model.EnumerateHierarchy()) + { + foreach (var property in objType.Properties) + { + if (property.InputModelProperty is not { } inputModelProperty) + continue; + + var declaredName = property.Declaration.Name; + var serializedName = inputModelProperty.SerializedName; + var memberValueExpression = new TypedMemberExpression(null, declaredName, property.Declaration.Type); + var valueSerialization = BuildMultipartSerialization(inputModelProperty.Type, property.Declaration.Type, false, property.SerializationFormat, memberValueExpression.NullableStructValue()); + TypedMemberExpression? enumerableExpression = null; + if (property.Declaration.Type.IsReadOnlyMemory) + { + enumerableExpression = property.Declaration.Type.IsNullable + ? new TypedMemberExpression(null, $"{property.Declaration.Name}.{nameof(Nullable>.Value)}.{nameof(ReadOnlyMemory.Span)}", typeof(ReadOnlySpan<>).MakeGenericType(property.Declaration.Type.Arguments[0].FrameworkType)) + : new TypedMemberExpression(null, $"{property.Declaration.Name}.{nameof(ReadOnlyMemory.Span)}", typeof(ReadOnlySpan<>).MakeGenericType(property.Declaration.Type.Arguments[0].FrameworkType)); + } + + yield return new MultipartPropertySerialization( + declaredName.ToVariableName(), + memberValueExpression, + serializedName, + property.ValueType.IsNullable && property.OptionalViaNullability ? property.ValueType.WithNullable(false) : property.ValueType, + valueSerialization, + property.IsRequired, + ShouldExcludeInWireSerialization(property, inputModelProperty), + null, + enumerableExpression: enumerableExpression); + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/SerializationMethodsBuilder.cs b/logger/autorest.csharp/common/Output/Builders/SerializationMethodsBuilder.cs new file mode 100644 index 0000000..e899098 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/SerializationMethodsBuilder.cs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Builders +{ + internal static class SerializationMethodsBuilder + { + public static IEnumerable BuildSerializationMethods(SerializableObjectType model) + { + var serialization = model.Serialization; + // xml has to be the first to minimize the amount of changes + if (serialization.Xml is { } xml) + { + if (model.IncludeSerializer) + { + foreach (var method in XmlSerializationMethodsBuilder.BuildXmlSerializationMethods(xml)) + { + yield return method; + } + } + + if (model.IncludeDeserializer) + { + yield return XmlSerializationMethodsBuilder.BuildDeserialize(model.Declaration, xml); + } + } + + if (serialization.Json is { } json) + { + if (model.IncludeSerializer) + { + bool hasInherits = model.Inherits is { IsFrameworkType: false }; + bool isSealed = model.GetExistingType()?.IsSealed == true; + foreach (var method in JsonSerializationMethodsBuilder.BuildJsonSerializationMethods(json, serialization.Interfaces, hasInherits, isSealed)) + { + yield return method; + } + } + + // the deserialize static method + if (model.IncludeDeserializer) + { + if (JsonSerializationMethodsBuilder.BuildDeserialize(model.Declaration, json, model.GetExistingType()) is { } deserialize) + { + yield return deserialize; + } + } + } + + if (serialization.Bicep is { } bicep) + { + foreach (var method in BicepSerializationMethodsBuilder.BuildPerTypeBicepSerializationMethods(bicep)) + { + yield return method; + } + } + + if (serialization.Multipart is { } multipart) + { + foreach (var method in MultipartSerializationMethodsBuilder.BuildMultipartSerializationMethods(multipart)) + { + yield return method; + } + } + + foreach (var method in BuildIModelMethods(model.Serialization)) + { + yield return method; + } + } + + private static IEnumerable BuildIModelMethods(ObjectTypeSerialization serialization) + { + var interfaces = serialization.Interfaces; + + var iModelTInterface = interfaces?.IPersistableModelTInterface; + var iModelObjectInterface = interfaces?.IPersistableModelObjectInterface; + + if (iModelTInterface is not null) + { + var typeOfT = iModelTInterface.Arguments[0]; + var model = typeOfT.Implementation as SerializableObjectType; + Debug.Assert(model != null, $"{typeOfT} should be a SerializableObjectType"); + + var options = new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.Options); + // BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IPersistableModel.Write), null, null, MethodSignatureModifiers.None, typeof(BinaryData), null, new[] { KnownParameters.Serializations.Options }, ExplicitInterface: iModelTInterface), + BuildModelWriteMethodBody(serialization, options, iModelTInterface).ToArray() + ); + + // T IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + var data = new BinaryDataExpression(KnownParameters.Serializations.Data); + yield return new + ( + new MethodSignature(nameof(IPersistableModel.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { KnownParameters.Serializations.Data, KnownParameters.Serializations.Options }, ExplicitInterface: iModelTInterface), + BuildModelCreateMethodBody(model, serialization, data, options, iModelTInterface).ToArray() + ); + + // ModelReaderWriterFormat IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { KnownParameters.Serializations.Options }, ExplicitInterface: iModelTInterface), + serialization.WireFormat + ); + + // if the model is a struct, it needs to implement IPersistableModel as well which leads to another 2 methods + if (iModelObjectInterface is not null) + { + // BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IPersistableModel.Write), null, null, MethodSignatureModifiers.None, typeof(BinaryData), null, new[] { KnownParameters.Serializations.Options }, ExplicitInterface: iModelObjectInterface), + // => (IPersistableModelthis).Write(options); + This.CastTo(iModelTInterface).Invoke(nameof(IPersistableModel.Write), options) + ); + + // object IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IPersistableModel.Create), null, null, MethodSignatureModifiers.None, typeof(object), null, new[] { KnownParameters.Serializations.Data, KnownParameters.Serializations.Options }, ExplicitInterface: iModelObjectInterface), + // => (IPersistableModelthis).Read(options); + This.CastTo(iModelTInterface).Invoke(nameof(IPersistableModel.Create), data, options) + ); + + // ModelReaderWriterFormat IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + yield return new + ( + new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { KnownParameters.Serializations.Options }, ExplicitInterface: iModelObjectInterface), + // => (IPersistableModelthis).GetFormatFromOptions(options); + This.CastTo(iModelTInterface).Invoke(nameof(IPersistableModel.GetFormatFromOptions), options) + ); + } + } + } + + private static IEnumerable BuildModelWriteMethodBody(ObjectTypeSerialization serialization, ModelReaderWriterOptionsExpression options, CSharpType iModelTInterface) + { + // var format = options.Format == "W" ? GetFormatFromOptions(options) : options.Format; + yield return Serializations.GetConcreteFormat(options, iModelTInterface, out var format); + + yield return EmptyLine; + + var switchStatement = new SwitchStatement(format); + + if (serialization.Json is { } json) + { + switchStatement.Add(JsonSerializationMethodsBuilder.BuildJsonWriteSwitchCase(json, options)); + } + + if (serialization.Xml is { } xml) + { + switchStatement.Add(XmlSerializationMethodsBuilder.BuildXmlWriteSwitchCase(xml, options)); + } + + if (serialization.Bicep is { } bicep) + { + switchStatement.Add(BicepSerializationMethodsBuilder.BuildBicepWriteSwitchCase(bicep, options)); + } + + if (serialization.Multipart is { } multipart) + { + switchStatement.Add(MultipartSerializationMethodsBuilder.BuildMultipartWriteSwitchCase(multipart, options)); + } + + // default case + /* + * throw new FormatException($"The model {nameof(T)} does not support '{options.Format}' format."); + */ + var typeOfT = iModelTInterface.Arguments[0]; + var defaultCase = SwitchCase.Default( + Serializations.ThrowValidationFailException(options.Format, typeOfT, Serializations.ValidationType.Write) + ); + switchStatement.Add(defaultCase); + + yield return switchStatement; + } + + private static IEnumerable BuildModelCreateMethodBody(SerializableObjectType model, ObjectTypeSerialization serialization, BinaryDataExpression data, ModelReaderWriterOptionsExpression options, CSharpType iModelTInterface) + { + // var format = options.Format == "W" ? GetFormatFromOptions(options) : options.Format; + yield return Serializations.GetConcreteFormat(options, iModelTInterface, out var format); + + yield return EmptyLine; + + var switchStatement = new SwitchStatement(format); + + if (serialization.Json is not null) + { + switchStatement.Add(JsonSerializationMethodsBuilder.BuildJsonCreateSwitchCase(model, data, options)); + } + + if (serialization.Xml is not null) + { + switchStatement.Add(XmlSerializationMethodsBuilder.BuildXmlCreateSwitchCase(model, data, options)); + } + + // default case + /* + * throw new FormatException($"The model {nameof(T)} does not support reading in '{options.Format}' format."); + */ + var typeOfT = iModelTInterface.Arguments[0]; + var defaultCase = SwitchCase.Default( + Serializations.ThrowValidationFailException(options.Format, typeOfT, Serializations.ValidationType.Read) + ); + switchStatement.Add(defaultCase); + + yield return switchStatement; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Builders/XmlSerializationMethodsBuilder.cs b/logger/autorest.csharp/common/Output/Builders/XmlSerializationMethodsBuilder.cs new file mode 100644 index 0000000..4d71deb --- /dev/null +++ b/logger/autorest.csharp/common/Output/Builders/XmlSerializationMethodsBuilder.cs @@ -0,0 +1,504 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Azure.ResourceManager.Models; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Builders +{ + internal static class XmlSerializationMethodsBuilder + { + public static IEnumerable BuildXmlSerializationMethods(XmlObjectSerialization serialization) + { + // a private helper method with the options to do the full xml serialization + var xmlWriter = new XmlWriterExpression(KnownParameters.Serializations.XmlWriter); + var nameHint = (ValueExpression)KnownParameters.Serializations.NameHint; + var options = new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.Options); + if (Configuration.UseModelReaderWriter) + { + yield return new Method + ( + new MethodSignature(serialization.WriteXmlMethodName, null, null, MethodSignatureModifiers.Private, null, null, new[] { KnownParameters.Serializations.XmlWriter, KnownParameters.Serializations.NameHint, KnownParameters.Serializations.Options }), + WriteObject(serialization, xmlWriter, nameHint, options).ToArray() + ); + + yield return new Method + ( + new MethodSignature(nameof(IXmlSerializable.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.XmlWriter, KnownParameters.Serializations.NameHint }, ExplicitInterface: typeof(IXmlSerializable)), + This.Invoke(serialization.WriteXmlMethodName, new[] { xmlWriter, nameHint, ModelReaderWriterOptionsExpression.Wire }) + ); + } + else + { + yield return new Method + ( + new MethodSignature(nameof(IXmlSerializable.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { KnownParameters.Serializations.XmlWriter, KnownParameters.Serializations.NameHint }, ExplicitInterface: typeof(IXmlSerializable)), + WriteObject(serialization, xmlWriter, nameHint, null).ToArray() + ); + } + } + + public static SwitchCase BuildXmlWriteSwitchCase(XmlObjectSerialization xml, ModelReaderWriterOptionsExpression options) + { + /* using MemoryStream stream = new MemoryStream(); + using XmlWriter writer = XmlWriter.Create(stream); + ((IXmlSerializable)this).Write(writer, null); + writer.Flush(); + // in the implementation of MemoryStream, `stream.Position` could never exceed `int.MaxValue`, therefore this if is redundant, we just need to keep the else branch + //if (stream.Position > int.MaxValue) + //{ + // return BinaryData.FromStream(stream); + //} + //else + //{ + return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); + //} + */ + return new SwitchCase(Serializations.XmlFormat, + new MethodBodyStatement[] + { + UsingDeclare("stream", typeof(MemoryStream), New.Instance(typeof(MemoryStream)), out var stream), + UsingDeclare("writer", typeof(XmlWriter), new InvokeStaticMethodExpression(typeof(XmlWriter), nameof(XmlWriter.Create), new[] { stream }), out var xmlWriter), + new InvokeInstanceMethodStatement(null, xml.WriteXmlMethodName, new[] { xmlWriter, Null, options }, false), + xmlWriter.Invoke(nameof(XmlWriter.Flush)).ToStatement(), + // return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); + Return(New.Instance(typeof(BinaryData), + InvokeStaticMethodExpression.Extension( + typeof(MemoryExtensions), + nameof(MemoryExtensions.AsMemory), + stream.Invoke(nameof(MemoryStream.GetBuffer)), + new ValueExpression[] { Int(0), stream.Property(nameof(Stream.Position)).CastTo(typeof(int)) } + ))) + }, addScope: true); // using statement must have a scope, if we do not have the addScope parameter here, the generated code will not compile + } + + public static SwitchCase BuildXmlCreateSwitchCase(SerializableObjectType model, BinaryDataExpression data, ModelReaderWriterOptionsExpression options) + { + // return DeserializeXmlCollection(XElement.Load(data.ToStream()), options); + return new SwitchCase(Serializations.XmlFormat, + Return(SerializableObjectTypeExpression.Deserialize(model, XElementExpression.Load(data.ToStream()), options))); + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable WriteObject(XmlObjectSerialization objectSerialization, XmlWriterExpression xmlWriter, ValueExpression nameHint, ModelReaderWriterOptionsExpression? options) + { + yield return xmlWriter.WriteStartElement(NullCoalescing(nameHint, Literal(objectSerialization.Name))); + + foreach (XmlObjectAttributeSerialization serialization in objectSerialization.Attributes) + { + yield return Serializations.WrapInCheckNotWire( + serialization.ShouldExcludeInWireSerialization, + options?.Format, + InvokeOptional.WrapInIsDefined(serialization, WrapInNullCheck(serialization, new[] + { + xmlWriter.WriteStartAttribute(serialization.SerializedName), + SerializeValueExpression(xmlWriter, serialization.ValueSerialization, serialization.Value), + xmlWriter.WriteEndAttribute() + }))); + } + + foreach (XmlObjectElementSerialization serialization in objectSerialization.Elements) + { + yield return Serializations.WrapInCheckNotWire( + serialization.ShouldExcludeInWireSerialization, + options?.Format, + InvokeOptional.WrapInIsDefined(serialization, WrapInNullCheck(serialization, SerializeExpression(xmlWriter, serialization.ValueSerialization, serialization.Value)))); + } + + foreach (XmlObjectArraySerialization serialization in objectSerialization.EmbeddedArrays) + { + yield return Serializations.WrapInCheckNotWire( + serialization.ShouldExcludeInWireSerialization, + options?.Format, + InvokeOptional.WrapInIsDefined(serialization, WrapInNullCheck(serialization, SerializeExpression(xmlWriter, serialization.ArraySerialization, serialization.Value)))); + } + + if (objectSerialization.ContentSerialization is { } contentSerialization) + { + yield return Serializations.WrapInCheckNotWire( + contentSerialization.ShouldExcludeInWireSerialization, + options?.Format, + SerializeValueExpression(xmlWriter, contentSerialization.ValueSerialization, contentSerialization.Value)); + } + + yield return xmlWriter.WriteEndElement(); + } + + private static MethodBodyStatement WrapInNullCheck(PropertySerialization serialization, MethodBodyStatement statement) + { + if (serialization.SerializedType is { IsNullable: true } serializedType) + { + if (serializedType.IsCollection && serialization.IsRequired) + { + return new IfElseStatement(And(NotEqual(serialization.Value, Null), InvokeOptional.IsCollectionDefined(serialization.Value)), statement, null); + } + + return new IfElseStatement(NotEqual(serialization.Value, Null), statement, null); + } + + return statement; + } + + public static MethodBodyStatement SerializeExpression(XmlWriterExpression xmlWriter, XmlElementSerialization serialization, ValueExpression expression) + => serialization switch + { + XmlArraySerialization array => SerializeArray(xmlWriter, array, new EnumerableExpression(array.Type.ElementType, expression)).AsStatement(), + XmlDictionarySerialization dictionary => SerializeDictionary(xmlWriter, dictionary, new DictionaryExpression(dictionary.Type.Arguments[0], dictionary.Type.Arguments[1], expression)), + XmlElementValueSerialization value => SerializeElement(xmlWriter, value, expression), + _ => throw new NotSupportedException() + }; + + private static IEnumerable SerializeArray(XmlWriterExpression xmlWriter, XmlArraySerialization arraySerialization, EnumerableExpression array) + { + if (arraySerialization.Wrapped) + { + yield return xmlWriter.WriteStartElement(arraySerialization.Name); + } + + yield return new ForeachStatement("item", array, out var item) + { + SerializeExpression(xmlWriter, arraySerialization.ValueSerialization, item) + }; + + if (arraySerialization.Wrapped) + { + yield return xmlWriter.WriteEndElement(); + } + } + + private static MethodBodyStatement SerializeDictionary(XmlWriterExpression xmlWriter, XmlDictionarySerialization dictionarySerialization, DictionaryExpression dictionary) + { + return new ForeachStatement("pair", dictionary, out var pair) + { + SerializeExpression(xmlWriter, dictionarySerialization.ValueSerialization, pair.Value) + }; + } + + private static MethodBodyStatement SerializeElement(XmlWriterExpression xmlWriter, XmlElementValueSerialization elementValueSerialization, ValueExpression element) + { + var type = elementValueSerialization.Value.Type; + string elementName = elementValueSerialization.Name; + + if (type is { IsFrameworkType: false, Implementation: ObjectType }) + { + return xmlWriter.WriteObjectValue(element, elementName); + } + + if (type.IsFrameworkType && type.FrameworkType == typeof(object)) + { + return xmlWriter.WriteObjectValue(element, elementName); + } + + return new[] + { + xmlWriter.WriteStartElement(elementName), + SerializeValueExpression(xmlWriter, elementValueSerialization.Value, element), + xmlWriter.WriteEndElement() + }; + } + + private static MethodBodyStatement SerializeValueExpression(XmlWriterExpression xmlWriter, XmlValueSerialization valueSerialization, ValueExpression value) + { + var type = valueSerialization.Type; + value = value.NullableStructValue(type); + + if (!type.IsFrameworkType) + { + return type.Implementation switch + { + EnumType clientEnum => xmlWriter.WriteValue(new EnumExpression(clientEnum, value).ToSerial()), + _ => throw new NotSupportedException("Object type references are only supported as elements") + }; + } + + var frameworkType = type.FrameworkType; + if (frameworkType == typeof(object)) + { + throw new NotSupportedException("Object references are only supported as elements"); + } + + return xmlWriter.WriteValue(value, frameworkType, valueSerialization.Format); + } + + public static Method BuildDeserialize(TypeDeclarationOptions declaration, XmlObjectSerialization serialization) + { + var methodName = $"Deserialize{declaration.Name}"; + var signature = Configuration.UseModelReaderWriter ? + new MethodSignature(methodName, null, null, MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static, serialization.Type, null, new[] { KnownParameters.Serializations.XElement, KnownParameters.Serializations.OptionalOptions }) : + new MethodSignature(methodName, null, null, MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static, serialization.Type, null, new[] { KnownParameters.Serializations.XElement }); + + return Configuration.UseModelReaderWriter ? + new Method(signature, BuildDeserializeBody(serialization, new XElementExpression(KnownParameters.Serializations.XElement), new ModelReaderWriterOptionsExpression(KnownParameters.Serializations.OptionalOptions)).ToArray()) : + new Method(signature, BuildDeserializeBody(serialization, new XElementExpression(KnownParameters.Serializations.XElement), null).ToArray()); + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable BuildDeserializeBody(XmlObjectSerialization objectSerialization, XElementExpression element, ModelReaderWriterOptionsExpression? options) + { + if (options != null) + { + yield return AssignIfNull(options, ModelReaderWriterOptionsExpression.Wire); + yield return EmptyLine; + } + + var propertyVariables = new Dictionary(); + + CollectProperties(propertyVariables, objectSerialization); + + foreach (var (property, variable) in propertyVariables) + { + yield return new DeclareVariableStatement(variable.Type, variable.Declaration, Default); + } + + foreach (XmlObjectAttributeSerialization attribute in objectSerialization.Attributes) + { + var attributeVariableName = attribute.SerializationConstructorParameterName + "Attribute"; + yield return new IfStatement(Is(element.Attribute(attribute.SerializedName), attributeVariableName, out var attributeVariable)) + { + Assign(propertyVariables[attribute], DeserializeValue(attribute.ValueSerialization, attributeVariable)) + }; + } + + foreach (XmlObjectElementSerialization elem in objectSerialization.Elements) + { + yield return BuildDeserialization(elem.ValueSerialization, propertyVariables[elem], element); + } + + foreach (XmlObjectArraySerialization embeddedArray in objectSerialization.EmbeddedArrays) + { + yield return BuildDeserialization(embeddedArray.ArraySerialization, propertyVariables[embeddedArray], element); + } + + if (objectSerialization.ContentSerialization is { } contentSerialization) + { + yield return Assign(propertyVariables[contentSerialization], DeserializeValue(contentSerialization.ValueSerialization, element)); + } + + var objectType = (ObjectType)objectSerialization.Type.Implementation; + var parameterValues = propertyVariables.ToDictionary(v => v.Key.SerializationConstructorParameterName, v => (ValueExpression)v.Value); + + var arguments = new List(); + foreach (var parameter in objectType.SerializationConstructor.Signature.Parameters) + { + if (parameterValues.TryGetValue(parameter.Name, out var argument)) + arguments.Add(argument); + else + { + // this must be the raw data property + arguments.Add(new PositionalParameterReference(parameter.Name, Null)); + } + } + + yield return Return(New.Instance(objectSerialization.Type, arguments)); + } + + public static MethodBodyStatement BuildDeserializationForMethods(XmlElementSerialization serialization, ValueExpression? variable, StreamExpression stream) + { + return new[] + { + Var("document", XDocumentExpression.Load(stream, LoadOptions.PreserveWhitespace), out var document), + BuildDeserialization(serialization, variable, document) + }; + } + + private static MethodBodyStatement BuildDeserialization(XmlElementSerialization serialization, ValueExpression? variable, XContainerExpression document) + { + if (serialization is XmlArraySerialization { Wrapped: false } arraySerialization) + { + var deserializedDocument = BuildDeserializationForArray(arraySerialization, document, out var deserialization); + return new[] { deserialization, AssignOrReturn(variable, deserializedDocument) }; + } + + var elementName = serialization.Name.ToVariableName() + "Element"; + return new IfStatement(Is(document.Element(serialization.Name), elementName, out var element)) + { + BuildDeserializationForXContainer(serialization, element, out var deserializedContainer), + AssignOrReturn(variable, deserializedContainer) + }; + } + + private static MethodBodyStatement BuildDeserializationForXContainer(XmlElementSerialization serialization, XElementExpression element, out ValueExpression deserializedContainer) + { + var deserialization = EmptyStatement; + switch (serialization) + { + case XmlArraySerialization arraySerialization: + deserializedContainer = BuildDeserializationForArray(arraySerialization, element, out deserialization); + break; + + case XmlDictionarySerialization dictionarySerialization: + deserializedContainer = BuildDeserializationForDictionary(dictionarySerialization, element, out deserialization); + break; + + case XmlElementValueSerialization valueSerialization: + deserializedContainer = DeserializeValue(valueSerialization.Value, element); + break; + + default: + throw new InvalidOperationException($"Unexpected {nameof(XmlElementSerialization)} type."); + } + + return deserialization; + } + + private static ValueExpression BuildDeserializationForArray(XmlArraySerialization arraySerialization, XContainerExpression container, out MethodBodyStatement deserializationStatement) + { + deserializationStatement = new MethodBodyStatement[] + { + Var("array", New.List(arraySerialization.Type.Arguments[0]), out var array), + new ForeachStatement("e", container.Elements(arraySerialization.ValueSerialization.Name), out var child) + { + BuildDeserializationForXContainer(arraySerialization.ValueSerialization, new XElementExpression(child), out var deserializedChild), + array.Add(deserializedChild) + } + }; + return array; + } + + private static ValueExpression BuildDeserializationForDictionary(XmlDictionarySerialization dictionarySerialization, XContainerExpression container, out MethodBodyStatement deserializationStatement) + { + deserializationStatement = new MethodBodyStatement[] + { + Var("dictionary", New.Dictionary(dictionarySerialization.Type.Arguments[0], dictionarySerialization.Type.Arguments[1]), out var dictionary), + new ForeachStatement("e", container.Elements(), out var element) + { + BuildDeserializationForXContainer(dictionarySerialization.ValueSerialization, new XElementExpression(element), out var deserializedElement), + dictionary.Add(new XElementExpression(element).Name.LocalName, deserializedElement) + } + }; + return dictionary; + } + + private static ValueExpression DeserializeValue(XmlValueSerialization serialization, ValueExpression value) + { + var type = serialization.Type; + + if (type.IsFrameworkType) + { + var frameworkType = type.FrameworkType; + if (frameworkType == typeof(bool) || + frameworkType == typeof(char) || + frameworkType == typeof(short) || + frameworkType == typeof(int) || + frameworkType == typeof(long) || + frameworkType == typeof(float) || + frameworkType == typeof(double) || + frameworkType == typeof(decimal) || + frameworkType == typeof(string) + ) + { + return value.CastTo(type); + } + + if (frameworkType == typeof(ResourceIdentifier)) + { + return New.Instance(typeof(ResourceIdentifier), value.CastTo(typeof(string))); + } + + if (frameworkType == typeof(SystemData)) + { + // XML Deserialization of SystemData isn't supported yet. + return Null; + } + + if (frameworkType == typeof(ResourceType)) + { + return value.CastTo(typeof(string)); + } + + if (frameworkType == typeof(Guid)) + { + return value is XElementExpression xElement + ? New.Instance(typeof(Guid), xElement.Value) + : value.CastTo(typeof(Guid)); + } + + if (frameworkType == typeof(Uri)) + { + return New.Instance(typeof(Uri), value.CastTo(typeof(string))); + } + + if (value is XElementExpression element) + { + if (frameworkType == typeof(byte[])) + { + return element.GetBytesFromBase64Value(serialization.Format.ToFormatSpecifier()); + } + + if (frameworkType == typeof(DateTimeOffset)) + { + return element.GetDateTimeOffsetValue(serialization.Format.ToFormatSpecifier()); + } + + if (frameworkType == typeof(TimeSpan)) + { + return element.GetTimeSpanValue(serialization.Format.ToFormatSpecifier()); + } + + if (frameworkType == typeof(object)) + { + return element.GetObjectValue(serialization.Format.ToFormatSpecifier()); + } + } + } + + switch (type.Implementation) + { + case SerializableObjectType serializableObjectType: + return SerializableObjectTypeExpression.Deserialize(serializableObjectType, value); + + case EnumType clientEnum when value is XElementExpression xe: + return EnumExpression.ToEnum(clientEnum, xe.Value); + + case EnumType clientEnum when value is XAttributeExpression xa: + return EnumExpression.ToEnum(clientEnum, xa.Value); + + default: + throw new NotSupportedException(); + } + } + + private static void CollectProperties(Dictionary propertyVariables, XmlObjectSerialization element) + { + foreach (var attribute in element.Attributes) + { + propertyVariables.Add(attribute, new VariableReference(attribute.Value.Type, attribute.SerializationConstructorParameterName)); + } + + foreach (var attribute in element.Elements) + { + propertyVariables.Add(attribute, new VariableReference(attribute.Value.Type, attribute.SerializationConstructorParameterName)); + } + + foreach (var attribute in element.EmbeddedArrays) + { + propertyVariables.Add(attribute, new VariableReference(attribute.Value.Type, attribute.SerializationConstructorParameterName)); + } + + if (element.ContentSerialization is { } contentSerialization) + { + propertyVariables.Add(contentSerialization, new VariableReference(contentSerialization.Value.Type, contentSerialization.SerializationConstructorParameterName)); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureModelSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureModelSnippets.cs new file mode 100644 index 0000000..e4c6634 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureModelSnippets.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using Azure; + +namespace AutoRest.CSharp.Common.Output.Expressions.Azure +{ + internal partial class AzureExtensibleSnippets + { + internal class AzureModelSnippets : ModelSnippets + { + public override Method BuildFromOperationResponseMethod(SerializableObjectType type, MethodSignatureModifiers modifiers) + { + var fromResponse = new Parameter("response", $"The response to deserialize the model from.", typeof(Response), null, ValidationType.None, null); + return new Method + ( + new MethodSignature(Configuration.ApiTypes.FromResponseName, null, $"Deserializes the model from a raw response.", modifiers, type.Type, null, new[] { fromResponse }), + new MethodBodyStatement[] + { + Snippets.UsingVar("document", JsonDocumentExpression.Parse(new ResponseExpression(fromResponse).Content), out var document), + Snippets.Return(SerializableObjectTypeExpression.Deserialize(type, document.RootElement)) + } + ); + } + + public override TypedValueExpression InvokeToRequestBodyMethod(TypedValueExpression model) => new RequestContentExpression(model.Invoke(Configuration.ApiTypes.ToRequestContentName)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureRequestContent.cs b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureRequestContent.cs new file mode 100644 index 0000000..20b4659 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureRequestContent.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.Azure +{ + internal partial class AzureExtensibleSnippets + { + internal class AzureRequestContentSnippets : RequestContentSnippets + { + public override ValueExpression Create(BinaryDataExpression binaryData) + => RequestContentExpression.Create(binaryData); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureRestOperationsSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureRestOperationsSnippets.cs new file mode 100644 index 0000000..3565720 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureRestOperationsSnippets.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.Azure +{ + internal partial class AzureExtensibleSnippets + { + internal class AzureRestOperationsSnippets : RestOperationsSnippets + { + public override StreamExpression GetContentStream(TypedValueExpression response) => new ResponseExpression(response).ContentStream; + + public override TypedValueExpression GetTypedResponseFromValue(TypedValueExpression value, TypedValueExpression response) + => ResponseExpression.FromValue(value, new ResponseExpression(response)); + + public override TypedValueExpression GetTypedResponseFromModel(SerializableObjectType type, TypedValueExpression response) + { + var rawResponse = new ResponseExpression(response); + var model = new InvokeStaticMethodExpression(type.Type, Configuration.ApiTypes.FromResponseName, new[] { rawResponse }); + return ResponseExpression.FromValue(model, rawResponse); + } + + public override TypedValueExpression GetTypedResponseFromEnum(EnumType enumType, TypedValueExpression response) + { + var rawResponse = new ResponseExpression(response); + return ResponseExpression.FromValue(EnumExpression.ToEnum(enumType, rawResponse.Content.ToObjectFromJson(typeof(string))), rawResponse); + } + + public override TypedValueExpression GetTypedResponseFromBinaryData(Type responseType, TypedValueExpression response, string? contentType = null) + { + var rawResponse = new ResponseExpression(response); + if (responseType == typeof(string) && contentType != null && FormattableStringHelpers.ToMediaType(contentType) == BodyMediaType.Text) + { + return ResponseExpression.FromValue(rawResponse.Content.InvokeToString(), rawResponse); + } + return responseType == typeof(BinaryData) + ? ResponseExpression.FromValue(rawResponse.Content, rawResponse) + : responseType == typeof(AzureLocation) ? + ResponseExpression.FromValue(New.Instance(typeof(AzureLocation), new[] { rawResponse.Content.ToObjectFromJson(typeof(string)) }), rawResponse) + : ResponseExpression.FromValue(rawResponse.Content.ToObjectFromJson(responseType), rawResponse); + } + + public override MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedValueExpression message) + { + var messageVar = new VariableReference(typeof(HttpMessage), "message"); + message = messageVar; + return Snippets.UsingDeclare(messageVar, new InvokeInstanceMethodExpression(null, createRequestMethodSignature.Name, createRequestMethodSignature.Parameters.Select(p => (ValueExpression)p).ToList(), null, false)); + } + + public override MethodBodyStatement DeclareContentWithXmlWriter(out TypedValueExpression content, out XmlWriterExpression writer) + { + var contentVar = new VariableReference(typeof(XmlWriterContent), "content"); + content = contentVar; + writer = new XmlWriterContentExpression(content).XmlWriter; + return Snippets.Var(contentVar, Snippets.New.Instance(typeof(XmlWriterContent))); + } + + public override MethodBodyStatement InvokeServiceOperationCallAndReturnHeadAsBool(TypedValueExpression pipeline, TypedValueExpression message, TypedValueExpression clientDiagnostics, bool async) + => Snippets.Return(new HttpPipelineExpression(pipeline).ProcessHeadAsBoolMessage(new HttpMessageExpression(message), clientDiagnostics, new RequestContextExpression(KnownParameters.RequestContext), async)); + + public override TypedValueExpression InvokeServiceOperationCall(TypedValueExpression pipeline, TypedValueExpression message, bool async) + => new HttpPipelineExpression(pipeline).ProcessMessage(new HttpMessageExpression(message), new RequestContextExpression(KnownParameters.RequestContext), null, async); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureXElementSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureXElementSnippets.cs new file mode 100644 index 0000000..82c6655 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureXElementSnippets.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.Azure +{ + internal partial class AzureExtensibleSnippets + { + private class AzureXElementSnippets : XElementSnippets + { + public override ValueExpression GetBytesFromBase64Value(XElementExpression xElement, string? format) + => InvokeExtension(typeof(XElementExtensions), xElement, nameof(XElementExtensions.GetBytesFromBase64Value), Snippets.Literal(format)); + public override ValueExpression GetDateTimeOffsetValue(XElementExpression xElement, string? format) + => InvokeExtension(typeof(XElementExtensions), xElement, nameof(XElementExtensions.GetDateTimeOffsetValue), Snippets.Literal(format)); + public override ValueExpression GetObjectValue(XElementExpression xElement, string? format) + => InvokeExtension(typeof(XElementExtensions), xElement, nameof(XElementExtensions.GetObjectValue), Snippets.Literal(format)); + public override ValueExpression GetTimeSpanValue(XElementExpression xElement, string? format) + => InvokeExtension(typeof(XElementExtensions), xElement, nameof(XElementExtensions.GetTimeSpanValue), Snippets.Literal(format)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureXmlWriterSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureXmlWriterSnippets.cs new file mode 100644 index 0000000..fd59c5a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.AzureXmlWriterSnippets.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.Azure +{ + internal partial class AzureExtensibleSnippets + { + private class AzureXmlWriterSnippets : XmlWriterSnippets + { + public override MethodBodyStatement WriteValue(XmlWriterExpression xmlWriter, ValueExpression value, string format) + => new InvokeStaticMethodStatement(typeof(XmlWriterExtensions), nameof(XmlWriterExtensions.WriteValue), new[] { xmlWriter, value, Snippets.Literal(format) }, CallAsExtension: true); + + public override MethodBodyStatement WriteObjectValue(XmlWriterExpression xmlWriter, ValueExpression value, string? nameHint) + => new InvokeStaticMethodStatement(typeof(XmlWriterExtensions), nameof(XmlWriterExtensions.WriteObjectValue), new[] { xmlWriter, value, Snippets.Literal(nameHint) }, CallAsExtension: true); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.cs new file mode 100644 index 0000000..166a2f3 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Azure/AzureExtensibleSnippets.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Common.Output.Expressions.Azure +{ + internal partial class AzureExtensibleSnippets : ExtensibleSnippets + { + public override RequestContentSnippets RequestContent { get; } = new AzureRequestContentSnippets(); + public override XElementSnippets XElement { get; } = new AzureXElementSnippets(); + public override XmlWriterSnippets XmlWriter { get; } = new AzureXmlWriterSnippets(); + public override RestOperationsSnippets RestOperations { get; } = new AzureRestOperationsSnippets(); + public override ModelSnippets Model { get; } = new AzureModelSnippets(); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.JsonElementSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.JsonElementSnippets.cs new file mode 100644 index 0000000..2ddc54d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.JsonElementSnippets.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + internal class JsonElementSnippets + { + public ValueExpression GetBytesFromBase64(JsonElementExpression element, string? format) + => ModelSerializationExtensionsProvider.Instance.GetBytesFromBase64(element, format); + + public ValueExpression GetChar(JsonElementExpression element) + => ModelSerializationExtensionsProvider.Instance.GetChar(element); + + public ValueExpression GetDateTimeOffset(JsonElementExpression element, string? format) + => ModelSerializationExtensionsProvider.Instance.GetDateTimeOffset(element, format); + + public ValueExpression GetObject(JsonElementExpression element) + => ModelSerializationExtensionsProvider.Instance.GetObject(element); + + public ValueExpression GetTimeSpan(JsonElementExpression element, string? format) + => ModelSerializationExtensionsProvider.Instance.GetTimeSpan(element, format); + + public MethodBodyStatement ThrowNonNullablePropertyIsNull(JsonPropertyExpression property) + => ModelSerializationExtensionsProvider.Instance.ThrowNonNullablePropertyIsNull(property); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.ModelSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.ModelSnippets.cs new file mode 100644 index 0000000..ebda9ca --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.ModelSnippets.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + internal abstract class ModelSnippets + { + public virtual Method BuildConversionToRequestBodyMethod(MethodSignatureModifiers modifiers, CSharpType type) + { + var bodyStatements = Configuration.IsBranded + ? new[] + { + Extensible.RestOperations.DeclareContentWithUtf8JsonWriter(out var requestContent, out var writer), + writer.WriteObjectValue(new TypedValueExpression(type, This), ModelReaderWriterOptionsExpression.Wire), + Return(requestContent) + } + : new[] + { + Return(BinaryContentExpression.Create(This, ModelReaderWriterOptionsExpression.Wire, type)) + }; + return new Method + ( + new MethodSignature(Configuration.ApiTypes.ToRequestContentName, null, $"Convert into a {Configuration.ApiTypes.RequestContentType:C}.", modifiers, Configuration.ApiTypes.RequestContentType, null, Array.Empty()), + bodyStatements + ); + } + + public abstract Method BuildFromOperationResponseMethod(SerializableObjectType type, MethodSignatureModifiers modifiers); + public abstract TypedValueExpression InvokeToRequestBodyMethod(TypedValueExpression model); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.RequestContent.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.RequestContent.cs new file mode 100644 index 0000000..761fd59 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.RequestContent.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + internal abstract class RequestContentSnippets + { + public abstract ValueExpression Create(BinaryDataExpression binaryData); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.RestOperationsSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.RestOperationsSnippets.cs new file mode 100644 index 0000000..0a13db4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.RestOperationsSnippets.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Types; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + internal abstract class RestOperationsSnippets + { + public abstract TypedValueExpression InvokeServiceOperationCall(TypedValueExpression pipeline, TypedValueExpression message, bool async); + + public abstract TypedValueExpression GetTypedResponseFromValue(TypedValueExpression value, TypedValueExpression response); + public abstract TypedValueExpression GetTypedResponseFromModel(SerializableObjectType type, TypedValueExpression response); + public abstract TypedValueExpression GetTypedResponseFromEnum(EnumType enumType, TypedValueExpression response); + public abstract TypedValueExpression GetTypedResponseFromBinaryData(Type responseType, TypedValueExpression response, string? contentType = null); + + public abstract MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedValueExpression message); + + public virtual MethodBodyStatement DeclareContentWithUtf8JsonWriter(out TypedValueExpression content, out Utf8JsonWriterExpression writer) + { + var contentVar = new VariableReference(Utf8JsonRequestContentProvider.Instance.Type, "content"); + content = contentVar; + writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + return Var(contentVar, New.Instance(Utf8JsonRequestContentProvider.Instance.Type)); + } + + public abstract MethodBodyStatement DeclareContentWithXmlWriter(out TypedValueExpression content, out XmlWriterExpression writer); + public abstract MethodBodyStatement InvokeServiceOperationCallAndReturnHeadAsBool(TypedValueExpression pipeline, TypedValueExpression message, TypedValueExpression clientDiagnostics, bool async); + public abstract StreamExpression GetContentStream(TypedValueExpression response); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.XElementSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.XElementSnippets.cs new file mode 100644 index 0000000..87583aa --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.XElementSnippets.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + internal abstract class XElementSnippets + { + public abstract ValueExpression GetBytesFromBase64Value(XElementExpression xElement, string? format); + public abstract ValueExpression GetDateTimeOffsetValue(XElementExpression xElement, string? format); + public abstract ValueExpression GetObjectValue(XElementExpression xElement, string? format); + public abstract ValueExpression GetTimeSpanValue(XElementExpression xElement, string? format); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.XmlWriterSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.XmlWriterSnippets.cs new file mode 100644 index 0000000..170b41a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.XmlWriterSnippets.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + internal abstract class XmlWriterSnippets + { + public abstract MethodBodyStatement WriteValue(XmlWriterExpression xmlWriter, ValueExpression value, string format); + public abstract MethodBodyStatement WriteObjectValue(XmlWriterExpression xmlWriter, ValueExpression value, string? nameHint); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.cs new file mode 100644 index 0000000..7d19ed3 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ExtensibleSnippets.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions +{ + internal abstract partial class ExtensibleSnippets + { + public abstract RequestContentSnippets RequestContent { get; } + public virtual JsonElementSnippets JsonElement { get; } = new JsonElementSnippets(); + public abstract ModelSnippets Model { get; } + public abstract RestOperationsSnippets RestOperations { get; } + public abstract XElementSnippets XElement { get; } + public abstract XmlWriterSnippets XmlWriter { get; } + + protected static InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, ValueExpression instance, string methodName) + => new(extensionType, methodName, new[] { instance }, CallAsAsync: false, CallAsExtension: true); + + protected static InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, ValueExpression instance, string methodName, ValueExpression arg) + => new(extensionType, methodName, new[] { instance, arg }, CallAsAsync: false, CallAsExtension: true); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownCodeBlocks/DiagnosticScopeMethodBodyBlock.cs b/logger/autorest.csharp/common/Output/Expressions/KnownCodeBlocks/DiagnosticScopeMethodBodyBlock.cs new file mode 100644 index 0000000..3409161 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownCodeBlocks/DiagnosticScopeMethodBodyBlock.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Requests; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownCodeBlocks +{ + internal record DiagnosticScopeMethodBodyBlock(Diagnostic Diagnostic, Reference ClientDiagnosticsReference, MethodBodyStatement InnerStatement) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + using (writer.WriteDiagnosticScope(Diagnostic, ClientDiagnosticsReference)) + { + InnerStatement.Write(writer); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownCodeBlocks/ParameterValidationBlock.cs b/logger/autorest.csharp/common/Output/Expressions/KnownCodeBlocks/ParameterValidationBlock.cs new file mode 100644 index 0000000..5b75e3f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownCodeBlocks/ParameterValidationBlock.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownCodeBlocks +{ + internal record ParameterValidationBlock(IReadOnlyList Parameters, bool IsLegacy = false) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + if (IsLegacy) + { + writer.WriteParameterNullChecks(Parameters); + } + else + { + writer.WriteParametersValidation(Parameters); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ArmResourceExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ArmResourceExpression.cs new file mode 100644 index 0000000..3e1e9d0 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ArmResourceExpression.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record ArmResourceExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public ResourceIdentifierExpression Id => new(Property(nameof(ArmResource.Id))); + public ResourceIdentifierExpression Name => new(Property("Name")); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/FormUrlEncodedContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/FormUrlEncodedContentExpression.cs new file mode 100644 index 0000000..447b722 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/FormUrlEncodedContentExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record FormUrlEncodedContentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public MethodBodyStatement Add(string name, StringExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(FormUrlEncodedContent.Add), Literal(name), value); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/HttpMessageExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/HttpMessageExpression.cs new file mode 100644 index 0000000..b3dc62f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/HttpMessageExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record HttpMessageExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public RequestExpression Request => new(Property(nameof(HttpMessage.Request))); + public ResponseExpression Response => new(Property(nameof(HttpMessage.Response))); + public ValueExpression BufferResponse => Property(nameof(HttpMessage.BufferResponse)); + + public StreamExpression ExtractResponseContent() => new(Invoke(nameof(HttpMessage.ExtractResponseContent))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/HttpPipelineExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/HttpPipelineExpression.cs new file mode 100644 index 0000000..9a79ac3 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/HttpPipelineExpression.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record HttpPipelineExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public HttpMessageExpression CreateMessage() => new(Invoke(nameof(HttpPipeline.CreateMessage))); + public HttpMessageExpression CreateMessage(RequestContextExpression requestContext) => new(Invoke(nameof(HttpPipeline.CreateMessage), requestContext)); + public HttpMessageExpression CreateMessage(RequestContextExpression requestContext, ValueExpression responseClassifier) => new(Invoke(nameof(HttpPipeline.CreateMessage), requestContext, responseClassifier)); + + public TypedValueExpression ProcessMessage(HttpMessageExpression message, RequestContextExpression? requestContext, CancellationTokenExpression? cancellationToken, bool async) + { + var arguments = new List + { + message, + requestContext ?? Snippets.Null + }; + + if (cancellationToken != null) + { + arguments.Add(cancellationToken); + } + + var methodName = async ? nameof(HttpPipelineExtensions.ProcessMessageAsync) : nameof(HttpPipelineExtensions.ProcessMessage); + return new ResponseExpression(InvokeExtension(typeof(HttpPipelineExtensions), methodName, arguments, async)); + } + + public ValueExpression ProcessHeadAsBoolMessage(HttpMessageExpression message, ValueExpression clientDiagnostics, RequestContextExpression? requestContext, bool async) + { + var arguments = new List + { + message, + clientDiagnostics, + requestContext ?? Snippets.Null + }; + + var methodName = async ? nameof(HttpPipelineExtensions.ProcessHeadAsBoolMessageAsync) : nameof(HttpPipelineExtensions.ProcessHeadAsBoolMessage); + return InvokeExtension(typeof(HttpPipelineExtensions), methodName, arguments, async); + } + + public MethodBodyStatement Send(HttpMessageExpression message, CancellationTokenExpression cancellationToken, bool async) + { + var methodName = async ? nameof(HttpPipeline.SendAsync) : nameof(HttpPipeline.Send); + return new InvokeInstanceMethodStatement(Untyped, methodName, new ValueExpression[] { message, cancellationToken }, async); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/NullableResponseExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/NullableResponseExpression.cs new file mode 100644 index 0000000..e176055 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/NullableResponseExpression.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record NullableResponseExpression(ValueExpression Untyped) : TypedValueExpression>(Untyped) + { + public BoolExpression HasValue => new(Property(nameof(NullableResponse.HasValue))); + public ValueExpression Value => Property(nameof(NullableResponse.Value)); + public ResponseExpression GetRawResponse() => new(Invoke(nameof(NullableResponse.GetRawResponse))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/OperationExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/OperationExpression.cs new file mode 100644 index 0000000..fb30ce2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/OperationExpression.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record OperationExpression(ValueExpression Untyped) : TypedValueExpression(typeof(Operation), Untyped) + { + public ValueExpression Value => Property(nameof(Operation.Value)); + public ResponseExpression GetRawResponse() => new(Invoke(nameof(Operation.GetRawResponse))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RawRequestUriBuilderExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RawRequestUriBuilderExpression.cs new file mode 100644 index 0000000..bb25db8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RawRequestUriBuilderExpression.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Serialization; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record RawRequestUriBuilderExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public MethodBodyStatement Reset(ValueExpression uri) => new InvokeInstanceMethodStatement(Untyped, nameof(RawRequestUriBuilder.Reset), uri); + + public MethodBodyStatement AppendRaw(string value, bool escape) => new InvokeInstanceMethodStatement(Untyped, nameof(RawRequestUriBuilder.AppendRaw), Literal(value), Bool(escape)); + public MethodBodyStatement AppendRaw(ValueExpression value, bool escape) => new InvokeInstanceMethodStatement(Untyped, nameof(RawRequestUriBuilder.AppendRaw), value, Bool(escape)); + + public MethodBodyStatement AppendPath(string value, bool escape) => new InvokeInstanceMethodStatement(Untyped, nameof(RequestUriBuilder.AppendPath), Literal(value), Bool(escape)); + public MethodBodyStatement AppendPath(ValueExpression value, bool escape) => new InvokeInstanceMethodStatement(Untyped, nameof(RequestUriBuilder.AppendPath), value, Bool(escape)); + public MethodBodyStatement AppendPath(ValueExpression value, string format, bool escape) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestUriBuilderExtensions.AppendPath), new[] { Untyped, value, Literal(format), Bool(escape) }, CallAsExtension: true); + + public MethodBodyStatement AppendPath(ValueExpression value, SerializationFormat format, bool escape) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AppendPath(value, formatSpecifier, escape) + : AppendPath(value, escape); + + public MethodBodyStatement AppendRawNextLink(ValueExpression nextLink, bool escape) => new InvokeInstanceMethodStatement(Untyped, nameof(RawRequestUriBuilder.AppendRawNextLink), nextLink, Bool(escape)); + + public MethodBodyStatement AppendQuery(string name, ValueExpression value, bool escape) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestUriBuilderExtensions.AppendQuery), new[] { Untyped, Literal(name), value, Bool(escape) }, CallAsExtension: true); + public MethodBodyStatement AppendQuery(string name, ValueExpression value, string format, bool escape) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestUriBuilderExtensions.AppendQuery), new[] { Untyped, Literal(name), value, Literal(format), Bool(escape) }, CallAsExtension: true); + public MethodBodyStatement AppendQuery(string name, ValueExpression value, SerializationFormat format, bool escape) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AppendQuery(name, value, formatSpecifier, escape) + : AppendQuery(name, value, escape); + + public MethodBodyStatement AppendQueryDelimited(string name, ValueExpression value, string delimiter, bool escape) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestUriBuilderExtensions.AppendQueryDelimited), new[] { Untyped, Literal(name), value, Literal(delimiter), Bool(escape) }, CallAsExtension: true); + public MethodBodyStatement AppendQueryDelimited(string name, ValueExpression value, string delimiter, string format, bool escape) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestUriBuilderExtensions.AppendQueryDelimited), new[] { Untyped, Literal(name), value, Literal(delimiter), Literal(format), Bool(escape) }, CallAsExtension: true); + public MethodBodyStatement AppendQueryDelimited(string name, ValueExpression value, string delimiter, SerializationFormat format, bool escape) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AppendQueryDelimited(name, value, delimiter, formatSpecifier, escape) + : AppendQueryDelimited(name, value, delimiter, escape); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestContentExpression.cs new file mode 100644 index 0000000..c6595b7 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestContentExpression.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record RequestContentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static RequestContentExpression Create(ValueExpression serializable) => new(InvokeStatic(nameof(RequestContent.Create), new[] { serializable })); + public static RequestContentExpression FromObject(ValueExpression value) => new(RequestContentHelperProvider.Instance.FromObject(value)); + public static RequestContentExpression FromEnumerable(ValueExpression enumerable) => new(RequestContentHelperProvider.Instance.FromEnumerable(enumerable)); + public static RequestContentExpression FromDictionary(ValueExpression dictionary) => new(RequestContentHelperProvider.Instance.FromDictionary(dictionary)); + + public static implicit operator RequestContentExpression(FormUrlEncodedContentExpression formUrlEncodedContent) => new(formUrlEncodedContent.Untyped); + public static implicit operator RequestContentExpression(MultipartFormDataContentExpression multipartFormDataContent) => new(multipartFormDataContent.Untyped); + public static implicit operator RequestContentExpression(StringRequestContentExpression stringRequestContentExpression) => new(stringRequestContentExpression.Untyped); + public static implicit operator RequestContentExpression(XmlWriterContentExpression xmlWriterContent) => new(xmlWriterContent.Untyped); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestContextExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestContextExpression.cs new file mode 100644 index 0000000..db814c3 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestContextExpression.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Shared; +using Azure; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record RequestContextExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static RequestContextExpression FromCancellationToken() + => new(new InvokeStaticMethodExpression(null, "FromCancellationToken", new ValueExpression[] { KnownParameters.CancellationTokenParameter })); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestExpression.cs new file mode 100644 index 0000000..e60c173 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record RequestExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public RequestContentExpression ClientRequestId => new(Property(nameof(Request.ClientRequestId))); + public RequestContentExpression Content => new(Property(nameof(Request.Content))); + public RequestHeadersExpression Headers => new(Property(nameof(Request.Headers))); + public ValueExpression Method => Property(nameof(Request.Method)); + public RawRequestUriBuilderExpression Uri => new(Property(nameof(Request.Uri))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestHeadersExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestHeadersExpression.cs new file mode 100644 index 0000000..a0758ef --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/RequestHeadersExpression.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Serialization; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record RequestHeadersExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public MethodBodyStatement Add(ValueExpression conditions) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestHeaderExtensions.Add), new[] { Untyped, conditions }, CallAsExtension: true); + public MethodBodyStatement Add(ValueExpression conditions, SerializationFormat format) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestHeaderExtensions.Add), new[] { Untyped, conditions, Literal(format.ToFormatSpecifier()) }, CallAsExtension: true); + + public MethodBodyStatement Add(string name, ValueExpression value) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestHeaderExtensions.Add), new[] { Untyped, Literal(name), value }, CallAsExtension: true); + public MethodBodyStatement Add(string name, ValueExpression value, string format) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestHeaderExtensions.Add), new[] { Untyped, Literal(name), value, Literal(format) }, CallAsExtension: true); + public MethodBodyStatement Add(string name, ValueExpression value, SerializationFormat format) + => format.ToFormatSpecifier() is { } formatSpecifier + ? Add(name, value, formatSpecifier) + : Add(name, value); + + public MethodBodyStatement AddDelimited(string name, ValueExpression value, string delimiter) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestHeaderExtensions.AddDelimited), new[] { Untyped, Literal(name), value, Literal(delimiter) }, CallAsExtension: true); + public MethodBodyStatement AddDelimited(string name, ValueExpression value, string delimiter, string format) + => new InvokeStaticMethodStatement(typeof(RequestUriBuilderExtensions), nameof(RequestHeaderExtensions.AddDelimited), new[] { Untyped, Literal(name), value, Literal(delimiter), Literal(format) }, CallAsExtension: true); + public MethodBodyStatement AddDelimited(string name, ValueExpression value, string delimiter, SerializationFormat format) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AddDelimited(name, value, delimiter, formatSpecifier) + : AddDelimited(name, value, delimiter); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResourceIdentifierExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResourceIdentifierExpression.cs new file mode 100644 index 0000000..d3db6b7 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResourceIdentifierExpression.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record ResourceIdentifierExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static ResourceIdentifierExpression Root => new(new MemberExpression(typeof(ResourceIdentifier), nameof(ResourceIdentifier.Root))); + + public StringExpression Name => new(Property(nameof(ResourceIdentifier.Name))); + public ResourceIdentifierExpression Parent => new(Property(nameof(ResourceIdentifier.Parent))); + public StringExpression Provider => new(Property(nameof(ResourceIdentifier.Provider))); + public ResourceTypeExpression ResourceType => new(Property(nameof(ResourceIdentifier.ResourceType))); + public StringExpression ResourceGroupName => new(Property(nameof(ResourceIdentifier.ResourceGroupName))); + public StringExpression SubscriptionId => new(Property(nameof(ResourceIdentifier.SubscriptionId))); + + public StringExpression SubstringAfterProviderNamespace() => new(InvokeExtension(typeof(SharedExtensions), nameof(SharedExtensions.SubstringAfterProviderNamespace))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResourceTypeExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResourceTypeExpression.cs new file mode 100644 index 0000000..1cc8e93 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResourceTypeExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record ResourceTypeExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public StringExpression Namespace => new(Property(nameof(ResourceType.Namespace))); + + public StringExpression GetLastType() => new(Invoke(nameof(ResourceType.GetLastType))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResponseExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResponseExpression.cs new file mode 100644 index 0000000..d3899e8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResponseExpression.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using Azure; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record ResponseExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public ValueExpression Status => Property(nameof(Response.Status)); + + public StreamExpression ContentStream => new(Property(nameof(Response.ContentStream))); + public BinaryDataExpression Content => new(Property(nameof(Response.Content))); + + public static NullableResponseExpression FromValue(ValueExpression value, ResponseExpression rawResponse) + => new(new InvokeStaticMethodExpression(typeof(Response), nameof(Response.FromValue), new[] { value, rawResponse })); + + public static NullableResponseExpression FromValue(CSharpType explicitValueType, ValueExpression value, ResponseExpression rawResponse) + => new(new InvokeStaticMethodExpression(typeof(Response), nameof(Response.FromValue), new[] { value, rawResponse }, new[] { explicitValueType })); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResponseWithHeadersExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResponseWithHeadersExpression.cs new file mode 100644 index 0000000..46aba05 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/ResponseWithHeadersExpression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record ResponseWithHeadersExpression(ValueExpression Untyped) : TypedValueExpression(typeof(ResponseWithHeaders<,>), Untyped) + { + public static ResponseWithHeadersExpression FromValue(ValueExpression value, ValueExpression headers, ResponseExpression response) + => new(new InvokeStaticMethodExpression(typeof(ResponseWithHeaders), nameof(ResponseWithHeaders.FromValue), new[] { value, headers, response })); + + public static ResponseWithHeadersExpression FromValue(CSharpType explicitValueType, ValueExpression value, TypedValueExpression headers, ResponseExpression response) + => new(new InvokeStaticMethodExpression(typeof(ResponseWithHeaders), nameof(ResponseWithHeaders.FromValue), new[] { value, headers, response }, new[] { explicitValueType, headers.Type })); + + public static ResponseWithHeadersExpression FromValue(ValueExpression headers, ResponseExpression response) + => new(new InvokeStaticMethodExpression(typeof(ResponseWithHeaders), nameof(ResponseWithHeaders.FromValue), new[] { headers, response })); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/StringRequestContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/StringRequestContentExpression.cs new file mode 100644 index 0000000..7807684 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/StringRequestContentExpression.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record StringRequestContentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/XmlWriterContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/XmlWriterContentExpression.cs new file mode 100644 index 0000000..b67aea0 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Azure/XmlWriterContentExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure +{ + internal sealed record XmlWriterContentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public XmlWriterExpression XmlWriter => new(Property(nameof(XmlWriterContent.XmlWriter))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/BinaryDataExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/BinaryDataExpression.cs new file mode 100644 index 0000000..00b192b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/BinaryDataExpression.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record BinaryDataExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public FrameworkTypeExpression ToObjectFromJson(Type responseType) + => new(responseType, new InvokeInstanceMethodExpression(Untyped, nameof(BinaryData.ToObjectFromJson), Array.Empty(), new[] { new CSharpType(responseType) }, false)); + + public static BinaryDataExpression FromStream(StreamExpression stream, bool async) + { + var methodName = async ? nameof(BinaryData.FromStreamAsync) : nameof(BinaryData.FromStream); + return new BinaryDataExpression(InvokeStatic(methodName, stream, async)); + } + + public static BinaryDataExpression FromStream(ValueExpression stream, bool async) + { + var methodName = async ? nameof(BinaryData.FromStreamAsync) : nameof(BinaryData.FromStream); + return new(InvokeStatic(methodName, stream, async)); + } + + public ValueExpression ToMemory() => Invoke(nameof(BinaryData.ToMemory)); + + public StreamExpression ToStream() => new(Invoke(nameof(BinaryData.ToStream))); + + public ListExpression ToArray() => new(typeof(byte[]), Invoke(nameof(BinaryData.ToArray))); + + public static BinaryDataExpression FromBytes(ValueExpression data) + => new(InvokeStatic(nameof(BinaryData.FromBytes), data)); + + public static BinaryDataExpression FromObjectAsJson(ValueExpression data) + => new(InvokeStatic(nameof(BinaryData.FromObjectAsJson), data)); + + public static BinaryDataExpression FromString(ValueExpression data) + => new(InvokeStatic(nameof(BinaryData.FromString), data)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/BoolExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/BoolExpression.cs new file mode 100644 index 0000000..a9cc996 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/BoolExpression.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record BoolExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public BoolExpression Or(ValueExpression other) => new(new BinaryOperatorExpression(" || ", this, other)); + + public BoolExpression And(ValueExpression other) => new(new BinaryOperatorExpression(" && ", this, other)); + + public static BoolExpression True => new(new ConstantExpression(new Constant(true, typeof(bool)))); + + public static BoolExpression False => new(new ConstantExpression(new Constant(false, typeof(bool)))); + + public static BoolExpression Is(ValueExpression untyped, CSharpType comparisonType) => new(new BinaryOperatorExpression("is", untyped, comparisonType)); + + public static BoolExpression Is(ValueExpression untyped, DeclarationExpression declaration) => new(new BinaryOperatorExpression("is", untyped, declaration)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/CancellationTokenExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/CancellationTokenExpression.cs new file mode 100644 index 0000000..c35c2ab --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/CancellationTokenExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record CancellationTokenExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public BoolExpression CanBeCanceled => new(Property(nameof(CancellationToken.CanBeCanceled))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/CharExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/CharExpression.cs new file mode 100644 index 0000000..0e4a4ac --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/CharExpression.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; + +internal sealed record CharExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) +{ + public StringExpression InvokeToString(ValueExpression cultureInfo) => new(Invoke(nameof(char.ToString), cultureInfo)); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DateTimeOffsetExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DateTimeOffsetExpression.cs new file mode 100644 index 0000000..2cdd916 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DateTimeOffsetExpression.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record DateTimeOffsetExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static DateTimeOffsetExpression Now => new(StaticProperty(nameof(DateTimeOffset.Now))); + public static DateTimeOffsetExpression UtcNow => new(StaticProperty(nameof(DateTimeOffset.UtcNow))); + + public static DateTimeOffsetExpression FromUnixTimeSeconds(ValueExpression expression) + => new(InvokeStatic(nameof(DateTimeOffset.FromUnixTimeSeconds), expression)); + + public StringExpression InvokeToString(StringExpression format, ValueExpression formatProvider) + => new(Invoke(nameof(DateTimeOffset.ToString), new[] { format, formatProvider })); + + public LongExpression ToUnixTimeSeconds() + => new(Invoke(nameof(DateTimeOffset.ToUnixTimeSeconds))); + + public DateTimeOffsetExpression ToUniversalTime() + => new(Invoke(nameof(DateTimeOffset.ToUniversalTime))); + + public static DateTimeOffsetExpression Parse(string s) => Parse(Literal(s)); + + public static DateTimeOffsetExpression Parse(ValueExpression value) + => new(InvokeStatic(nameof(DateTimeOffset.Parse), value)); + + public static DateTimeOffsetExpression Parse(ValueExpression value, ValueExpression formatProvider, ValueExpression style) + => new(InvokeStatic(nameof(DateTimeOffset.Parse), new[] { value, formatProvider, style })); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DictionaryExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DictionaryExpression.cs new file mode 100644 index 0000000..0cc91d3 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DictionaryExpression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record DictionaryExpression(CSharpType KeyType, CSharpType ValueType, ValueExpression Untyped) : TypedValueExpression(new CSharpType(typeof(Dictionary<,>), KeyType, ValueType), Untyped) + { + public MethodBodyStatement Add(ValueExpression key, ValueExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(Dictionary.Add), key, value); + + public MethodBodyStatement Add(KeyValuePairExpression pair) + => new InvokeInstanceMethodStatement(Untyped, nameof(Dictionary.Add), pair); + + public BoolExpression ContainsKey(ValueExpression key) => new(Invoke(nameof(Dictionary.ContainsKey), key)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DoubleExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DoubleExpression.cs new file mode 100644 index 0000000..c4aab30 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/DoubleExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; + +internal sealed record DoubleExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) +{ + public static DoubleExpression MaxValue => new(StaticProperty(nameof(double.MaxValue))); + + public static BoolExpression IsNan(ValueExpression d) => new(new InvokeStaticMethodExpression(typeof(double), nameof(double.IsNaN), new[] { d })); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnumExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnumExpression.cs new file mode 100644 index 0000000..bfcb5a6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnumExpression.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record EnumExpression(EnumType EnumType, ValueExpression Untyped) : TypedValueExpression(EnumType.Type, Untyped) + { + public TypedValueExpression ToSerial() + => EnumType.SerializationMethodName is {} name + ? EnumType.IsExtensible + ? new FrameworkTypeExpression(EnumType.ValueType.FrameworkType, Untyped.Invoke(name)) + : new FrameworkTypeExpression(EnumType.ValueType.FrameworkType, new InvokeStaticMethodExpression(EnumType.Type, name, new[] { Untyped }, null, true)) + : EnumType is { IsExtensible: true, IsStringValueType: true } + ? Untyped.InvokeToString() + : throw new InvalidOperationException($"No conversion available fom {EnumType.Type.Name}"); + + public static TypedValueExpression ToEnum(EnumType enumType, ValueExpression value) + => enumType.IsExtensible + ? new EnumExpression(enumType, Snippets.New.Instance(enumType.Type, value)) + : new EnumExpression(enumType, new InvokeStaticMethodExpression(enumType.Type, $"To{enumType.Declaration.Name}", new[] { value }, null, true)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnumerableExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnumerableExpression.cs new file mode 100644 index 0000000..421bd1b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnumerableExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record EnumerableExpression(CSharpType ItemType, ValueExpression Untyped) : TypedValueExpression(new CSharpType(typeof(IEnumerable<>), ItemType), Untyped) + { + public BoolExpression Any() => new(new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.Any), new[] { Untyped }, CallAsExtension: true)); + public EnumerableExpression Select(TypedFuncExpression selector) => new(selector.Inner.Type, new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.Select), new[] { Untyped, selector }, CallAsExtension: true)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnvironmentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnvironmentExpression.cs new file mode 100644 index 0000000..454e662 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/EnvironmentExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record EnvironmentExpression(ValueExpression Untyped) : TypedValueExpression(typeof(Environment), Untyped) + { + public static StringExpression NewLine() => new(new TypeReference(typeof(Environment)).Property(nameof(Environment.NewLine))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/FrameworkTypeExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/FrameworkTypeExpression.cs new file mode 100644 index 0000000..aa82158 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/FrameworkTypeExpression.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + /// + /// Represents expression which has a return value of a framework type + /// + /// Framework type + /// + internal sealed record FrameworkTypeExpression(Type FrameworkType, ValueExpression Untyped) : TypedValueExpression(FrameworkType, Untyped); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/GuidExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/GuidExpression.cs new file mode 100644 index 0000000..5878f7f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/GuidExpression.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record GuidExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static GuidExpression NewGuid() => new(InvokeStatic(nameof(Guid.NewGuid))); + + public static GuidExpression Parse(ValueExpression expression) => new(InvokeStatic(nameof(Guid.Parse), expression)); + + public static GuidExpression Parse(string input) => Parse(Literal(input)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/HttpContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/HttpContentExpression.cs new file mode 100644 index 0000000..7cf60d7 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/HttpContentExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information.using System; + +using System.Net.Http; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record HttpContentExpression(ValueExpression Untyped): TypedValueExpression(Untyped) + { + public HttpContentHeadersExpression Headers => new HttpContentHeadersExpression(Property(nameof(HttpContent.Headers))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/HttpContentHeadersExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/HttpContentHeadersExpression.cs new file mode 100644 index 0000000..8abf492 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/HttpContentHeadersExpression.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net.Http.Headers; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record HttpContentHeadersExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public ValueExpression ContentType => Property(nameof(HttpContentHeaders.ContentType)); + public ValueExpression ContentLength => Property(nameof(HttpContentHeaders.ContentLength)); + public ValueExpression ContentDisposition => Property(nameof(HttpContentHeaders.ContentDisposition)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/IntExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/IntExpression.cs new file mode 100644 index 0000000..2d24967 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/IntExpression.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record IntExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static IntExpression MaxValue => new(StaticProperty(nameof(int.MaxValue))); + + public IntExpression Add(IntExpression value) => Operator("+", value); + public IntExpression Minus(IntExpression value) => Operator("-", value); + public IntExpression Multiply(IntExpression value) => Operator("*", value); + public IntExpression DivideBy(IntExpression value) => Operator("/", value); + + public static IntExpression operator +(IntExpression left, IntExpression right) => left.Add(right); + public static IntExpression operator -(IntExpression left, IntExpression right) => left.Minus(right); + public static IntExpression operator *(IntExpression left, IntExpression right) => left.Multiply(right); + public static IntExpression operator /(IntExpression left, IntExpression right) => left.DivideBy(right); + + private IntExpression Operator(string op, IntExpression other) => new(new BinaryOperatorExpression(op, this, other)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonDocumentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonDocumentExpression.cs new file mode 100644 index 0000000..77eb129 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonDocumentExpression.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record JsonDocumentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public JsonElementExpression RootElement => new(Property(nameof(JsonDocument.RootElement))); + + public static JsonDocumentExpression ParseValue(ValueExpression reader) => new(InvokeStatic(nameof(JsonDocument.ParseValue), reader)); + public static JsonDocumentExpression Parse(ValueExpression json) => new(InvokeStatic(nameof(JsonDocument.Parse), json)); + public static JsonDocumentExpression Parse(BinaryDataExpression binaryData) => new(InvokeStatic(nameof(JsonDocument.Parse), binaryData)); + public static JsonDocumentExpression Parse(StreamExpression stream) => new(InvokeStatic(nameof(JsonDocument.Parse), stream)); + + public static JsonDocumentExpression Parse(StreamExpression stream, bool async) + { + // Sync and async methods have different set of parameters + return async + // non-azure libraries do not have cancellationToken parameter + ? new JsonDocumentExpression(InvokeStatic(nameof(JsonDocument.ParseAsync), new[] { stream, Snippets.Default, Configuration.IsBranded ? KnownParameters.CancellationTokenParameter : Snippets.Default }, true)) + : new JsonDocumentExpression(InvokeStatic(nameof(JsonDocument.Parse), stream)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonElementExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonElementExpression.cs new file mode 100644 index 0000000..5eb08f9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonElementExpression.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record JsonElementExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public JsonValueKindExpression ValueKind => new(Property(nameof(JsonElement.ValueKind))); + public EnumerableExpression EnumerateArray() => new(typeof(JsonElement), Invoke(nameof(JsonElement.EnumerateArray))); + public EnumerableExpression EnumerateObject() => new(typeof(JsonProperty), Invoke(nameof(JsonElement.EnumerateObject))); + public JsonElementExpression this[int index] => new(new IndexerExpression(Untyped, Int(index))); + public JsonElementExpression GetProperty(string propertyName) => new(Invoke(nameof(JsonElement.GetProperty), Literal(propertyName))); + + public ValueExpression InvokeClone() => Invoke(nameof(JsonElement.Clone)); + public ValueExpression GetArrayLength() => Invoke(nameof(JsonElement.GetArrayLength)); + public ValueExpression GetBoolean() => Invoke(nameof(JsonElement.GetBoolean)); + public ValueExpression GetBytesFromBase64() => Invoke(nameof(JsonElement.GetBytesFromBase64)); + public ValueExpression GetBytesFromBase64(string? format) => Extensible.JsonElement.GetBytesFromBase64(this, format); + public ValueExpression GetChar() => Extensible.JsonElement.GetChar(this); + public ValueExpression GetDateTimeOffset(string? format) => Extensible.JsonElement.GetDateTimeOffset(this, format); + public ValueExpression GetDateTime() => Invoke(nameof(JsonElement.GetDateTime)); + public ValueExpression GetDecimal() => Invoke(nameof(JsonElement.GetDecimal)); + public ValueExpression GetDouble() => Invoke(nameof(JsonElement.GetDouble)); + public ValueExpression GetGuid() => Invoke(nameof(JsonElement.GetGuid)); + public ValueExpression GetSByte() => Invoke(nameof(JsonElement.GetSByte)); + public ValueExpression GetByte() => Invoke(nameof(JsonElement.GetByte)); + public ValueExpression GetInt16() => Invoke(nameof(JsonElement.GetInt16)); + public ValueExpression GetInt32() => Invoke(nameof(JsonElement.GetInt32)); + public ValueExpression GetInt64() => Invoke(nameof(JsonElement.GetInt64)); + public ValueExpression GetObject() => Extensible.JsonElement.GetObject(this); + public StringExpression GetRawText() => new(Invoke(nameof(JsonElement.GetRawText))); + public ValueExpression GetSingle() => Untyped.Invoke(nameof(JsonElement.GetSingle)); + public StringExpression GetString() => new(Untyped.Invoke(nameof(JsonElement.GetString))); + public ValueExpression GetTimeSpan(string? format) => Extensible.JsonElement.GetTimeSpan(this, format); + + public BoolExpression ValueKindEqualsNull() + => new(new BinaryOperatorExpression("==", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.Null))); + + public BoolExpression ValueKindNotEqualsUndefined() + => new(new BinaryOperatorExpression("!=", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.Undefined))); + + public BoolExpression ValueKindEqualsString() + => new(new BinaryOperatorExpression("==", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.String))); + + public MethodBodyStatement WriteTo(ValueExpression writer) => new InvokeInstanceMethodStatement(Untyped, nameof(JsonElement.WriteTo), new[] { writer }, false); + + public BoolExpression TryGetProperty(string propertyName, out JsonElementExpression discriminator) + { + var discriminatorDeclaration = new VariableReference(typeof(JsonElement), "discriminator"); + discriminator = new JsonElementExpression(discriminatorDeclaration); + var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetProperty), new ValueExpression[] { Literal(propertyName), new DeclarationExpression(discriminatorDeclaration, true) }, null, false); + return new BoolExpression(invocation); + } + + public BoolExpression TryGetInt32(out IntExpression intValue) + { + var intValueDeclaration = new VariableReference(typeof(int), "intValue"); + intValue = new IntExpression(intValueDeclaration); + var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetInt32), new ValueExpression[] { new DeclarationExpression(intValueDeclaration, true) }, null, false); + return new BoolExpression(invocation); + } + + public BoolExpression TryGetInt64(out LongExpression longValue) + { + var longValueDeclaration = new VariableReference(typeof(long), "longValue"); + longValue = new LongExpression(longValueDeclaration); + var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetInt64), new ValueExpression[] { new DeclarationExpression(longValueDeclaration, true) }, null, false); + return new BoolExpression(invocation); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonPropertyExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonPropertyExpression.cs new file mode 100644 index 0000000..41ec8db --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonPropertyExpression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Input; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record JsonPropertyExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public StringExpression Name => new(Property(nameof(JsonProperty.Name))); + public JsonElementExpression Value => new(Property(nameof(JsonProperty.Value))); + + public BoolExpression NameEquals(string value) => new(Invoke(nameof(JsonProperty.NameEquals), LiteralU8(value))); + + public MethodBodyStatement ThrowNonNullablePropertyIsNull() => Extensible.JsonElement.ThrowNonNullablePropertyIsNull(this); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonSerializerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonSerializerExpression.cs new file mode 100644 index 0000000..b6ca495 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonSerializerExpression.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal static class JsonSerializerExpression + { + public static InvokeStaticMethodExpression Serialize(ValueExpression writer, ValueExpression value, ValueExpression? options = null) + { + var arguments = options is null + ? new[] { writer, value } + : new[] { writer, value, options }; + return new InvokeStaticMethodExpression(typeof(JsonSerializer), nameof(JsonSerializer.Serialize), arguments); + } + + public static InvokeStaticMethodExpression Deserialize(JsonElementExpression element, CSharpType serializationType, ValueExpression? options = null) + { + var arguments = options is null + ? new[] { element.GetRawText() } + : new[] { element.GetRawText(), options }; + return new InvokeStaticMethodExpression(typeof(JsonSerializer), nameof(JsonSerializer.Deserialize), arguments, new[] { serializationType }); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonValueKindExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonValueKindExpression.cs new file mode 100644 index 0000000..da91cdd --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/JsonValueKindExpression.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record JsonValueKindExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static JsonValueKindExpression String => InvokeStaticProperty(nameof(JsonValueKind.String)); + public static JsonValueKindExpression Number => InvokeStaticProperty(nameof(JsonValueKind.Number)); + public static JsonValueKindExpression True => InvokeStaticProperty(nameof(JsonValueKind.True)); + public static JsonValueKindExpression False => InvokeStaticProperty(nameof(JsonValueKind.False)); + public static JsonValueKindExpression Undefined => InvokeStaticProperty(nameof(JsonValueKind.Undefined)); + public static JsonValueKindExpression Null => InvokeStaticProperty(nameof(JsonValueKind.Null)); + public static JsonValueKindExpression Array => InvokeStaticProperty(nameof(JsonValueKind.Array)); + public static JsonValueKindExpression Object => InvokeStaticProperty(nameof(JsonValueKind.Object)); + + private static JsonValueKindExpression InvokeStaticProperty(string name) + => new(new MemberExpression(typeof(JsonValueKind), name)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/KeyValuePairExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/KeyValuePairExpression.cs new file mode 100644 index 0000000..9eff69e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/KeyValuePairExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record KeyValuePairExpression(CSharpType KeyType, CSharpType ValueType, ValueExpression Untyped) : TypedValueExpression(GetType(KeyType, ValueType), Untyped) + { + public TypedValueExpression Key => new TypedMemberExpression(Untyped, nameof(KeyValuePair.Key), KeyType); + public TypedValueExpression Value => new TypedMemberExpression(Untyped, nameof(KeyValuePair.Value), ValueType); + + public static CSharpType GetType(CSharpType keyType, CSharpType valueType) => new(typeof(KeyValuePair<,>), keyType, valueType); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/ListExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/ListExpression.cs new file mode 100644 index 0000000..e16ffee --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/ListExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record ListExpression(CSharpType ItemType, ValueExpression Untyped) : TypedValueExpression(new CSharpType(typeof(List<>), ItemType), Untyped) + { + public MethodBodyStatement Add(ValueExpression item) => new InvokeInstanceMethodStatement(Untyped, nameof(List.Add), item); + + public ValueExpression ToArray() => Invoke(nameof(List.ToArray)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/LongExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/LongExpression.cs new file mode 100644 index 0000000..f6cfe4e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/LongExpression.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; + +internal sealed record LongExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) +{ + public StringExpression InvokeToString(ValueExpression formatProvider) + => new(Invoke(nameof(long.ToString), formatProvider)); + + public static LongExpression Parse(StringExpression value, ValueExpression formatProvider) + => new(new InvokeStaticMethodExpression(typeof(long), nameof(long.Parse), new[] { value, formatProvider })); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/ModelReaderWriterOptionsExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/ModelReaderWriterOptionsExpression.cs new file mode 100644 index 0000000..670caee --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/ModelReaderWriterOptionsExpression.cs @@ -0,0 +1,16 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal record ModelReaderWriterOptionsExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static readonly ModelReaderWriterOptionsExpression Wire = ModelSerializationExtensionsProvider.Instance.WireOptions; + + public ValueExpression Format => new MemberExpression(this, nameof(ModelReaderWriterOptions.Format)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/MultipartFormDataContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/MultipartFormDataContentExpression.cs new file mode 100644 index 0000000..7001879 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/MultipartFormDataContentExpression.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net.Http; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record MultipartFormDataContentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public MethodBodyStatement Add(ValueExpression content, ValueExpression name, ValueExpression fileName) => new InvokeInstanceMethodStatement(Untyped, nameof(MultipartFormDataContent.Add), new[] { content, name, fileName }, false); + public MethodBodyStatement Add(ValueExpression content, ValueExpression name) => new InvokeInstanceMethodStatement(Untyped, nameof(MultipartFormDataContent.Add), new[] { content, name }, false); + public HttpContentHeadersExpression Headers => new HttpContentHeadersExpression(Property(nameof(MultipartFormDataContent.Headers))); + public MethodBodyStatement CopyToAsync(ValueExpression stream, ValueExpression cancellatinToken, bool isCallAsync) => new InvokeInstanceMethodStatement(Untyped, nameof(MultipartFormDataContent.CopyToAsync), new[] { stream, cancellatinToken }, isCallAsync); + public MethodBodyStatement CopyToAsync(ValueExpression stream, bool isCallAsync) => new InvokeInstanceMethodStatement(Untyped, nameof(MultipartFormDataContent.CopyToAsync), new[] { stream }, isCallAsync); + public ValueExpression CopyToAsyncExpression(ValueExpression stream) => new InvokeInstanceMethodExpression(Untyped, nameof(MultipartFormDataContent.CopyToAsync), new[] { stream }, null, false); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/SerializableObjectTypeExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/SerializableObjectTypeExpression.cs new file mode 100644 index 0000000..e1bf08f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/SerializableObjectTypeExpression.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record SerializableObjectTypeExpression(SerializableObjectType ObjectType, ValueExpression Untyped) : TypedValueExpression(ObjectType.Type, Untyped) + { + public static MemberExpression FromResponseDelegate(SerializableObjectType serializableObjectType) + => new(new TypeReference(serializableObjectType.Type), Configuration.ApiTypes.FromResponseName); + + public static MemberExpression DeserializeDelegate(SerializableObjectType serializableObjectType) + => new(new TypeReference(serializableObjectType.Type), $"Deserialize{serializableObjectType.Declaration.Name}"); + + public static SerializableObjectTypeExpression FromResponse(SerializableObjectType serializableObjectType, ResponseExpression response) + => new(serializableObjectType, new InvokeStaticMethodExpression(serializableObjectType.Type, Configuration.ApiTypes.FromResponseName, new[] { response })); + + public static SerializableObjectTypeExpression Deserialize(SerializableObjectType model, ValueExpression element, ValueExpression? options = null) + { + var arguments = options == null ? new[] { element } : new[] { element, options }; + return new(model, new InvokeStaticMethodExpression(model.Type, $"Deserialize{model.Declaration.Name}", arguments)); + } + public RequestContentExpression ToRequestContent() => new(Untyped.Invoke(Configuration.ApiTypes.ToRequestContentName)); + public RequestContentExpression ToMultipartRequestContent() => new(Untyped.Invoke(Configuration.ApiTypes.ToMultipartRequestContentName)); + + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StreamExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StreamExpression.cs new file mode 100644 index 0000000..b64ecf9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StreamExpression.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record StreamExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public MethodBodyStatement CopyTo(StreamExpression destination) => new InvokeInstanceMethodStatement(Untyped, nameof(Stream.CopyTo), destination); + + public ValueExpression Position => new TypedMemberExpression(this, nameof(Stream.Position), typeof(long)); + public ValueExpression GetBuffer => new InvokeInstanceMethodExpression(this, nameof(MemoryStream.GetBuffer), Array.Empty(), null, false); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StreamReaderExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StreamReaderExpression.cs new file mode 100644 index 0000000..59b1395 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StreamReaderExpression.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record StreamReaderExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public StringExpression ReadToEnd(bool async) + { + var methodName = async ? nameof(StreamReader.ReadToEndAsync) : nameof(StreamReader.ReadToEnd); + return new(Invoke(methodName, Array.Empty(), async)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringBuilderExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringBuilderExpression.cs new file mode 100644 index 0000000..ea9e54f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringBuilderExpression.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record StringBuilderExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public IntExpression Length => new(Property(nameof(StringBuilder.Length))); + + public MethodBodyStatement Append(StringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); + + public MethodBodyStatement AppendLine(StringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.AppendLine), value); + + public MethodBodyStatement Append(ValueExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); + + public MethodBodyStatement AppendLine(ValueExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.AppendLine), value); + + public MethodBodyStatement Append(string value) => Append(Snippets.Literal(value)); + + public MethodBodyStatement AppendLine(string value) => AppendLine(Snippets.Literal(value)); + + public MethodBodyStatement Append(FormattableStringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); + + public MethodBodyStatement AppendLine(FormattableStringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.AppendLine), value); + + public MethodBodyStatement Remove(ValueExpression startIndex, ValueExpression length) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Remove), startIndex, length); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringComparerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringComparerExpression.cs new file mode 100644 index 0000000..f6fc9c6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringComparerExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record StringComparerExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public BoolExpression Equals(StringExpression left, StringExpression right) => new(Invoke(nameof(StringComparer.Equals), left, right)); + + public static StringComparerExpression Ordinal => new(StaticProperty(nameof(StringComparer.Ordinal))); + public static StringComparerExpression OrdinalIgnoreCase => new(StaticProperty(nameof(StringComparer.OrdinalIgnoreCase))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringExpression.cs new file mode 100644 index 0000000..b7e01e5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/StringExpression.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record StringExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public CharExpression Index(ValueExpression index) => new(new IndexerExpression(this, index)); + public CharExpression Index(int index) => Index(Literal(index)); + public ValueExpression Length => Property(nameof(string.Length)); + + public static BoolExpression Equals(StringExpression left, StringExpression right, StringComparison comparisonType) + => new(InvokeStatic(nameof(string.Equals), new[] { left, right, FrameworkEnumValue(comparisonType) })); + + public static StringExpression Format(StringExpression format, params ValueExpression[] args) + => new(new InvokeStaticMethodExpression(typeof(string), nameof(string.Format), args.Prepend(format).ToArray())); + + public static BoolExpression IsNullOrWhiteSpace(StringExpression value, params ValueExpression[] args) + => new(new InvokeStaticMethodExpression(typeof(string), nameof(string.IsNullOrWhiteSpace), args.Prepend(value).ToArray())); + + public static StringExpression Join(ValueExpression separator, ValueExpression values) + => new(new InvokeStaticMethodExpression(typeof(string), nameof(string.Join), new[] { separator, values })); + + public StringExpression Substring(ValueExpression startIndex) + => new(new InvokeInstanceMethodExpression(this, nameof(string.Substring), new[] { startIndex }, null, false)); + public ValueExpression ToCharArray() + => new InvokeInstanceMethodExpression(this, nameof(string.ToCharArray), Array.Empty(), null, false); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/BinaryContentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/BinaryContentExpression.cs new file mode 100644 index 0000000..c6ee89c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/BinaryContentExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.ClientModel; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record BinaryContentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static BinaryContentExpression Create(ValueExpression serializable) => new(InvokeStatic(nameof(BinaryContent.Create), serializable)); + + public static BinaryContentExpression Create(ValueExpression serializable, ModelReaderWriterOptionsExpression options, CSharpType? typeArgument = null) => new(new InvokeStaticMethodExpression(typeof(BinaryContent), nameof(BinaryContent.Create), new[] { serializable, options }, TypeArguments: typeArgument != null ? new[] { typeArgument } : null)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/ClientPipelineExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/ClientPipelineExpression.cs new file mode 100644 index 0000000..aea7304 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/ClientPipelineExpression.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Output.Models.Types.System; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record ClientPipelineExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public PipelineMessageExpression CreateMessage(RequestOptionsExpression requestOptions, ValueExpression responseClassifier) => new(Invoke(nameof(ClientPipeline.CreateMessage), requestOptions, responseClassifier)); + + public PipelineResponseExpression ProcessMessage(TypedValueExpression message, RequestOptionsExpression? requestOptions, bool async) + { + var arguments = new List + { + Untyped, + message, + requestOptions ?? Snippets.Null + }; + + return ClientPipelineExtensionsProvider.Instance.ProcessMessage(arguments, async); + } + + public ClientResultExpression ProcessHeadAsBoolMessage(TypedValueExpression message, RequestOptionsExpression? requestContext, bool async) + { + var arguments = new List + { + Untyped, + message, + requestContext ?? Snippets.Null + }; + + return ClientPipelineExtensionsProvider.Instance.ProcessHeadAsBoolMessage(arguments, async); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/ClientResultExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/ClientResultExpression.cs new file mode 100644 index 0000000..1cf32d9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/ClientResultExpression.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record ClientResultExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public ValueExpression Value => Property(nameof(ClientResult.Value)); + public BinaryDataExpression Content => throw new InvalidOperationException("Result does not have a Content property"); + public StreamExpression ContentStream => throw new InvalidOperationException("Result does not have a ContentStream property"); + + public static ClientResultExpression FromResponse(PipelineResponseExpression response) + => new(InvokeStatic(nameof(ClientResult.FromResponse), response)); + + public static ClientResultExpression FromValue(ValueExpression value, PipelineResponseExpression response) + => new(InvokeStatic(nameof(ClientResult.FromValue), value, response)); + + public static ClientResultExpression FromValue(CSharpType explicitValueType, ValueExpression value, PipelineResponseExpression response) + => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, response }, new[] { explicitValueType })); + + public ClientResultExpression FromValue(ValueExpression value) + => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, this })); + + public ClientResultExpression FromValue(CSharpType explicitValueType, ValueExpression value) + => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, this }, new[] { explicitValueType })); + + public PipelineResponseExpression GetRawResponse() => new(Invoke(nameof(ClientResult.GetRawResponse))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineMessageExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineMessageExpression.cs new file mode 100644 index 0000000..55b90cd --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineMessageExpression.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record PipelineMessageExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public PipelineRequestExpression Request => new(Property(nameof(PipelineMessage.Request))); + + public PipelineResponseExpression Response => new(Property(nameof(PipelineMessage.Response))); + + public BoolExpression BufferResponse => new(Property(nameof(PipelineMessage.BufferResponse))); + + public PipelineResponseExpression ExtractResponse() => new(new InvokeInstanceMethodExpression(Untyped, nameof(PipelineMessage.ExtractResponse), Array.Empty(), false)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineRequestExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineRequestExpression.cs new file mode 100644 index 0000000..6f42619 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineRequestExpression.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record PipelineRequestExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public TypedValueExpression Uri => new FrameworkTypeExpression(typeof(Uri), Property(nameof(PipelineRequest.Uri))); + public BinaryContentExpression Content => new(Property(nameof(PipelineRequest.Content))); + public MethodBodyStatement SetMethod(string method) => Assign(Untyped.Property("Method"), Literal(method)); + public MethodBodyStatement SetHeaderValue(string name, StringExpression value) + => new InvokeInstanceMethodStatement(Untyped.Property(nameof(PipelineRequest.Headers)), nameof(PipelineRequestHeaders.Set), Literal(name), value); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineResponseExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineResponseExpression.cs new file mode 100644 index 0000000..b3d89dc --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/PipelineResponseExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record PipelineResponseExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public BinaryDataExpression Content => new(Property(nameof(PipelineResponse.Content))); + + public StreamExpression ContentStream => new(Property(nameof(PipelineResponse.ContentStream))); + + public BoolExpression IsError => new(Property(nameof(PipelineResponse.IsError))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/RequestOptionsExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/RequestOptionsExpression.cs new file mode 100644 index 0000000..fc6da83 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/RequestOptionsExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record RequestOptionsExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static RequestOptionsExpression FromCancellationToken() + => new(new InvokeStaticMethodExpression(null, "FromCancellationToken", new ValueExpression[] { KnownParameters.CancellationTokenParameter })); + + public ValueExpression ErrorOptions => Property(nameof(RequestOptions.ErrorOptions)); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/RequestUriExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/RequestUriExpression.cs new file mode 100644 index 0000000..0c40361 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/System/RequestUriExpression.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Serialization; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System +{ + internal sealed record RequestUriExpression() : ValueExpression + { + public InvokeInstanceMethodExpression ToUri() => Invoke("ToUri"); + + public MethodBodyStatement Reset(ValueExpression uri) => new InvokeInstanceMethodStatement(this, "Reset", uri); + + public MethodBodyStatement AppendRawPathOrQueryOrHostOrScheme(string value, bool escape) => new InvokeInstanceMethodStatement(this, "AppendRawPathOrQueryOrHostOrScheme", Literal(value), Bool(escape)); + public MethodBodyStatement AppendRawPathOrQueryOrHostOrScheme(ValueExpression value, bool escape) => new InvokeInstanceMethodStatement(this, "AppendRawPathOrQueryOrHostOrScheme", value, Bool(escape)); + + public MethodBodyStatement AppendPath(string value, bool escape) => new InvokeInstanceMethodStatement(this, "AppendPath", Literal(value), Bool(escape)); + public MethodBodyStatement AppendPath(ValueExpression value, bool escape) => new InvokeInstanceMethodStatement(this, "AppendPath", value, Bool(escape)); + public MethodBodyStatement AppendPath(ValueExpression value, string formatSpecifier, bool escape) => new InvokeInstanceMethodStatement(this, "AppendPath", new[]{ value, Literal(formatSpecifier), Bool(escape) }, false); + + public MethodBodyStatement AppendPath(ValueExpression value, SerializationFormat format, bool escape) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AppendPath(value, formatSpecifier, escape) + : AppendPath(value, escape); + + public MethodBodyStatement AppendQuery(string name, TypedValueExpression value, bool escape) + => new InvokeInstanceMethodStatement(this, "AppendQuery", new[]{ Literal(name), value, Bool(escape) }, false); + public MethodBodyStatement AppendQuery(string name, TypedValueExpression value, string format, bool escape) + => new InvokeInstanceMethodStatement(this, "AppendQuery", new[]{ Literal(name), value, Literal(format), Bool(escape) }, false); + public MethodBodyStatement AppendQuery(string name, TypedValueExpression value, SerializationFormat format, bool escape) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AppendQuery(name, value, formatSpecifier, escape) + : AppendQuery(name, value, escape); + + public MethodBodyStatement AppendQueryDelimited(string name, ValueExpression value, string delimiter, bool escape) + => new InvokeInstanceMethodStatement(this, "AppendQueryDelimited", new[]{ Literal(name), value, Literal(delimiter), Bool(escape) }, false); + public MethodBodyStatement AppendQueryDelimited(string name, ValueExpression value, string delimiter, string format, bool escape) + => new InvokeInstanceMethodStatement(this, "AppendQueryDelimited", new[]{ Literal(name), value, Literal(delimiter), Literal(format), Bool(escape) }, false); + public MethodBodyStatement AppendQueryDelimited(string name, ValueExpression value, string delimiter, SerializationFormat format, bool escape) + => format.ToFormatSpecifier() is { } formatSpecifier + ? AppendQueryDelimited(name, value, delimiter, formatSpecifier, escape) + : AppendQueryDelimited(name, value, delimiter, escape); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/TimeSpanExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/TimeSpanExpression.cs new file mode 100644 index 0000000..3865a72 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/TimeSpanExpression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record TimeSpanExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public StringExpression InvokeToString(string? format) => new(Invoke(nameof(TimeSpan.ToString), new[] { Literal(format) })); + public StringExpression InvokeToString(ValueExpression format, ValueExpression formatProvider) + => new(Invoke(nameof(TimeSpan.ToString), new[] { format, formatProvider })); + + public static TimeSpanExpression FromSeconds(ValueExpression value) => new(InvokeStatic(nameof(TimeSpan.FromSeconds), value)); + + public static TimeSpanExpression ParseExact(ValueExpression value, ValueExpression format, ValueExpression formatProvider) + => new(new InvokeStaticMethodExpression(typeof(TimeSpan), nameof(TimeSpan.ParseExact), new[] { value, format, formatProvider })); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Utf8JsonWriterExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Utf8JsonWriterExpression.cs new file mode 100644 index 0000000..336ea6d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/Utf8JsonWriterExpression.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Types; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record Utf8JsonWriterExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public LongExpression BytesCommitted => new(Property(nameof(Utf8JsonWriter.BytesCommitted))); + public LongExpression BytesPending => new(Property(nameof(Utf8JsonWriter.BytesPending))); + + public MethodBodyStatement WriteStartObject() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteStartObject)); + public MethodBodyStatement WriteEndObject() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteEndObject)); + public MethodBodyStatement WriteStartArray(ValueExpression name) => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteStartArray), name); + public MethodBodyStatement WriteStartArray() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteStartArray)); + public MethodBodyStatement WriteEndArray() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteEndArray)); + public MethodBodyStatement WritePropertyName(string propertyName) => WritePropertyName(LiteralU8(propertyName)); + public MethodBodyStatement WritePropertyName(ValueExpression propertyName) => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WritePropertyName), propertyName); + public MethodBodyStatement WriteNull(string propertyName) => WriteNull(Literal(propertyName)); + public MethodBodyStatement WriteNull(ValueExpression propertyName) => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteNull), propertyName); + public MethodBodyStatement WriteNullValue() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteNullValue)); + + public MethodBodyStatement WriteNumberValue(ValueExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteNumberValue), value); + + public MethodBodyStatement WriteStringValue(ValueExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteStringValue), value); + + public MethodBodyStatement WriteBooleanValue(ValueExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteBooleanValue), value); + + public MethodBodyStatement WriteRawValue(ValueExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteRawValue), value); + + public MethodBodyStatement WriteNumberValue(ValueExpression value, string? format) + => ModelSerializationExtensionsProvider.Instance.WriteNumberValue(this, value, format); + + public MethodBodyStatement WriteStringValue(ValueExpression value, string? format) + => ModelSerializationExtensionsProvider.Instance.WriteStringValue(this, value, format); + + public MethodBodyStatement WriteObjectValue(TypedValueExpression value, ValueExpression? options = null) + => Configuration.UseModelReaderWriter + ? ModelSerializationExtensionsProvider.Instance.WriteObjectValue(this, value, options: options) + : ModelSerializationExtensionsProvider.Instance.WriteObjectValue(this, value); + + public MethodBodyStatement WriteBase64StringValue(ValueExpression value) + => Invoke(nameof(Utf8JsonWriter.WriteBase64StringValue), value).ToStatement(); + + public MethodBodyStatement WriteBase64StringValue(ValueExpression value, string? format) + => ModelSerializationExtensionsProvider.Instance.WriteBase64StringValue(this, value, format); + + public MethodBodyStatement WriteBinaryData(ValueExpression value) + => new IfElsePreprocessorDirective + ( + "NET6_0_OR_GREATER", + WriteRawValue(value), + new UsingScopeStatement(typeof(JsonDocument), "document", JsonDocumentExpression.Parse(value), out var jsonDocumentVar) + { + JsonSerializerExpression.Serialize(this, new JsonDocumentExpression(jsonDocumentVar).RootElement).ToStatement() + } + ); + + public MethodBodyStatement Flush() + => new InvokeInstanceMethodStatement(this, nameof(Utf8JsonWriter.Flush), Array.Empty(), false); + + public MethodBodyStatement FlushAsync(ValueExpression? cancellationToken = null) + { + var arguments = cancellationToken is null + ? Array.Empty() + : new[] { cancellationToken }; + return new InvokeInstanceMethodStatement(this, nameof(Utf8JsonWriter.FlushAsync), arguments, true); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XAttributeExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XAttributeExpression.cs new file mode 100644 index 0000000..7c2a453 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XAttributeExpression.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Xml.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record XAttributeExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public XNameExpression Name => new(Property(nameof(XAttribute.Name))); + public StringExpression Value => new(Property(nameof(XAttribute.Value))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XContainerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XContainerExpression.cs new file mode 100644 index 0000000..19ba28a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XContainerExpression.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Xml.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record XContainerExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public XElementExpression Element(string name) => new(Invoke(nameof(XDocument.Element), Snippets.Literal(name))); + public EnumerableExpression Elements() => new(typeof(XElement), Invoke(nameof(XDocument.Elements))); + public EnumerableExpression Elements(string name) => new(typeof(XElement), Invoke(nameof(XDocument.Elements), Snippets.Literal(name))); + + public static implicit operator XContainerExpression(XElementExpression xElement) => new(xElement.Untyped); + public static implicit operator XContainerExpression(XDocumentExpression xDocument) => new(xDocument.Untyped); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XDocumentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XDocumentExpression.cs new file mode 100644 index 0000000..c6a6512 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XDocumentExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Xml.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record XDocumentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public static XDocumentExpression Load(StreamExpression stream, LoadOptions loadOptions) + => new(InvokeStatic(nameof(XDocument.Load), stream, FrameworkEnumValue(loadOptions))); + + public XElementExpression Element(string name) => new(Invoke(nameof(XDocument.Element), Literal(name))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XElementExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XElementExpression.cs new file mode 100644 index 0000000..606be33 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XElementExpression.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Xml.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record XElementExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public XNameExpression Name => new(Property(nameof(XElement.Name))); + public StringExpression Value => new(Property(nameof(XElement.Value))); + + public XAttributeExpression Attribute(string name) + => new(Invoke(nameof(XElement.Attribute), Literal(name))); + + public ValueExpression GetBytesFromBase64Value(string? format) => Extensible.XElement.GetBytesFromBase64Value(this, format); + public ValueExpression GetDateTimeOffsetValue(string? format) => Extensible.XElement.GetDateTimeOffsetValue(this, format); + public ValueExpression GetObjectValue(string? format) => Extensible.XElement.GetObjectValue(this, format); + public ValueExpression GetTimeSpanValue(string? format) => Extensible.XElement.GetTimeSpanValue(this, format); + + public static XElementExpression Load(StreamExpression stream) => new(new InvokeStaticMethodExpression(typeof(XElement), nameof(XElement.Load), new[] { stream })); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XNameExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XNameExpression.cs new file mode 100644 index 0000000..5915669 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XNameExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Xml.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record XNameExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public StringExpression LocalName => new(Property(nameof(XName.LocalName))); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XmlWriterExpression.cs b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XmlWriterExpression.cs new file mode 100644 index 0000000..4c5d1a7 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/KnownValueExpressions/XmlWriterExpression.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Xml; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions +{ + internal sealed record XmlWriterExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + { + public MethodBodyStatement WriteStartAttribute(string localName) => new InvokeInstanceMethodStatement(Untyped, nameof(XmlWriter.WriteStartAttribute), Literal(localName)); + public MethodBodyStatement WriteEndAttribute() => new InvokeInstanceMethodStatement(Untyped, nameof(XmlWriter.WriteEndAttribute)); + + public MethodBodyStatement WriteStartElement(string localName) => WriteStartElement(Literal(localName)); + public MethodBodyStatement WriteStartElement(ValueExpression localName) => new InvokeInstanceMethodStatement(Untyped, nameof(XmlWriter.WriteStartElement), localName); + public MethodBodyStatement WriteEndElement() => new InvokeInstanceMethodStatement(Untyped, nameof(XmlWriter.WriteEndElement)); + + public MethodBodyStatement WriteValue(ValueExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(XmlWriter.WriteValue), value); + public MethodBodyStatement WriteValue(ValueExpression value, string format) => Extensible.XmlWriter.WriteValue(this, value, format); + + public MethodBodyStatement WriteValue(ValueExpression value, Type frameworkType, SerializationFormat format) + { + bool writeFormat = frameworkType == typeof(byte[]) || + frameworkType == typeof(DateTimeOffset) || + frameworkType == typeof(DateTime) || + frameworkType == typeof(TimeSpan); + return writeFormat && format.ToFormatSpecifier() is { } formatSpecifier + ? WriteValue(value, formatSpecifier) + : WriteValue(value); + } + + public MethodBodyStatement WriteObjectValue(ValueExpression value, string? nameHint) => Extensible.XmlWriter.WriteObjectValue(this, value, nameHint); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Argument.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Argument.cs new file mode 100644 index 0000000..251f892 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Argument.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + internal class Argument + { + public static MethodBodyStatement AssertNotNull(ValueExpression variable) + { + return ArgumentProvider.Instance.AssertNotNull(variable); + } + + public static MethodBodyStatement AssertNotNullOrEmpty(ValueExpression variable) + { + return ArgumentProvider.Instance.AssertNotNullOrEmpty(variable); + } + + public static MethodBodyStatement AssertNotNullOrWhiteSpace(ValueExpression variable) + { + return ArgumentProvider.Instance.AssertNotNullOrWhiteSpace(variable); + } + + public static MethodBodyStatement ValidateParameter(Parameter parameter) + { + return parameter.Validation switch + { + ValidationType.AssertNotNullOrEmpty => AssertNotNullOrEmpty(parameter), + ValidationType.AssertNotNull => AssertNotNull(parameter), + _ => EmptyStatement + }; + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Assertions.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Assertions.cs new file mode 100644 index 0000000..bd1c7fc --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Assertions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using NUnit.Framework; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static class Assertions + { + public static MethodBodyStatement IsNotNull(ValueExpression target) + => new InvokeStaticMethodStatement(typeof(Assert), nameof(Assert.IsNotNull), target); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Convert.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Convert.cs new file mode 100644 index 0000000..88f102e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Convert.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static ValueExpression GetConversion(this ValueExpression expression, CSharpType from, CSharpType to) + { + if (CSharpType.RequiresToList(from, to)) + { + if (from.IsNullable) + expression = new NullConditionalExpression(expression); + return new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.ToList), new[] { expression }, CallAsExtension: true); + } + + // null value cannot directly assign to extensible enum from string, because it invokes the implicit operator from string which invokes the ctor, and the ctor does not allow the value to be null + if (RequiresNullCheckForExtensibleEnum(from, to)) + { + expression = new TernaryConditionalOperator( + Equal(expression, Null), + to.IsNullable ? Null.CastTo(to) : Default.CastTo(to), + New.Instance(to, expression)); + } + + return expression; + } + + public static ValueExpression GetConversionToProtocol(this Parameter convenience, CSharpType toType, string? contentType) + { + // deal with the cases of converting to RequestContent + if (toType.EqualsIgnoreNullable(Configuration.ApiTypes.RequestContentType)) + { + return GetConversionToRequestContent(convenience, contentType); + } + + TypedValueExpression expression = new ParameterReference(convenience); + // converting to anything else should be path, query, head parameters + if (expression.Type is { IsFrameworkType: false, Implementation: EnumType enumType }) + { + if (convenience.Type.IsNullable) + { + expression = expression.NullConditional(); + } + expression = new EnumExpression(enumType, expression).ToSerial(); + } + + return expression; + + static ValueExpression GetConversionToRequestContent(Parameter convenience, string? contentType) + { + switch (convenience.Type) + { + case { IsFrameworkType: true }: + return GetConversionFromFrameworkToRequestContent(convenience, contentType); + case { IsFrameworkType: false, Implementation: EnumType enumType }: + TypedValueExpression enumExpression = new ParameterReference(convenience); + if (convenience.IsOptionalInSignature) + { + enumExpression = enumExpression.NullableStructValue(); + } + var convenienceEnum = new EnumExpression(enumType, enumExpression); + return Extensible.RequestContent.Create(BinaryDataExpression.FromObjectAsJson(convenienceEnum.ToSerial())); + case { IsFrameworkType: false, Implementation: ModelTypeProvider model }: + TypedValueExpression modelExpression = new ParameterReference(convenience); + if (convenience.IsOptionalInSignature) + { + modelExpression = modelExpression.NullConditional(); + } + var serializableObjectExpression = new SerializableObjectTypeExpression(model, modelExpression); + if (contentType != null && FormattableStringHelpers.ToMediaType(contentType) == BodyMediaType.Multipart) + { + return serializableObjectExpression.ToMultipartRequestContent(); + } + return serializableObjectExpression.ToRequestContent(); + default: + throw new InvalidOperationException($"Unhandled type: {convenience.Type}"); + } + } + + static ValueExpression GetConversionFromFrameworkToRequestContent(Parameter parameter, string? contentType) + { + if (parameter.Type.IsReadWriteDictionary) + { + var expression = RequestContentHelperProvider.Instance.FromDictionary(parameter); + if (parameter.IsOptionalInSignature) + { + expression = new TernaryConditionalOperator(NotEqual(parameter, Null), expression, Null); + } + return expression; + } + + if (parameter.Type.IsList) + { + var content = (ValueExpression)parameter; + content = parameter.Type.IsReadOnlyMemory + ? content.Property(nameof(ReadOnlyMemory.Span)) // for ReadOnlyMemory, we need to get the Span and pass it through + : content; + var expression = RequestContentHelperProvider.Instance.FromEnumerable(content); + if (parameter.IsOptionalInSignature) + { + expression = new TernaryConditionalOperator(NotEqual(parameter, Null), expression, Null); + } + return expression; + } + + if (parameter.Type.IsFrameworkType == true && parameter.Type.FrameworkType == typeof(AzureLocation)) + { + return RequestContentHelperProvider.Instance.FromObject(((ValueExpression)parameter).InvokeToString()); + } + + BodyMediaType? mediaType = contentType == null ? null : FormattableStringHelpers.ToMediaType(contentType); + if (parameter.RequestLocation == RequestLocation.Body && mediaType == BodyMediaType.Binary) + { + return parameter; + } + // TODO: Here we only consider the case when body is string type. We will add support for other types. + if (parameter.RequestLocation == RequestLocation.Body && mediaType == BodyMediaType.Text && parameter.Type.FrameworkType == typeof(string)) + { + return parameter; + } + + return RequestContentHelperProvider.Instance.FromObject(parameter); + } + } + + /// + /// null value cannot directly assign to extensible enum from string, because it invokes the implicit operator from string which invokes the ctor, and the ctor does not allow the value to be null + /// This method checks if we need an explicitly null check + /// + /// + /// + /// + private static bool RequiresNullCheckForExtensibleEnum(CSharpType from, CSharpType to) + { + return from is { IsFrameworkType: true, FrameworkType: { } frameworkType } && frameworkType == typeof(string) + && to is { IsFrameworkType: false, Implementation: EnumType { IsExtensible: true } }; + } + + internal static MethodBodyStatement Increment(ValueExpression value) => new UnaryOperatorStatement(new UnaryOperatorExpression("++", value, true)); + + public static class InvokeConvert + { + public static ValueExpression ToDouble(StringExpression value) => new InvokeStaticMethodExpression(typeof(Convert), nameof(Convert.ToDouble), Arguments: new[] { value }); + public static ValueExpression ToInt32(StringExpression value) => new InvokeStaticMethodExpression(typeof(Convert), nameof(Convert.ToInt32), Arguments: new[] { value }); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.DeclarationStatements.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.DeclarationStatements.cs new file mode 100644 index 0000000..8716ddb --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.DeclarationStatements.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static MethodBodyStatement UsingDeclare(string name, CSharpType type, ValueExpression value, out VariableReference variable) + { + var declaration = new CodeWriterDeclaration(name); + variable = new VariableReference(type, declaration); + return new UsingDeclareVariableStatement(type, declaration, value); + } + + public static MethodBodyStatement UsingDeclare(string name, JsonDocumentExpression value, out JsonDocumentExpression variable) + => UsingDeclare(name, value, d => new JsonDocumentExpression(d), out variable); + + public static MethodBodyStatement UsingDeclare(string name, StreamExpression value, out StreamExpression variable) + => UsingDeclare(name, value, d => new StreamExpression(d), out variable); + + public static MethodBodyStatement UsingDeclare(VariableReference variable, ValueExpression value) + => new UsingDeclareVariableStatement(variable.Type, variable.Declaration, value); + + public static MethodBodyStatement Declare(CSharpType operationType, string name, OperationExpression value, out OperationExpression operation) + { + var variable = new VariableReference(operationType, name); + operation = new OperationExpression(variable); + return Declare(variable, value); + } + + public static MethodBodyStatement Declare(CSharpType responseType, string name, ResponseExpression value, out ResponseExpression response) + { + var variable = new VariableReference(responseType, name); + response = new ResponseExpression(variable); + return Declare(variable, value); + } + + public static MethodBodyStatement Declare(CSharpType variableType, string name, ValueExpression value, out TypedValueExpression variable) + { + var variableRef = new VariableReference(variableType, name); + variable = variableRef; + return Declare(variableRef, value); + } + + public static MethodBodyStatement Declare(RequestContextExpression value, out RequestContextExpression variable) + => Declare(KnownParameters.RequestContext.Name, value, d => new RequestContextExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, BinaryDataExpression value, out BinaryDataExpression variable) + => Declare(name, value, d => new BinaryDataExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, DictionaryExpression value, out DictionaryExpression variable) + => Declare(name, value, d => new DictionaryExpression(value.KeyType, value.ValueType, d), out variable); + + public static MethodBodyStatement Declare(string name, EnumerableExpression value, out EnumerableExpression variable) + => Declare(name, value, d => new EnumerableExpression(value.ItemType, d), out variable); + + public static MethodBodyStatement Declare(string name, JsonElementExpression value, out JsonElementExpression variable) + => Declare(name, value, d => new JsonElementExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, ListExpression value, out ListExpression variable) + => Declare(name, value, d => new ListExpression(value.ItemType, d), out variable); + + public static MethodBodyStatement Declare(string name, OperationExpression value, out OperationExpression variable) + => Declare(name, value, d => new OperationExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, RequestContentExpression value, out RequestContentExpression variable) + => Declare(name, value, d => new RequestContentExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, ResponseExpression value, out ResponseExpression variable) + => Declare(name, value, d => new ResponseExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, StreamReaderExpression value, out StreamReaderExpression variable) + => Declare(name, value, d => new StreamReaderExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, XmlWriterContentExpression value, out XmlWriterContentExpression variable) + => Declare(name, value, d => new XmlWriterContentExpression(d), out variable); + + public static MethodBodyStatement Declare(string name, TypedValueExpression value, out TypedValueExpression variable) + { + var declaration = new VariableReference(value.Type, name); + variable = declaration; + return Declare(declaration, value); + } + + public static MethodBodyStatement Declare(VariableReference variable, ValueExpression value) + => new DeclareVariableStatement(variable.Type, variable.Declaration, value); + + public static MethodBodyStatement UsingVar(string name, HttpMessageExpression value, out HttpMessageExpression variable) + => UsingVar(name, value, d => new HttpMessageExpression(d), out variable); + + public static MethodBodyStatement UsingVar(string name, JsonDocumentExpression value, out JsonDocumentExpression variable) + => UsingVar(name, value, d => new JsonDocumentExpression(d), out variable); + + public static MethodBodyStatement Var(string name, DictionaryExpression value, out DictionaryExpression variable) + => Var(name, value, d => new DictionaryExpression(value.KeyType, value.ValueType, d), out variable); + + public static MethodBodyStatement Var(string name, FormUrlEncodedContentExpression value, out FormUrlEncodedContentExpression variable) + => Var(name, value, d => new FormUrlEncodedContentExpression(d), out variable); + + public static MethodBodyStatement Var(string name, HttpMessageExpression value, out HttpMessageExpression variable) + => Var(name, value, d => new HttpMessageExpression(d), out variable); + + public static MethodBodyStatement Var(string name, ListExpression value, out ListExpression variable) + => Var(name, value, d => new ListExpression(value.ItemType, d), out variable); + + public static MethodBodyStatement Var(string name, MultipartFormDataContentExpression value, out MultipartFormDataContentExpression variable) + => Var(name, value, d => new MultipartFormDataContentExpression(d), out variable); + + public static MethodBodyStatement Var(string name, OperationExpression value, out OperationExpression variable) + => Var(name, value, d => new OperationExpression(d), out variable); + + public static MethodBodyStatement Var(string name, RawRequestUriBuilderExpression value, out RawRequestUriBuilderExpression variable) + => Var(name, value, d => new RawRequestUriBuilderExpression(d), out variable); + + public static MethodBodyStatement Var(string name, RequestExpression value, out RequestExpression variable) + => Var(name, value, d => new RequestExpression(d), out variable); + + public static MethodBodyStatement Var(string name, ResponseExpression value, out ResponseExpression variable) + => Var(name, value, d => new ResponseExpression(d), out variable); + + public static MethodBodyStatement Var(string name, StringExpression value, out StringExpression variable) + => Var(name, value, d => new StringExpression(d), out variable); + + public static MethodBodyStatement Var(string name, Utf8JsonWriterExpression value, out Utf8JsonWriterExpression variable) + => Var(name, value, d => new Utf8JsonWriterExpression(d), out variable); + + public static MethodBodyStatement Var(string name, XDocumentExpression value, out XDocumentExpression variable) + => Var(name, value, d => new XDocumentExpression(d), out variable); + + public static MethodBodyStatement Var(string name, XmlWriterContentExpression value, out XmlWriterContentExpression variable) + => Var(name, value, d => new XmlWriterContentExpression(d), out variable); + + public static MethodBodyStatement Var(string name, TypedValueExpression value, out TypedValueExpression variable) + { + var reference = new VariableReference(value.Type, name); + variable = reference; + return Var(reference, value); + } + + public static MethodBodyStatement Var(VariableReference variable, ValueExpression value) + => new DeclareVariableStatement(null, variable.Declaration, value); + + private static MethodBodyStatement UsingDeclare(string name, T value, Func factory, out T variable) where T : TypedValueExpression + { + var declaration = new CodeWriterDeclaration(name); + variable = factory(new VariableReference(value.Type, declaration)); + return new UsingDeclareVariableStatement(value.Type, declaration, value); + } + + private static MethodBodyStatement UsingVar(string name, T value, Func factory, out T variable) where T : TypedValueExpression + { + var declaration = new CodeWriterDeclaration(name); + variable = factory(new VariableReference(value.Type, declaration)); + return new UsingDeclareVariableStatement(null, declaration, value); + } + + private static MethodBodyStatement Declare(string name, T value, Func factory, out T variable) where T : TypedValueExpression + { + var declaration = new CodeWriterDeclaration(name); + variable = factory(new VariableReference(value.Type, declaration)); + return new DeclareVariableStatement(value.Type, declaration, value); + } + + private static MethodBodyStatement Var(string name, T value, Func factory, out T variable) where T : TypedValueExpression + { + var declaration = new CodeWriterDeclaration(name); + variable = factory(new VariableReference(value.Type, declaration)); + return new DeclareVariableStatement(null, declaration, value); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Linq.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Linq.cs new file mode 100644 index 0000000..4d8f39f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Linq.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static class Linq + { + public static ValueExpression ToList(ValueExpression expression) + { + return new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.ToList), new[] { expression }, CallAsExtension: true); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.New.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.New.cs new file mode 100644 index 0000000..23b6637 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.New.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static class New + { + public static ValueExpression ArgumentOutOfRangeException(EnumType enumType, Parameter valueParameter) + => Instance(typeof(ArgumentOutOfRangeException), Nameof(valueParameter), valueParameter, Literal($"Unknown {enumType.Declaration.Name} value.")); + public static ValueExpression ArgumentOutOfRangeException(ValueExpression valueParameter, string message, bool wrapInNameOf = true) + => Instance(typeof(ArgumentOutOfRangeException), wrapInNameOf ? Nameof(valueParameter) : valueParameter, Literal(message)); + + public static ValueExpression NotImplementedException(ValueExpression message) + => Instance(typeof(NotImplementedException), message); + + public static ValueExpression NotSupportedException(ValueExpression message) + => Instance(typeof(NotSupportedException), message); + + public static ValueExpression InvalidOperationException(ValueExpression message) + => Instance(typeof(InvalidOperationException), message); + + public static ValueExpression ArgumentNullException(ValueExpression parameter, bool wrapInNameOf = true) + => Instance(typeof(ArgumentNullException), wrapInNameOf ? Nameof(parameter) : parameter); + + public static ValueExpression ArgumentException(ValueExpression parameter, string message, bool wrapInNameOf = true) + => ArgumentException(parameter, Literal(message), wrapInNameOf); + + public static ValueExpression ArgumentException(ValueExpression parameter, ValueExpression message, bool wrapInNameOf = true) + => Instance(typeof(ArgumentException), message, wrapInNameOf ? Nameof(parameter) : parameter); + + public static ValueExpression JsonException(ValueExpression message) + => Instance(typeof(JsonException), message); + + public static EnumerableExpression Array(CSharpType? elementType) => new(elementType ?? typeof(object), new NewArrayExpression(elementType)); + public static EnumerableExpression Array(CSharpType? elementType, params ValueExpression[] items) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, new ArrayInitializerExpression(items))); + public static EnumerableExpression Array(CSharpType? elementType, bool isInline, params ValueExpression[] items) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, new ArrayInitializerExpression(items, isInline))); + public static EnumerableExpression Array(CSharpType? elementType, ValueExpression size) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, Size: size)); + + public static DictionaryExpression Dictionary(CSharpType keyType, CSharpType valueType) + => new(keyType, valueType, new NewDictionaryExpression(new CSharpType(typeof(Dictionary<,>), keyType, valueType))); + public static DictionaryExpression Dictionary(CSharpType keyType, CSharpType valueType, params (ValueExpression Key, ValueExpression Value)[] values) + => new(keyType, valueType, new NewDictionaryExpression(new CSharpType(typeof(Dictionary<,>), keyType, valueType), new DictionaryInitializerExpression(values))); + + public static TypedValueExpression JsonSerializerOptions() => new FrameworkTypeExpression(typeof(JsonSerializerOptions), new NewJsonSerializerOptionsExpression()); + + public static ListExpression List(CSharpType elementType) => new(elementType, Instance(new CSharpType(typeof(List<>), elementType))); + + public static StreamReaderExpression StreamReader(ValueExpression stream) => new(Instance(typeof(StreamReader), stream)); + + public static TimeSpanExpression TimeSpan(int hours, int minutes, int seconds) => new(Instance(typeof(TimeSpan), Int(hours), Int(minutes), Int(seconds))); + public static TypedValueExpression Uri(string uri) => Instance(typeof(Uri), Literal(uri)); + + public static ValueExpression Anonymous(string key, ValueExpression value) => Anonymous(new Dictionary { [key] = value }); + public static ValueExpression Anonymous(IReadOnlyDictionary? properties) => new KeywordExpression("new", new ObjectInitializerExpression(properties, UseSingleLine: false)); + public static ValueExpression Instance(ConstructorSignature ctorSignature, IReadOnlyList arguments, IReadOnlyDictionary? properties = null) => new NewInstanceExpression(ctorSignature.Type, arguments, properties != null ? new ObjectInitializerExpression(properties) : null); + public static ValueExpression Instance(ConstructorSignature ctorSignature, IReadOnlyDictionary? properties = null) => Instance(ctorSignature, ctorSignature.Parameters.Select(p => (ValueExpression)p).ToArray(), properties); + public static ValueExpression Instance(CSharpType type, IReadOnlyList arguments) => new NewInstanceExpression(type, arguments); + public static ValueExpression Instance(CSharpType type, params ValueExpression[] arguments) => new NewInstanceExpression(type, arguments); + public static ValueExpression Instance(CSharpType type, IReadOnlyDictionary properties) => new NewInstanceExpression(type, System.Array.Empty(), new ObjectInitializerExpression(properties)); + public static TypedValueExpression Instance(Type type, params ValueExpression[] arguments) => new FrameworkTypeExpression(type, new NewInstanceExpression(type, arguments)); + public static TypedValueExpression Instance(Type type, IReadOnlyDictionary properties) => new FrameworkTypeExpression(type, new NewInstanceExpression(type, System.Array.Empty(), new ObjectInitializerExpression(properties))); + public static TypedValueExpression Instance(Type type, IReadOnlyList arguments, IReadOnlyDictionary properties) => new FrameworkTypeExpression(type, new NewInstanceExpression(type, arguments, new ObjectInitializerExpression(properties))); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Optional.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Optional.cs new file mode 100644 index 0000000..4fe0c96 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Optional.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static class InvokeOptional + { + public static BoolExpression IsCollectionDefined(TypedValueExpression collection) + { + return OptionalTypeProvider.Instance.IsCollectionDefined(collection); + } + + public static ValueExpression FallBackToChangeTrackingCollection(TypedValueExpression collection, CSharpType? paramType) + { + if (!collection.Type.IsCollection || collection.Type.IsReadOnlyMemory) + { + return collection; + } + + var changeTrackingType = collection.Type.Arguments.Count == 1 + ? ChangeTrackingListProvider.Instance.Type.MakeGenericType(collection.Type.Arguments) + : ChangeTrackingDictionaryProvider.Instance.Type.MakeGenericType(collection.Type.Arguments); + return NullCoalescing(collection, New.Instance(changeTrackingType)); + } + + public static MethodBodyStatement WrapInIsDefined(PropertySerialization serialization, MethodBodyStatement statement) + { + if (serialization.IsRequired) + { + return statement; + } + + if (serialization.Value.Type is { IsNullable: false, IsValueType: true }) + { + if (!serialization.Value.Type.Equals(typeof(JsonElement))) + { + return statement; + } + } + + return serialization.Value.Type is { IsCollection: true, IsReadOnlyMemory: false } + ? new IfStatement(IsCollectionDefined(serialization.Value)) { statement } + : new IfStatement(OptionalTypeProvider.Instance.IsDefined(serialization.Value)) { statement }; + } + + public static MethodBodyStatement WrapInIsNotEmpty(PropertySerialization serialization, MethodBodyStatement statement) + { + return serialization.Value.Type is { IsCollection: true, IsReadOnlyMemory: false } + ? new IfStatement(new BoolExpression(InvokeStaticMethodExpression.Extension(typeof(Enumerable), nameof(Enumerable.Any), serialization.Value))) { statement } + : statement; + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Pageable.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Pageable.cs new file mode 100644 index 0000000..4173528 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Pageable.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Models.Types; +using Autorest.CSharp.Core; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static MethodBodyStatement DeclareFirstPageRequestLocalFunction(ValueExpression? restClient, string methodName, IEnumerable arguments, out CodeWriterDeclaration localFunctionName) + { + var requestMethodCall = new InvokeInstanceMethodExpression(restClient, methodName, arguments.ToList(), null, false); + localFunctionName = new CodeWriterDeclaration("FirstPageRequest"); + return new DeclareLocalFunctionStatement(localFunctionName, new[]{KnownParameters.PageSizeHint}, typeof(HttpMessage), requestMethodCall); + } + + public static MethodBodyStatement DeclareNextPageRequestLocalFunction(ValueExpression? restClient, string methodName, IEnumerable arguments, out CodeWriterDeclaration localFunctionName) + { + var requestMethodCall = new InvokeInstanceMethodExpression(restClient, methodName, arguments.ToList(), null, false); + localFunctionName = new CodeWriterDeclaration("NextPageRequest"); + return new DeclareLocalFunctionStatement(localFunctionName, new[]{KnownParameters.PageSizeHint, KnownParameters.NextLink}, typeof(HttpMessage), requestMethodCall); + } + + public static ValueExpression CreatePageable( + CodeWriterDeclaration createFirstPageRequest, + CodeWriterDeclaration? createNextPageRequest, + ValueExpression clientDiagnostics, + ValueExpression pipeline, + CSharpType? pageItemType, + string scopeName, + string itemPropertyName, + string? nextLinkPropertyName, + ValueExpression? requestContextOrCancellationToken, + bool async) + { + var arguments = new List + { + new VariableReference(typeof(Func), createFirstPageRequest), + createNextPageRequest is not null ? new VariableReference(typeof(Func), createNextPageRequest) : Null, + GetValueFactory(pageItemType), + clientDiagnostics, + pipeline, + Literal(scopeName), + Literal(itemPropertyName), + Literal(nextLinkPropertyName) + }; + + if (requestContextOrCancellationToken is not null) + { + arguments.Add(requestContextOrCancellationToken); + } + + var methodName = async ? nameof(GeneratorPageableHelpers.CreateAsyncPageable) : nameof(GeneratorPageableHelpers.CreatePageable); + return new InvokeStaticMethodExpression(typeof(GeneratorPageableHelpers), methodName, arguments); + } + + public static ValueExpression CreatePageable( + ValueExpression message, + CodeWriterDeclaration? createNextPageRequest, + ValueExpression clientDiagnostics, + ValueExpression pipeline, + CSharpType? pageItemType, + OperationFinalStateVia finalStateVia, + string scopeName, + string itemPropertyName, + string? nextLinkPropertyName, + ValueExpression? requestContext, + bool async) + { + var arguments = new List + { + KnownParameters.WaitForCompletion, + message, + createNextPageRequest is not null ? new VariableReference(typeof(Func), createNextPageRequest) : Null, + GetValueFactory(pageItemType), + clientDiagnostics, + pipeline, + FrameworkEnumValue(finalStateVia), + Literal(scopeName), + Literal(itemPropertyName), + Literal(nextLinkPropertyName) + }; + + if (requestContext is not null) + { + arguments.Add(requestContext); + } + + var methodName = async ? nameof(GeneratorPageableHelpers.CreateAsyncPageable) : nameof(GeneratorPageableHelpers.CreatePageable); + return new InvokeStaticMethodExpression(typeof(GeneratorPageableHelpers), methodName, arguments, null, false, async); + } + + private static ValueExpression GetValueFactory(CSharpType? pageItemType) + { + if (pageItemType is null) + { + throw new NotSupportedException("Type of the element of the page must be specified"); + } + + if (pageItemType.Equals(typeof(BinaryData))) + { + // When `JsonElement` provides access to its UTF8 buffer, change this code to create `BinaryData` from it. + // See also PageableHelpers.ParseResponseForBinaryData + var e = new VariableReference(typeof(JsonElement), "e"); + return new FuncExpression(new[] { e.Declaration }, BinaryDataExpression.FromString(new JsonElementExpression(e).GetRawText())); + } + + if (pageItemType is { IsFrameworkType: false, Implementation: SerializableObjectType { Serialization.Json: { } } type }) + { + return SerializableObjectTypeExpression.DeserializeDelegate(type); + } + + var variable = new VariableReference(typeof(JsonElement), "e"); + var deserializeImplementation = JsonSerializationMethodsBuilder.GetDeserializeValueExpression(new JsonElementExpression(variable), pageItemType, null); + return new FuncExpression(new[] { variable.Declaration }, deserializeImplementation); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Serialization.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Serialization.cs new file mode 100644 index 0000000..d64e33c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Serialization.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static class Serializations + { + public static StringExpression WireFormat = Literal("W"); + public static StringExpression JsonFormat = Literal("J"); + public static StringExpression XmlFormat = Literal("X"); + public static StringExpression BicepFormat = Literal("bicep"); + public static StringExpression MultipartFormat = Literal("MFD"); + + public static MethodBodyStatement WrapInCheckInRawData(ValueExpression? rawDataExpression, string propertyName, MethodBodyStatement statement) + { + if (rawDataExpression == null || !Configuration.EnableInternalRawData) + { + return statement; + } + + var rawDataDict = new DictionaryExpression(typeof(string), typeof(BinaryData), new NullConditionalExpression(rawDataExpression)); + var condition = NotEqual(rawDataDict.ContainsKey(Literal(propertyName)), True); + + if (statement is IfStatement ifStatement) + { + return ifStatement with + { + Condition = And(condition, ifStatement.Condition) + }; + } + + return new IfStatement(condition) + { + statement + }; + } + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + public static MethodBodyStatement WrapInCheckNotWire(bool shouldExcludeInWire, ValueExpression? format, MethodBodyStatement statement) + { + // if format is null, indicating the model reader writer is not enabled + if (format == null) + { + // when the model reader writer is not enabled, we just omit the serialization when it should not be included. + if (shouldExcludeInWire) + { + return EmptyStatement; + } + else + { + return statement; + } + } + + if (!shouldExcludeInWire) + return statement; + + // we need to wrap a check `format != "W"` around the statement + // if the statement is not an IfStatement, we just create an IfStatement and return + // if the statement is an IfStatement, we could add the condition to its condition which should simplify the generated code. + /* it looks like, if we have + * if (outer) + * { + * if (inner) { DoSomething(); } + * } + * we could always simplify this to: + * if (outer && inner) + * { + * DoSomething(); + * } + * these are exactly the same. + * 1. When outer is false, inner is never calculated, and DoSomething will not be execute + * 2. When outer is true, inner is calculated, and DoSomething will be execute when inner is true + * These hold true for both snippets + * + * These statements are only true when it is a IfStatement. If the statement is IfElseStatement with an else branch, they are no longer equivalent. + */ + + var isNotWireCondition = NotEqual(format, WireFormat); + if (statement is IfStatement ifStatement) + { + return ifStatement with + { + Condition = And(isNotWireCondition, ifStatement.Condition) + }; + } + + return new IfStatement(isNotWireCondition) + { + statement + }; + } + + public static MethodBodyStatement ValidateJsonFormat(ModelReaderWriterOptionsExpression? options, CSharpType? iModelTInterface, ValidationType validationType) + => ValidateFormat(options, JsonFormat, iModelTInterface, validationType).ToArray(); + + // TODO -- make the options parameter non-nullable again when we remove the `UseModelReaderWriter` flag. + private static IEnumerable ValidateFormat(ModelReaderWriterOptionsExpression? options, ValueExpression formatValue, CSharpType? iModelTInterface, ValidationType validationType) + { + if (options == null || iModelTInterface == null) + yield break; // if options expression is null, we skip outputting the following statements + /* + var format = options.Format == "W" ? GetFormatFromOptions(options) : options.Format; + if (format != ) + { + throw new FormatException($"The model {nameof(ThisModel)} does not support '{format}' format."); + } + */ + yield return GetConcreteFormat(options, iModelTInterface, out var format); + + yield return new IfStatement(NotEqual(format, formatValue)) + { + ThrowValidationFailException(format, iModelTInterface.Arguments[0], validationType) + }; + + yield return EmptyLine; // always outputs an empty line here because we will always have other statements after this + } + + public static MethodBodyStatement GetConcreteFormat(ModelReaderWriterOptionsExpression options, CSharpType iModelTInterface, out TypedValueExpression format) + => Var("format", new TypedTernaryConditionalOperator( + Equal(options.Format, WireFormat), + new StringExpression(This.CastTo(iModelTInterface).Invoke(nameof(IPersistableModel.GetFormatFromOptions), options)), + options.Format), out format); + + public static MethodBodyStatement ThrowValidationFailException(ValueExpression format, CSharpType typeOfT, ValidationType validationType) + => Throw(New.Instance( + typeof(FormatException), + new FormattableStringExpression($"The model {{{0}}} does not support {(validationType == ValidationType.Write ? "writing" : "reading")} '{{{1}}}' format.", new[] + { + Nameof(typeOfT), + format + }))); + + public enum ValidationType + { + Write, + Read + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.Where.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.Where.cs new file mode 100644 index 0000000..afcf3af --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.Where.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static class Where + { + public static WhereExpression NotNull(CSharpType type) => new WhereExpression(type, new KeywordExpression("notnull", null)); + public static WhereExpression Struct(CSharpType type) => new WhereExpression(type, new KeywordExpression("struct", null)); + public static WhereExpression Class(CSharpType type) => new WhereExpression(type, new KeywordExpression("class", null)); + public static WhereExpression Implements(CSharpType type, params CSharpType[] typesToImplement) + { + if (typesToImplement.Length == 0) + { + throw new InvalidOperationException("You must provide at least one type to implement"); + + } + List constraints = new List(); + foreach (var implementation in typesToImplement) + { + constraints.Add(implementation); + } + return new WhereExpression(type, constraints); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Snippets.cs b/logger/autorest.csharp/common/Output/Expressions/Snippets.cs new file mode 100644 index 0000000..7bba6e7 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Snippets.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Types; +using static AutoRest.CSharp.Common.Input.Configuration; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal static partial class Snippets + { + public static ExtensibleSnippets Extensible => Configuration.ApiTypes.ExtensibleSnippets; + + public static MethodBodyStatement EmptyStatement { get; } = new(); + public static MethodBodyStatement AsStatement(this IEnumerable statements) => statements.ToArray(); + + public static ValueExpression Dash { get; } = new KeywordExpression("_", null); + public static ValueExpression Default { get; } = new KeywordExpression("default", null); + public static ValueExpression Null { get; } = new KeywordExpression("null", null); + + public static ValueExpression DefaultOf(CSharpType type) => type is { IsValueType: true, IsNullable: false } ? Default.CastTo(type) : Null.CastTo(type); + public static ValueExpression This { get; } = new KeywordExpression("this", null); + public static KeywordExpression Base => new KeywordExpression("base", null); + public static BoolExpression True { get; } = new(new KeywordExpression("true", null)); + public static BoolExpression False { get; } = new(new KeywordExpression("false", null)); + + public static BoolExpression Bool(bool value) => value ? True : False; + public static IntExpression Int(int value) => new IntExpression(Literal(value)); + public static LongExpression Long(long value) => new LongExpression(Literal(value)); + public static ValueExpression Float(float value) => new FormattableStringToExpression($"{value}f"); + public static ValueExpression Double(double value) => new FormattableStringToExpression($"{value}d"); + + public static ValueExpression Nameof(ValueExpression expression) => new InvokeInstanceMethodExpression(null, "nameof", new[] { expression }, null, false); + public static ValueExpression ThrowExpression(ValueExpression expression) => new KeywordExpression("throw", expression); + + public static ValueExpression NullCoalescing(ValueExpression left, ValueExpression right) => new BinaryOperatorExpression("??", left, right); + public static ValueExpression EnumValue(EnumType type, EnumTypeValue value) => new MemberExpression(new TypeReference(type.Type), value.Declaration.Name); + public static ValueExpression FrameworkEnumValue(TEnum value) where TEnum : struct, Enum => new MemberExpression(new TypeReference(typeof(TEnum)), Enum.GetName(value)!); + + public static ValueExpression RemoveAllNullConditional(ValueExpression expression) + => expression switch + { + NullConditionalExpression nullConditional => RemoveAllNullConditional(nullConditional.Inner), + MemberExpression { Inner: { } inner } member => member with { Inner = RemoveAllNullConditional(inner) }, + TypedValueExpression typed => typed with { Untyped = RemoveAllNullConditional(typed.Untyped) }, + _ => expression + }; + + public static TypedValueExpression RemoveAllNullConditional(TypedValueExpression expression) + => expression with { Untyped = RemoveAllNullConditional(expression.Untyped) }; + + public static ValueExpression Literal(object? value) => new FormattableStringToExpression($"{value:L}"); + + public static StringExpression Literal(string? value) => new(value is null ? Null : new StringLiteralExpression(value, false)); + public static StringExpression LiteralU8(string value) => new(new StringLiteralExpression(value, true)); + + public static BoolExpression GreaterThan(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression(">", left, right)); + public static BoolExpression LessThan(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("<", left, right)); + public static BoolExpression Equal(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("==", left, right)); + public static BoolExpression NotEqual(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("!=", left, right)); + + public static BoolExpression Is(XElementExpression value, string name, out XElementExpression xElement) + => Is(value, name, d => new XElementExpression(d), out xElement); + public static BoolExpression Is(XAttributeExpression value, string name, out XAttributeExpression xAttribute) + => Is(value, name, d => new XAttributeExpression(d), out xAttribute); + public static BoolExpression Is(ValueExpression left, ValueExpression right) + => new(new BinaryOperatorExpression("is", left, right)); + + public static BoolExpression Or(BoolExpression left, BoolExpression right) => new(new BinaryOperatorExpression("||", left.Untyped, right.Untyped)); + public static BoolExpression And(BoolExpression left, BoolExpression right) => new(new BinaryOperatorExpression("&&", left.Untyped, right.Untyped)); + public static BoolExpression Not(BoolExpression operand) => new(new UnaryOperatorExpression("!", operand, false)); + + public static MethodBodyStatement EmptyLine => new EmptyLineStatement(); + public static KeywordStatement Continue => new("continue", null); + public static KeywordStatement Break => new("break", null); + public static KeywordStatement Return(ValueExpression expression) => new("return", expression); + public static KeywordStatement Return() => new("return", null); + public static KeywordStatement Throw(ValueExpression expression) => new("throw", expression); + + public static EnumerableExpression InvokeArrayEmpty(CSharpType arrayItemType) + => new(arrayItemType, new InvokeStaticMethodExpression(typeof(Array), nameof(Array.Empty), Array.Empty(), new[] { arrayItemType })); + + public static StreamExpression InvokeFileOpenRead(string filePath) + => new(new InvokeStaticMethodExpression(typeof(System.IO.File), nameof(System.IO.File.OpenRead), new[] { Literal(filePath) })); + public static StreamExpression InvokeFileOpenWrite(string filePath) + => new(new InvokeStaticMethodExpression(typeof(System.IO.File), nameof(System.IO.File.OpenWrite), new[] { Literal(filePath) })); + + // Expected signature: MethodName(Utf8JsonWriter writer, ModelReaderWriterOptions? options); + public static MethodBodyStatement InvokeCustomSerializationMethod(string methodName, Utf8JsonWriterExpression utf8JsonWriter, ModelReaderWriterOptionsExpression? options) + { + // If options is null, the method signature should not include the parameter + return options is null + ? new InvokeInstanceMethodStatement(null, methodName, utf8JsonWriter) + : new InvokeInstanceMethodStatement(null, methodName, utf8JsonWriter, options); + } + + // TODO: https://github.com/Azure/autorest.csharp/issues/4633 + // Expected signature: MethodName(StringBuilder builder); + public static MethodBodyStatement InvokeCustomBicepSerializationMethod(string methodName, StringBuilderExpression stringBuilder) + => new InvokeInstanceMethodStatement(null, methodName, stringBuilder); + + // Expected signature: MethodName(JsonProperty property, ref T optional) + public static MethodBodyStatement InvokeCustomDeserializationMethod(string methodName, JsonPropertyExpression jsonProperty, CodeWriterDeclaration variable) + => new InvokeStaticMethodStatement(null, methodName, new ValueExpression[] { jsonProperty, new FormattableStringToExpression($"ref {variable}") }); + + public static AssignValueIfNullStatement AssignIfNull(ValueExpression variable, ValueExpression expression) => new(variable, expression); + public static AssignValueStatement Assign(ValueExpression variable, ValueExpression expression) => new(variable, expression); + + public static MethodBodyStatement AssignOrReturn(ValueExpression? variable, ValueExpression expression) + => variable != null ? Assign(variable, expression) : Return(expression); + + public static MethodBodyStatement InvokeConsoleWriteLine(ValueExpression expression) + => new InvokeStaticMethodStatement(typeof(Console), nameof(Console.WriteLine), expression); + + private static BoolExpression Is(T value, string name, Func factory, out T variable) where T : TypedValueExpression + { + var declaration = new CodeWriterDeclaration(name); + variable = factory(new VariableReference(value.Type, declaration)); + return new(new BinaryOperatorExpression("is", value, new FormattableStringToExpression($"{value.Type} {declaration:D}"))); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/AssignValueIfNullStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/AssignValueIfNullStatement.cs new file mode 100644 index 0000000..d0be8a4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/AssignValueIfNullStatement.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record AssignValueIfNullStatement(ValueExpression To, ValueExpression From) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + To.Write(writer); + writer.AppendRaw(" ??= "); + From.Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/AssignValueStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/AssignValueStatement.cs new file mode 100644 index 0000000..f1bce4f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/AssignValueStatement.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record AssignValueStatement(ValueExpression To, ValueExpression From) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + To.Write(writer); + writer.AppendRaw(" = "); + From.Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/CatchStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/CatchStatement.cs new file mode 100644 index 0000000..045831b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/CatchStatement.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record CatchStatement(ValueExpression? Exception, MethodBodyStatement Body) + { + public void Write(CodeWriter writer) + { + writer.AppendRaw("catch"); + if (Exception != null) + { + writer.AppendRaw(" ("); + Exception.Write(writer); + writer.AppendRaw(")"); + } + writer.LineRaw("{"); + Body.Write(writer); + writer.LineRaw("}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/DeclareLocalFunctionStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/DeclareLocalFunctionStatement.cs new file mode 100644 index 0000000..47326f8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/DeclareLocalFunctionStatement.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, ValueExpression? BodyExpression, MethodBodyStatement? BodyStatement) : MethodBodyStatement + { + internal DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, MethodBodyStatement BodyStatement) + : this(Name, Parameters, ReturnType, null, BodyStatement) { } + + internal DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, ValueExpression BodyExpression) + : this(Name, Parameters, ReturnType, BodyExpression, null) { } + + public sealed override void Write(CodeWriter writer) + { + writer.Append($"{ReturnType} {Name:D}("); + foreach (var parameter in Parameters) + { + writer.Append($"{parameter.Type} {parameter.Name}, "); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw(")"); + if (BodyExpression is not null) + { + writer.AppendRaw(" => "); + BodyExpression.Write(writer); + writer.LineRaw(";"); + } + else if (BodyStatement is not null) + { + using (writer.Scope()) + { + BodyStatement.Write(writer); + } + } + else + { + throw new InvalidOperationException($"{nameof(DeclareLocalFunctionStatement)}.{nameof(BodyExpression)} and {nameof(DeclareLocalFunctionStatement)}.{nameof(BodyStatement)} can't both be null."); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/DeclareVariableStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/DeclareVariableStatement.cs new file mode 100644 index 0000000..23b0b63 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/DeclareVariableStatement.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record DeclareVariableStatement(CSharpType? Type, CodeWriterDeclaration Name, ValueExpression Value) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + if (Type != null) + { + writer.Append($"{Type}"); + } + else + { + writer.AppendRaw("var"); + } + + writer.Append($" {Name:D} = "); + Value.Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/EmptyLineStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/EmptyLineStatement.cs new file mode 100644 index 0000000..7276858 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/EmptyLineStatement.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record EmptyLineStatement : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + writer.Line(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/ForStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/ForStatement.cs new file mode 100644 index 0000000..64192f8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/ForStatement.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record ForStatement(AssignmentExpression? IndexerAssignment, BoolExpression? Condition, ValueExpression? IncrementExpression) : MethodBodyStatement, IEnumerable + { + private readonly List _body = new(); + public IReadOnlyList Body => _body; + + public void Add(MethodBodyStatement statement) => _body.Add(statement); + public IEnumerator GetEnumerator() => _body.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); + + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + writer.AppendRaw("for ("); + IndexerAssignment?.Write(writer); + writer.AppendRaw("; "); + Condition?.Write(writer); + writer.AppendRaw("; "); + IncrementExpression?.Write(writer); + writer.LineRaw(")"); + + writer.LineRaw("{"); + writer.LineRaw(""); + foreach (var bodyStatement in Body) + { + bodyStatement.Write(writer); + } + writer.LineRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/ForeachStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/ForeachStatement.cs new file mode 100644 index 0000000..c9290de --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/ForeachStatement.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record ForeachStatement(CSharpType? ItemType, CodeWriterDeclaration Item, ValueExpression Enumerable, bool IsAsync) : MethodBodyStatement, IEnumerable + { + private readonly List _body = new(); + public IReadOnlyList Body => _body; + + public ForeachStatement(CSharpType itemType, string itemName, ValueExpression enumerable, bool isAsync, out VariableReference item) + : this(itemType, new CodeWriterDeclaration(itemName), enumerable, isAsync) + { + item = new VariableReference(itemType, Item); + } + + public ForeachStatement(string itemName, EnumerableExpression enumerable, out TypedValueExpression item) + : this(null, new CodeWriterDeclaration(itemName), enumerable, false) + { + item = new VariableReference(enumerable.ItemType, Item); + } + + public ForeachStatement(string itemName, EnumerableExpression enumerable, bool isAsync, out TypedValueExpression item) + : this(null, new CodeWriterDeclaration(itemName), enumerable, isAsync) + { + item = new VariableReference(enumerable.ItemType, Item); + } + + public ForeachStatement(string itemName, DictionaryExpression dictionary, out KeyValuePairExpression item) + : this(null, new CodeWriterDeclaration(itemName), dictionary, false) + { + var variable = new VariableReference(KeyValuePairExpression.GetType(dictionary.KeyType, dictionary.ValueType), Item); + item = new KeyValuePairExpression(dictionary.KeyType, dictionary.ValueType, variable); + } + + public void Add(MethodBodyStatement statement) => _body.Add(statement); + public IEnumerator GetEnumerator() => _body.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); + + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + writer.AppendRawIf("await ", IsAsync); + writer.AppendRaw("foreach ("); + if (ItemType == null) + { + writer.AppendRaw("var "); + } + else + { + writer.Append($"{ItemType} "); + } + + writer.Append($"{Item:D} in "); + Enumerable.Write(writer); + //writer.AppendRawIf(".ConfigureAwait(false)", foreachStatement.IsAsync); + writer.LineRaw(")"); + + writer.LineRaw("{"); + foreach (var bodyStatement in Body) + { + bodyStatement.Write(writer); + } + writer.LineRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/IfElsePreprocessorDirective.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/IfElsePreprocessorDirective.cs new file mode 100644 index 0000000..ab06f73 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/IfElsePreprocessorDirective.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record IfElsePreprocessorDirective(string Condition, MethodBodyStatement If, MethodBodyStatement? Else) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + writer.Line($"#if {Condition}"); + writer.AppendRaw("\t\t\t\t"); + If.Write(writer); + if (Else is not null) + { + writer.LineRaw("#else"); + Else.Write(writer); + } + + writer.LineRaw("#endif"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/IfElseStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/IfElseStatement.cs new file mode 100644 index 0000000..0310300 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/IfElseStatement.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record IfElseStatement(IfStatement If, MethodBodyStatement? Else) : MethodBodyStatement + { + public IfElseStatement(BoolExpression condition, MethodBodyStatement ifStatement, MethodBodyStatement? elseStatement, bool inline = false, bool addBraces = true) + : this(new IfStatement(condition, inline, addBraces) { ifStatement }, elseStatement) { } + + public sealed override void Write(CodeWriter writer) + { + If.Write(writer); + if (Else is null) + { + return; + } + + if (If.Inline || !If.AddBraces) + { + using (writer.AmbientScope()) + { + writer.AppendRaw("else "); + if (!If.Inline) + { + writer.Line(); + } + Else.Write(writer); + } + } + else + { + using (writer.Scope($"else")) + { + Else.Write(writer); + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/IfStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/IfStatement.cs new file mode 100644 index 0000000..02610b5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/IfStatement.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record IfStatement(BoolExpression Condition, bool Inline = false, bool AddBraces = true) : MethodBodyStatement, IEnumerable + { + private readonly List _body = new(); + public MethodBodyStatement Body => _body; + + public void Add(MethodBodyStatement statement) => _body.Add(statement); + public IEnumerator GetEnumerator() => _body.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); + + public sealed override void Write(CodeWriter writer) + { + writer.AppendRaw("if ("); + Condition.Write(writer); + + if (Inline) + { + writer.AppendRaw(") "); + using (writer.AmbientScope()) + { + Body.Write(writer); + } + } + else + { + writer.LineRaw(")"); + using (AddBraces ? writer.Scope() : writer.AmbientScope()) + { + Body.Write(writer); + } + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/InvokeInstanceMethodStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/InvokeInstanceMethodStatement.cs new file mode 100644 index 0000000..de988f5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/InvokeInstanceMethodStatement.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record InvokeInstanceMethodStatement(ValueExpression? InstanceReference, string MethodName, IReadOnlyList Arguments, bool CallAsAsync) : MethodBodyStatement + { + public InvokeInstanceMethodStatement(ValueExpression? instance, string methodName) : this(instance, methodName, Array.Empty(), false) { } + public InvokeInstanceMethodStatement(ValueExpression? instance, string methodName, ValueExpression arg) : this(instance, methodName, new[] { arg }, false) { } + public InvokeInstanceMethodStatement(ValueExpression? instance, string methodName, ValueExpression arg1, ValueExpression arg2) : this(instance, methodName, new[] { arg1, arg2 }, false) { } + public InvokeInstanceMethodStatement(ValueExpression? instance, string methodName, ValueExpression arg1, ValueExpression arg2, ValueExpression arg3) : this(instance, methodName, new[] { arg1, arg2, arg3 }, false) { } + + public sealed override void Write(CodeWriter writer) + { + new InvokeInstanceMethodExpression(InstanceReference, MethodName, Arguments, null, CallAsAsync).Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/InvokeStaticMethodStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/InvokeStaticMethodStatement.cs new file mode 100644 index 0000000..6bdaaec --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/InvokeStaticMethodStatement.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record InvokeStaticMethodStatement(CSharpType? MethodType, string MethodName, IReadOnlyList Arguments, IReadOnlyList? TypeArguments = null, bool CallAsExtension = false, bool CallAsAsync = false) : MethodBodyStatement + { + public InvokeStaticMethodStatement(CSharpType? methodType, string methodName) : this(methodType, methodName, Array.Empty()) { } + public InvokeStaticMethodStatement(CSharpType? methodType, string methodName, ValueExpression arg) : this(methodType, methodName, new[] { arg }) { } + public InvokeStaticMethodStatement(CSharpType? methodType, string methodName, ValueExpression arg1, ValueExpression arg2) : this(methodType, methodName, new[] { arg1, arg2 }) { } + public static InvokeStaticMethodStatement Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference) => new(methodType, methodName, new[] { instanceReference }, CallAsExtension: true); + public static InvokeStaticMethodStatement Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference, ValueExpression arg) => new(methodType, methodName, new[] { instanceReference, arg }, CallAsExtension: true); + + public sealed override void Write(CodeWriter writer) + { + new InvokeStaticMethodExpression(MethodType, MethodName, Arguments, TypeArguments, CallAsExtension, CallAsAsync).Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/KeywordStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/KeywordStatement.cs new file mode 100644 index 0000000..452acf6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/KeywordStatement.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record KeywordStatement(string Keyword, ValueExpression? Expression) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + writer.AppendRaw(Keyword); + if (Expression is not null) + { + writer.AppendRaw(" "); + Expression.Write(writer); + } + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/MethodBodyStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/MethodBodyStatement.cs new file mode 100644 index 0000000..2477424 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/MethodBodyStatement.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal record MethodBodyStatement + { + public virtual void Write(CodeWriter writer) { } + + public static implicit operator MethodBodyStatement(MethodBodyStatement[] statements) => new MethodBodyStatements(Statements: statements); + public static implicit operator MethodBodyStatement(List statements) => new MethodBodyStatements(Statements: statements); + + private string GetDebuggerDisplay() + { + using var writer = new DebuggerCodeWriter(); + Write(writer); + return writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/MethodBodyStatements.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/MethodBodyStatements.cs new file mode 100644 index 0000000..f17479e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/MethodBodyStatements.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record MethodBodyStatements(IReadOnlyList Statements) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + foreach (var statement in Statements) + { + statement.Write(writer); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/PragmaWarningPreprocessorDirective.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/PragmaWarningPreprocessorDirective.cs new file mode 100644 index 0000000..f91aa95 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/PragmaWarningPreprocessorDirective.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record PragmaWarningPreprocessorDirective(string action, string warningList) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + writer.Line($"#pragma warning {action} {warningList}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/SingleLineCommentStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/SingleLineCommentStatement.cs new file mode 100644 index 0000000..1f83292 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/SingleLineCommentStatement.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements; + +internal record SingleLineCommentStatement(FormattableString Message) : MethodBodyStatement +{ + public SingleLineCommentStatement(string message) : this(FormattableStringHelpers.FromString(message)) + { } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/SwitchCase.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/SwitchCase.cs new file mode 100644 index 0000000..6993547 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/SwitchCase.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record SwitchCase(IReadOnlyList Match, MethodBodyStatement Statement, bool Inline = false, bool AddScope = false) + { + public SwitchCase(ValueExpression match, MethodBodyStatement statement, bool inline = false, bool addScope = false) : this(new[] { match }, statement, inline, addScope) { } + + public static SwitchCase Default(MethodBodyStatement statement, bool inline = false, bool addScope = false) => new(Array.Empty(), statement, inline, addScope); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/SwitchStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/SwitchStatement.cs new file mode 100644 index 0000000..b2bc4a5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/SwitchStatement.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record SwitchStatement(ValueExpression MatchExpression) : MethodBodyStatement, IEnumerable + { + public SwitchStatement(ValueExpression matchExpression, IEnumerable cases) : this(matchExpression) + { + _cases.AddRange(cases); + } + + private readonly List _cases = new(); + public IReadOnlyList Cases => _cases; + + public void Add(SwitchCase statement) => _cases.Add(statement); + public IEnumerator GetEnumerator() => _cases.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_cases).GetEnumerator(); + + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + writer.Append($"switch ("); + MatchExpression.Write(writer); + writer.LineRaw(")"); + writer.LineRaw("{"); + foreach (var switchCase in Cases) + { + if (switchCase.Match.Any()) + { + for (var i = 0; i < switchCase.Match.Count; i++) + { + ValueExpression? match = switchCase.Match[i]; + writer.AppendRaw("case "); + match.Write(writer); + if (i < switchCase.Match.Count - 1) + { + writer.LineRaw(":"); + } + } + } + else + { + writer.AppendRaw("default"); + } + + writer.AppendRaw(": "); + if (!switchCase.Inline) + { + writer.Line(); + } + + if (switchCase.AddScope) + { + using (writer.Scope()) + { + switchCase.Statement.Write(writer); + } + } + else + { + switchCase.Statement.Write(writer); + } + } + writer.LineRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/ThrowStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/ThrowStatement.cs new file mode 100644 index 0000000..9d57513 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/ThrowStatement.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record ThrowStatement(ValueExpression ThrowExpression) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + ThrowExpression.Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/TryCatchFinallyStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/TryCatchFinallyStatement.cs new file mode 100644 index 0000000..2a601c0 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/TryCatchFinallyStatement.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record TryCatchFinallyStatement(MethodBodyStatement Try, IReadOnlyList Catches, MethodBodyStatement? Finally) : MethodBodyStatement + { + public override void Write(CodeWriter writer) + { + writer.LineRaw("try"); + writer.LineRaw("{"); + Try.Write(writer); + writer.LineRaw("}"); + + foreach (var catchStatement in Catches) + { + catchStatement.Write(writer); + } + + if (Finally != null) + { + writer.LineRaw("finally"); + writer.LineRaw("{"); + Finally.Write(writer); + writer.LineRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/UnaryOperatorStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/UnaryOperatorStatement.cs new file mode 100644 index 0000000..dbcdbe6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/UnaryOperatorStatement.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record UnaryOperatorStatement(UnaryOperatorExpression Expression) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + Expression.Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/UsingDeclareVariableStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/UsingDeclareVariableStatement.cs new file mode 100644 index 0000000..949c033 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/UsingDeclareVariableStatement.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record UsingDeclareVariableStatement(CSharpType? Type, CodeWriterDeclaration Name, ValueExpression Value) : MethodBodyStatement + { + public sealed override void Write(CodeWriter writer) + { + if (Type != null) + { + writer.Append($"using {Type}"); + } + else + { + writer.AppendRaw("using var"); + } + + writer.Append($" {Name:D} = "); + Value.Write(writer); + writer.LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/UsingScopeStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/UsingScopeStatement.cs new file mode 100644 index 0000000..8cfcdf4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/UsingScopeStatement.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record UsingScopeStatement(CSharpType? Type, CodeWriterDeclaration Variable, ValueExpression Value) : MethodBodyStatement, IEnumerable + { + private readonly List _body = new(); + public IReadOnlyList Body => _body; + + public UsingScopeStatement(CSharpType type, string variableName, ValueExpression value, out VariableReference variable) : this(type, new CodeWriterDeclaration(variableName), value) + { + variable = new VariableReference(type, Variable); + } + + public void Add(MethodBodyStatement statement) => _body.Add(statement); + public IEnumerator GetEnumerator() => _body.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); + + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + writer.AppendRaw("using ("); + if (Type == null) + { + writer.AppendRaw("var "); + } + else + { + writer.Append($"{Type} "); + } + + writer.Append($"{Variable:D} = "); + Value.Write(writer); + writer.LineRaw(")"); + + writer.LineRaw("{"); + foreach (var bodyStatement in Body) + { + bodyStatement.Write(writer); + } + writer.LineRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/Statements/WhileStatement.cs b/logger/autorest.csharp/common/Output/Expressions/Statements/WhileStatement.cs new file mode 100644 index 0000000..bae95fa --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/Statements/WhileStatement.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.Statements +{ + internal record WhileStatement(BoolExpression Condition) : MethodBodyStatement, IEnumerable + { + private readonly List _body = new(); + public IReadOnlyList Body => _body; + + public void Add(MethodBodyStatement statement) => _body.Add(statement); + public IEnumerator GetEnumerator() => _body.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); + + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + writer.AppendRaw("while ("); + Condition.Write(writer); + writer.LineRaw(")"); + + writer.LineRaw("{"); + foreach (var bodyStatement in Body) + { + bodyStatement.Write(writer); + } + writer.LineRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemModelSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemModelSnippets.cs new file mode 100644 index 0000000..19ab92a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemModelSnippets.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.ClientModel.Primitives; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.System +{ + internal partial class SystemExtensibleSnippets + { + internal class SystemModelSnippets : ModelSnippets + { + public override Method BuildFromOperationResponseMethod(SerializableObjectType type, MethodSignatureModifiers modifiers) + { + var result = new Parameter("response", $"The result to deserialize the model from.", typeof(PipelineResponse), null, ValidationType.None, null); + return new Method + ( + new MethodSignature(Configuration.ApiTypes.FromResponseName, null, $"Deserializes the model from a raw response.", modifiers, type.Type, null, new[] { result }), + new MethodBodyStatement[] + { + Snippets.UsingVar("document", JsonDocumentExpression.Parse(new PipelineResponseExpression(result).Content), out var document), + Snippets.Return(SerializableObjectTypeExpression.Deserialize(type, document.RootElement)) + } + ); + } + + public override TypedValueExpression InvokeToRequestBodyMethod(TypedValueExpression model) => new BinaryContentExpression(model.Invoke("ToRequestBody")); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemRequestContent.cs b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemRequestContent.cs new file mode 100644 index 0000000..d32f1c4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemRequestContent.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.System +{ + internal partial class SystemExtensibleSnippets + { + internal class SystemRequestContentSnippets : RequestContentSnippets + { + public override ValueExpression Create(BinaryDataExpression binaryData) + => BinaryContentExpression.Create(binaryData); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs new file mode 100644 index 0000000..4542781 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.System +{ + internal partial class SystemExtensibleSnippets + { + private class SystemRestOperationsSnippets : RestOperationsSnippets + { + public override StreamExpression GetContentStream(TypedValueExpression result) + => new ClientResultExpression(result).GetRawResponse().ContentStream; + + public override TypedValueExpression GetTypedResponseFromValue(TypedValueExpression value, TypedValueExpression result) + { + return ClientResultExpression.FromValue(value, GetRawResponse(result)); + } + + public override TypedValueExpression GetTypedResponseFromModel(SerializableObjectType type, TypedValueExpression result) + { + var response = GetRawResponse(result); + var model = new InvokeStaticMethodExpression(type.Type, Configuration.ApiTypes.FromResponseName, new[] { response }); + return ClientResultExpression.FromValue(model, response); + } + + public override TypedValueExpression GetTypedResponseFromEnum(EnumType enumType, TypedValueExpression result) + { + var response = GetRawResponse(result); + return ClientResultExpression.FromValue(EnumExpression.ToEnum(enumType, response.Content.ToObjectFromJson(typeof(string))), response); + } + + public override TypedValueExpression GetTypedResponseFromBinaryData(Type responseType, TypedValueExpression result, string? contentType = null) + { + var rawResponse = GetRawResponse(result); + if (responseType == typeof(string) && contentType != null && FormattableStringHelpers.ToMediaType(contentType) == BodyMediaType.Text) + { + return ClientResultExpression.FromValue(rawResponse.Content.InvokeToString(), rawResponse); + } + return responseType == typeof(BinaryData) + ? ClientResultExpression.FromValue(rawResponse.Content, rawResponse) + : ClientResultExpression.FromValue(rawResponse.Content.ToObjectFromJson(responseType), rawResponse); + } + + public override MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedValueExpression message) + { + var messageVar = new VariableReference(typeof(PipelineMessage), "message"); + message = messageVar; + return Snippets.UsingDeclare(messageVar, new InvokeInstanceMethodExpression(null, createRequestMethodSignature.Name, createRequestMethodSignature.Parameters.Select(p => (ValueExpression)p).ToList(), null, false)); + } + + public override MethodBodyStatement DeclareContentWithXmlWriter(out TypedValueExpression content, out XmlWriterExpression writer) + { + throw new NotImplementedException("Xml serialization isn't supported in System.Net.ClientModel yet"); + } + + public override MethodBodyStatement InvokeServiceOperationCallAndReturnHeadAsBool(TypedValueExpression pipeline, TypedValueExpression message, TypedValueExpression clientDiagnostics, bool async) + { + var resultVar = new VariableReference(typeof(ClientResult), "result"); + var result = new ClientResultExpression(resultVar); + return new MethodBodyStatement[] + { + Snippets.Var(resultVar, new ClientPipelineExpression(pipeline).ProcessHeadAsBoolMessage(message, new RequestOptionsExpression(KnownParameters.RequestContext), async)), + Snippets.Return(ClientResultExpression.FromValue(result.Value, result.GetRawResponse())) + }; + } + + public override TypedValueExpression InvokeServiceOperationCall(TypedValueExpression pipeline, TypedValueExpression message, bool async) + => ClientResultExpression.FromResponse(new ClientPipelineExpression(pipeline).ProcessMessage(message, new RequestOptionsExpression(KnownParameters.RequestContext), async)); + + private static PipelineResponseExpression GetRawResponse(TypedValueExpression result) + => result.Type.Equals(typeof(PipelineResponse)) + ? new PipelineResponseExpression(result) + : new ClientResultExpression(result).GetRawResponse(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.cs b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.cs new file mode 100644 index 0000000..ade88c5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/System/SystemExtensibleSnippets.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; + +namespace AutoRest.CSharp.Common.Output.Expressions.System +{ + internal partial class SystemExtensibleSnippets : ExtensibleSnippets + { + public override RequestContentSnippets RequestContent { get; } = new SystemRequestContentSnippets(); + public override XElementSnippets XElement => throw new NotImplementedException("XElement extensions aren't supported in unbranded yet."); + public override XmlWriterSnippets XmlWriter => throw new NotImplementedException("XmlWriter extensions aren't supported in unbranded yet."); + public override RestOperationsSnippets RestOperations { get; } = new SystemRestOperationsSnippets(); + public override ModelSnippets Model { get; } = new SystemModelSnippets(); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ArrayElementExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ArrayElementExpression.cs new file mode 100644 index 0000000..969da3a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ArrayElementExpression.cs @@ -0,0 +1,18 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record ArrayElementExpression(ValueExpression Array, ValueExpression Index) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + Array.Write(writer); + writer.AppendRaw("["); + Index.Write(writer); + writer.AppendRaw("]"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ArrayInitializerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ArrayInitializerExpression.cs new file mode 100644 index 0000000..883b901 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ArrayInitializerExpression.cs @@ -0,0 +1,47 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record ArrayInitializerExpression(IReadOnlyList? Elements = null, bool IsInline = true) : InitializerExpression + { + public sealed override void Write(CodeWriter writer) + { + if (Elements is not { Count: > 0 }) + { + writer.AppendRaw("{}"); + return; + } + + if (IsInline) + { + writer.AppendRaw("{"); + foreach (var item in Elements) + { + item.Write(writer); + writer.AppendRaw(", "); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw("}"); + } + else + { + writer.Line(); + writer.LineRaw("{"); + foreach (var item in Elements) + { + item.Write(writer); + writer.LineRaw(","); + } + + writer.RemoveTrailingComma(); + writer.Line(); + writer.AppendRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/AsExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/AsExpression.cs new file mode 100644 index 0000000..9ad33d0 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/AsExpression.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record AsExpression(ValueExpression Inner, CSharpType Type) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + Inner.Write(writer); + writer.Append($" as {Type}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/AssignmentExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/AssignmentExpression.cs new file mode 100644 index 0000000..82c05b1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/AssignmentExpression.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record AssignmentExpression(VariableReference Variable, ValueExpression Value) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.Append($"{Variable.Type} {Variable.Declaration:D} = {Value}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/BinaryOperatorExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/BinaryOperatorExpression.cs new file mode 100644 index 0000000..8ec07c0 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/BinaryOperatorExpression.cs @@ -0,0 +1,20 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record BinaryOperatorExpression(string Operator, ValueExpression Left, ValueExpression Right) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + // we should always write parenthesis around this expression since some or the logic operator has lower priority, and we might get trouble when there is a chain of binary operator expression, for instance (a || b) && c. + writer.AppendRaw("("); + Left.Write(writer); + writer.AppendRaw(" ").AppendRaw(Operator).AppendRaw(" "); + Right.Write(writer); + writer.AppendRaw(")"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/CastExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/CastExpression.cs new file mode 100644 index 0000000..3c77a71 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/CastExpression.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record CastExpression(ValueExpression Inner, CSharpType Type) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + // wrap the cast expression with parenthesis, so that it would not cause ambiguity for leading recursive calls + // if the parenthesis are not needed, the roslyn reducer will remove it. + writer.AppendRaw("("); + writer.Append($"({Type})"); + Inner.Write(writer); + writer.AppendRaw(")"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/CollectionInitializerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/CollectionInitializerExpression.cs new file mode 100644 index 0000000..29669cf --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/CollectionInitializerExpression.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record CollectionInitializerExpression(params ValueExpression[] Items) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.AppendRaw("{ "); + foreach (var item in Items) + { + item.Write(writer); + writer.AppendRaw(","); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw(" }"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ConstantExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ConstantExpression.cs new file mode 100644 index 0000000..e41e211 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ConstantExpression.cs @@ -0,0 +1,16 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record ConstantExpression(Constant Constant) : TypedValueExpression(Constant.Type, new UntypedConstantExpression(Constant)) + { + private record UntypedConstantExpression(Constant Constant) : ValueExpression + { + public override void Write(CodeWriter writer) => writer.Append(Constant.GetConstantFormattable()); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/DeclarationExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/DeclarationExpression.cs new file mode 100644 index 0000000..f920e65 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/DeclarationExpression.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record DeclarationExpression(VariableReference Variable, bool IsOut) : ValueExpression + { + public DeclarationExpression(CSharpType type, string name, out VariableReference variable, bool isOut = false) : this(new VariableReference(type, name), isOut) + { + variable = Variable; + } + + public sealed override void Write(CodeWriter writer) + { + writer.AppendRawIf("out ", IsOut); + writer.Append($"{Variable.Type} {Variable.Declaration:D}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/DictionaryInitializerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/DictionaryInitializerExpression.cs new file mode 100644 index 0000000..187c021 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/DictionaryInitializerExpression.cs @@ -0,0 +1,35 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record DictionaryInitializerExpression(IReadOnlyList<(ValueExpression Key, ValueExpression Value)>? Values = null) : InitializerExpression + { + public sealed override void Write(CodeWriter writer) + { + if (Values is not { Count: > 0 }) + { + writer.AppendRaw("{}"); + return; + } + + writer.Line(); + writer.LineRaw("{"); + foreach (var (key, value) in Values) + { + writer.AppendRaw("["); + key.Write(writer); + writer.AppendRaw("] = "); + value.Write(writer); + writer.LineRaw(","); + } + + writer.RemoveTrailingComma(); + writer.Line(); + writer.AppendRaw("}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FormattableStringExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FormattableStringExpression.cs new file mode 100644 index 0000000..b8f52d1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FormattableStringExpression.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + /// + /// Represents a FormattableString literal expression such as $"foo{bar}" + /// Constructed by a format string which should look like "foo{0}" + /// and a list of arguments of ValueExpressions + /// The constructor throws IndexOutOfRangeException when the count of arguments does not match in the format string and argument list. + /// + internal sealed record FormattableStringExpression : ValueExpression + { + public FormattableStringExpression(string format, IReadOnlyList args) + { +#if DEBUG + Validate(format, args); +#endif + Format = format; + Args = args; + } + + public FormattableStringExpression(string format, params ValueExpression[] args) : this(format, args as IReadOnlyList) + { + } + + public string Format { get; init; } + public IReadOnlyList Args { get; init; } + + public void Deconstruct(out string format, out IReadOnlyList args) + { + format = Format; + args = Args; + } + + public sealed override void Write(CodeWriter writer) + { + writer.AppendRaw("$\""); + var argumentCount = 0; + foreach ((var span, bool isLiteral) in StringExtensions.GetPathParts(Format)) + { + if (isLiteral) + { + writer.AppendRaw(span.ToString()); + continue; + } + + var arg = Args[argumentCount]; + argumentCount++; + // append the argument + writer.AppendRaw("{"); + arg.Write(writer); + writer.AppendRaw("}"); + } + writer.AppendRaw("\""); + } + + private static void Validate(string format, IReadOnlyList args) + { + var count = 0; + foreach (var (_, isLiteral) in StringExtensions.GetPathParts(format)) + { + if (!isLiteral) + count++; + } + + if (count != args.Count) + { + throw new IndexOutOfRangeException($"The count of arguments in format {format} does not match with the arguments provided"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FormattableStringToExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FormattableStringToExpression.cs new file mode 100644 index 0000000..f3b18f4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FormattableStringToExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record FormattableStringToExpression(FormattableString Value) : ValueExpression // Shim between formattable strings and expressions + { + public override void Write(CodeWriter writer) + { + writer.Append(Value); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FuncExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FuncExpression.cs new file mode 100644 index 0000000..be746aa --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/FuncExpression.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record FuncExpression(IReadOnlyList Parameters, ValueExpression Inner) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + if (Parameters.Count == 1) + { + var parameter = Parameters[0]; + if (parameter is not null) + { + writer.Declaration(parameter); + } + else + { + writer.AppendRaw("_"); + } + } + else + { + writer.AppendRaw("("); + foreach (var parameter in Parameters) + { + if (parameter is not null) + { + writer.Declaration(parameter); + } + else + { + writer.AppendRaw("_"); + } + writer.AppendRaw(", "); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw(")"); + } + + writer.AppendRaw(" => "); + Inner.Write(writer); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/IndexerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/IndexerExpression.cs new file mode 100644 index 0000000..965d394 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/IndexerExpression.cs @@ -0,0 +1,21 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record IndexerExpression(ValueExpression? Inner, ValueExpression Index) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + if (Inner is not null) + { + Inner.Write(writer); + } + writer.AppendRaw("["); + Index.Write(writer); + writer.AppendRaw("]"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InitializerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InitializerExpression.cs new file mode 100644 index 0000000..75578e1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InitializerExpression.cs @@ -0,0 +1,7 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record InitializerExpression : ValueExpression; +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InvokeInstanceMethodExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InvokeInstanceMethodExpression.cs new file mode 100644 index 0000000..30155ff --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InvokeInstanceMethodExpression.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + // [TODO]: AddConfigureAwaitFalse is needed only in docs. Consider removing. + internal record InvokeInstanceMethodExpression(ValueExpression? InstanceReference, string MethodName, IReadOnlyList Arguments, IReadOnlyList? TypeArguments, bool CallAsAsync, bool AddConfigureAwaitFalse = true) : ValueExpression + { + public InvokeInstanceMethodExpression(ValueExpression? instanceReference, string methodName, IReadOnlyList arguments, bool callAsAsync) : this(instanceReference, methodName, arguments, null, callAsAsync) { } + + public InvokeInstanceMethodExpression(ValueExpression? instanceReference, MethodSignature signature, IReadOnlyList arguments, bool addConfigureAwaitFalse = true) + : this(instanceReference, signature.Name, arguments, signature.GenericArguments, signature.Modifiers.HasFlag(MethodSignatureModifiers.Async), addConfigureAwaitFalse) { } + + public InvokeInstanceMethodExpression(ValueExpression? instanceReference, MethodSignature signature, bool addConfigureAwaitFalse = true) + : this(instanceReference, signature, signature.Parameters.Select(p => (ValueExpression)p).ToArray(), addConfigureAwaitFalse) { } + + public MethodBodyStatement ToStatement() + => new InvokeInstanceMethodStatement(InstanceReference, MethodName, Arguments, CallAsAsync); + + public sealed override void Write(CodeWriter writer) + { + writer.AppendRawIf("await ", CallAsAsync); + if (InstanceReference != null) + { + InstanceReference.Write(writer); + writer.AppendRaw("."); + } + + writer.AppendRaw(MethodName); + writer.WriteTypeArguments(TypeArguments); + writer.WriteArguments(Arguments); + writer.AppendRawIf(".ConfigureAwait(false)", CallAsAsync && AddConfigureAwaitFalse); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InvokeStaticMethodExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InvokeStaticMethodExpression.cs new file mode 100644 index 0000000..0d6a1be --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/InvokeStaticMethodExpression.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record InvokeStaticMethodExpression(CSharpType? MethodType, string MethodName, IReadOnlyList Arguments, IReadOnlyList? TypeArguments = null, bool CallAsExtension = false, bool CallAsAsync = false) : ValueExpression + { + public static InvokeStaticMethodExpression Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference) => new(methodType, methodName, new[] { instanceReference }, CallAsExtension: true); + public static InvokeStaticMethodExpression Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference, ValueExpression arg) => new(methodType, methodName, new[] { instanceReference, arg }, CallAsExtension: true); + public static InvokeStaticMethodExpression Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference, IReadOnlyList arguments) + => new(methodType, methodName, arguments.Prepend(instanceReference).ToArray(), CallAsExtension: true); + + public MethodBodyStatement ToStatement() + => new InvokeStaticMethodStatement(MethodType, MethodName, Arguments, TypeArguments, CallAsExtension, CallAsAsync); + + public sealed override void Write(CodeWriter writer) + { + if (CallAsExtension) + { + writer.AppendRawIf("await ", CallAsAsync); + if (MethodType != null) + { + writer.UseNamespace(MethodType.Namespace); + } + + Arguments[0].Write(writer); + writer.AppendRaw("."); + writer.AppendRaw(MethodName); + writer.WriteTypeArguments(TypeArguments); + writer.WriteArguments(Arguments.Skip(1)); + writer.AppendRawIf(".ConfigureAwait(false)", CallAsAsync); + } + else + { + writer.AppendRawIf("await ", CallAsAsync); + if (MethodType != null) + { + writer.Append($"{MethodType}."); + } + + writer.AppendRaw(MethodName); + writer.WriteTypeArguments(TypeArguments); + writer.WriteArguments(Arguments); + writer.AppendRawIf(".ConfigureAwait(false)", CallAsAsync); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/KeywordExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/KeywordExpression.cs new file mode 100644 index 0000000..ee65a48 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/KeywordExpression.cs @@ -0,0 +1,20 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record KeywordExpression(string Keyword, ValueExpression? Expression) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.AppendRaw(Keyword); + if (Expression is not null) + { + writer.AppendRaw(" "); + Expression.Write(writer); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/MemberExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/MemberExpression.cs new file mode 100644 index 0000000..9397054 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/MemberExpression.cs @@ -0,0 +1,20 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record MemberExpression(ValueExpression? Inner, string MemberName) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + if (Inner is not null) + { + Inner.Write(writer); + writer.AppendRaw("."); + } + writer.AppendRaw(MemberName); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewArrayExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewArrayExpression.cs new file mode 100644 index 0000000..a73dd9a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewArrayExpression.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record NewArrayExpression(CSharpType? Type, ArrayInitializerExpression? Items = null, ValueExpression? Size = null) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + if (Size is not null) + { + if (Type is null) + { + writer.AppendRaw("new["); + } + else + { + writer.Append($"new {Type}["); + } + + Size.Write(writer); + writer.AppendRaw("]"); + return; + } + + if (Items is { Elements.Count: > 0 }) + { + if (Type is null) + { + writer.AppendRaw("new[]"); + } + else + { + writer.Append($"new {Type}[]"); + } + + Items.Write(writer); + } + else if (Type is null) + { + writer.AppendRaw("new[]{}"); + } + else + { + writer.Append($"Array.Empty<{Type}>()"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewDictionaryExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewDictionaryExpression.cs new file mode 100644 index 0000000..4699204 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewDictionaryExpression.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record NewDictionaryExpression(CSharpType Type, DictionaryInitializerExpression? Values = null) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.Append($"new {Type}"); + if (Values is { Values.Count: > 0 }) + { + Values.Write(writer); + } + else + { + writer.AppendRaw("()"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewInstanceExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewInstanceExpression.cs new file mode 100644 index 0000000..1e86778 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewInstanceExpression.cs @@ -0,0 +1,28 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record NewInstanceExpression(CSharpType Type, IReadOnlyList Parameters, ObjectInitializerExpression? InitExpression = null) : ValueExpression + { + private const int SingleLineParameterThreshold = 6; + + public sealed override void Write(CodeWriter writer) + { + writer.Append($"new {Type}"); + if (Parameters.Count > 0 || InitExpression is not { Parameters.Count: > 0 }) + { + writer.WriteArguments(Parameters, Parameters.Count < SingleLineParameterThreshold); + } + + if (InitExpression is { Parameters.Count: > 0 }) + { + InitExpression.Write(writer); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewJsonSerializerOptionsExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewJsonSerializerOptionsExpression.cs new file mode 100644 index 0000000..c3499fa --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NewJsonSerializerOptionsExpression.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using AutoRest.CSharp.Generation.Writers; +using Azure.ResourceManager.Models; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal sealed record NewJsonSerializerOptionsExpression : ValueExpression + { + public override void Write(CodeWriter writer) + { + writer.UseNamespace("Azure.ResourceManager.Models"); + writer.Append($"new {typeof(JsonSerializerOptions)} {{ Converters = {{ new {nameof(ManagedServiceIdentityTypeV3Converter)}() }} }}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NullConditionalExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NullConditionalExpression.cs new file mode 100644 index 0000000..37a3e3d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/NullConditionalExpression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record NullConditionalExpression(ValueExpression Inner) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + Inner.Write(writer); + writer.AppendRaw("?"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ObjectInitializerExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ObjectInitializerExpression.cs new file mode 100644 index 0000000..b5dcd58 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ObjectInitializerExpression.cs @@ -0,0 +1,49 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record ObjectInitializerExpression(IReadOnlyDictionary? Parameters = null, bool UseSingleLine = true) : InitializerExpression + { + public sealed override void Write(CodeWriter writer) + { + if (Parameters is not { Count: > 0 }) + { + writer.AppendRaw("{}"); + return; + } + + if (UseSingleLine) + { + writer.AppendRaw("{"); + foreach (var (name, value) in Parameters) + { + writer.Append($"{name} = "); + value.Write(writer); + writer.AppendRaw(", "); + } + + writer.RemoveTrailingComma(); + writer.AppendRaw("}"); + } + else + { + writer.Line(); + writer.LineRaw("{"); + foreach (var (name, value) in Parameters) + { + writer.Append($"{name} = "); + value.Write(writer); + writer.LineRaw(","); + } + // Commented to minimize changes in generated code + //writer.RemoveTrailingComma(); + //writer.Line(); + writer.AppendRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ParameterReference.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ParameterReference.cs new file mode 100644 index 0000000..c17d506 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ParameterReference.cs @@ -0,0 +1,20 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record ParameterReference(Parameter Parameter) : TypedValueExpression(Parameter.Type, new UntypedParameterReference(Parameter)) + { + private record UntypedParameterReference(Parameter Parameter) : ValueExpression + { + public override void Write(CodeWriter writer) + { + writer.AppendRawIf("ref ", Parameter.IsRef); + writer.Append($"{Parameter.Name:I}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/PositionalParameterReference.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/PositionalParameterReference.cs new file mode 100644 index 0000000..9974a3d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/PositionalParameterReference.cs @@ -0,0 +1,19 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record PositionalParameterReference(string ParameterName, ValueExpression ParameterValue) : ValueExpression + { + public PositionalParameterReference(Parameter parameter) : this(parameter.Name, parameter) { } + + public sealed override void Write(CodeWriter writer) + { + writer.Append($"{ParameterName}: "); + ParameterValue.Write(writer); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/StringLiteralExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/StringLiteralExpression.cs new file mode 100644 index 0000000..b334b9b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/StringLiteralExpression.cs @@ -0,0 +1,19 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record StringLiteralExpression(string Literal, bool U8) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.Literal(Literal); + if (U8) + { + writer.AppendRaw("u8"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchCaseExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchCaseExpression.cs new file mode 100644 index 0000000..07ae43c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchCaseExpression.cs @@ -0,0 +1,17 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record SwitchCaseExpression(ValueExpression Case, ValueExpression Expression) + { + public static SwitchCaseExpression When(ValueExpression caseExpression, BoolExpression condition, ValueExpression expression) + { + return new(new SwitchCaseWhenExpression(caseExpression, condition), expression); + } + public static SwitchCaseExpression Default(ValueExpression expression) => new SwitchCaseExpression(Dash, expression); + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchCaseWhenExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchCaseWhenExpression.cs new file mode 100644 index 0000000..ec79319 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchCaseWhenExpression.cs @@ -0,0 +1,18 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record SwitchCaseWhenExpression(ValueExpression Case, BoolExpression Condition) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + Case.Write(writer); + writer.AppendRaw(" when "); + Condition.Write(writer); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchExpression.cs new file mode 100644 index 0000000..72768c9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/SwitchExpression.cs @@ -0,0 +1,30 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record SwitchExpression(ValueExpression MatchExpression, params SwitchCaseExpression[] Cases) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + using (writer.AmbientScope()) + { + MatchExpression.Write(writer); + writer.LineRaw(" switch"); + writer.LineRaw("{"); + foreach (var switchCase in Cases) + { + switchCase.Case.Write(writer); + writer.AppendRaw(" => "); + switchCase.Expression.Write(writer); + writer.LineRaw(","); + } + writer.RemoveTrailingComma(); + writer.Line(); + writer.AppendRaw("}"); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TernaryConditionalOperator.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TernaryConditionalOperator.cs new file mode 100644 index 0000000..04b06b9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TernaryConditionalOperator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record TernaryConditionalOperator(ValueExpression Condition, ValueExpression Consequent, ValueExpression Alternative) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + Condition.Write(writer); + writer.AppendRaw(" ? "); + Consequent.Write(writer); + writer.AppendRaw(" : "); + Alternative.Write(writer); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypeReference.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypeReference.cs new file mode 100644 index 0000000..7ce3351 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypeReference.cs @@ -0,0 +1,16 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record TypeReference(CSharpType Type) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.Append($"{Type}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedFuncExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedFuncExpression.cs new file mode 100644 index 0000000..a59041d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedFuncExpression.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record TypedFuncExpression(IReadOnlyList Parameters, TypedValueExpression Inner) : TypedValueExpression(Inner.Type, new FuncExpression(Parameters, Inner)); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedMemberExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedMemberExpression.cs new file mode 100644 index 0000000..44d6863 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedMemberExpression.cs @@ -0,0 +1,9 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record TypedMemberExpression(ValueExpression? Inner, string MemberName, CSharpType MemberType) : TypedValueExpression(MemberType, new MemberExpression(Inner, MemberName)); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedNullConditionalExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedNullConditionalExpression.cs new file mode 100644 index 0000000..eaca602 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedNullConditionalExpression.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record TypedNullConditionalExpression(TypedValueExpression Inner) : TypedValueExpression(Inner.Type, new NullConditionalExpression(Inner.Untyped)); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedTernaryConditionalOperator.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedTernaryConditionalOperator.cs new file mode 100644 index 0000000..8cc8d95 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedTernaryConditionalOperator.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record TypedTernaryConditionalOperator(BoolExpression Condition, TypedValueExpression Consequent, ValueExpression Alternative) + : TypedValueExpression(Consequent.Type, new TernaryConditionalOperator(Condition, Consequent, Alternative)); +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedValueExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedValueExpression.cs new file mode 100644 index 0000000..5ee83ff --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedValueExpression.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + /// + /// A wrapper around ValueExpression that also specifies return type of the expression + /// Return type doesn't affect how expression is written, but help creating strong-typed helpers to create value expressions + /// + /// Type expected to be returned by value expression + /// + internal record TypedValueExpression(CSharpType Type, ValueExpression Untyped) : ValueExpression + { + public sealed override void Write(CodeWriter writer) => Untyped.Write(writer); + + public static implicit operator TypedValueExpression(FieldDeclaration name) => new VariableReference(name.Type, name.Declaration); + public static implicit operator TypedValueExpression(Parameter parameter) => new ParameterReference(parameter); + + public TypedValueExpression NullableStructValue() => this is not ConstantExpression && Type is { IsNullable: true, IsValueType: true } ? new TypedMemberExpression(this, nameof(Nullable.Value), Type.WithNullable(false)) : this; + + public TypedValueExpression NullConditional() => Type.IsNullable ? new TypedNullConditionalExpression(this) : this; + + public virtual TypedValueExpression Property(string propertyName, CSharpType propertyType) + => new TypedMemberExpression(this, propertyName, propertyType); + + protected static ValueExpression ValidateType(TypedValueExpression typed, CSharpType type) + { + if (type.EqualsIgnoreNullable(typed.Type)) + { + return typed.Untyped; + } + + throw new InvalidOperationException($"Expression with return type {typed.Type.Name} is cast to type {type.Name}"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedValueExpressionOfT.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedValueExpressionOfT.cs new file mode 100644 index 0000000..322a86b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/TypedValueExpressionOfT.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ +#pragma warning disable SA1649 // File name should match first type name + internal abstract record TypedValueExpression(ValueExpression Untyped) : TypedValueExpression(typeof(T), ValidateType(Untyped, typeof(T))) +#pragma warning restore SA1649 // File name should match first type name + { + protected static MemberExpression StaticProperty(string name) => new(typeof(T), name); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName) + => new(typeof(T), methodName, Array.Empty(), null, false); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName, ValueExpression arg) + => new(typeof(T), methodName, new[] { arg }, null, false); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName, ValueExpression arg1, ValueExpression arg2) + => new(typeof(T), methodName, new[] { arg1, arg2 }, null, false); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName, IReadOnlyList arguments) + => new(typeof(T), methodName, arguments, null, false); + + protected static InvokeStaticMethodExpression InvokeStatic(MethodSignature method) + => new(typeof(T), method.Name, method.Parameters.Select(p => (ValueExpression)p).ToList()); + + protected static InvokeStaticMethodExpression InvokeStatic(MethodSignature method, bool async) + => new(typeof(T), method.Name, method.Parameters.Select(p => (ValueExpression)p).ToList(), CallAsAsync: async); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName, bool async) + => new(typeof(T), methodName, Array.Empty(), CallAsAsync: async); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName, ValueExpression arg, bool async) + => new(typeof(T), methodName, new[] { arg }, CallAsAsync: async); + + protected static InvokeStaticMethodExpression InvokeStatic(string methodName, IReadOnlyList arguments, bool async) + => new(typeof(T), methodName, arguments, CallAsAsync: async); + + protected InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, string methodName) + => new(extensionType, methodName, new[] { Untyped }, CallAsAsync: false, CallAsExtension: true); + + protected InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, string methodName, ValueExpression arg) + => new(extensionType, methodName, new[] { Untyped, arg }, CallAsAsync: false, CallAsExtension: true); + + protected InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, string methodName, ValueExpression arg1, ValueExpression arg2) + => new(extensionType, methodName, new[] { Untyped, arg1, arg2 }, CallAsAsync: false, CallAsExtension: true); + + protected InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, string methodName, IEnumerable arguments, bool async) + => new(extensionType, methodName, arguments.Prepend(Untyped).ToArray(), CallAsAsync: async, CallAsExtension: true); + + private static ValueExpression ValidateType(ValueExpression untyped, Type type) + { +#if DEBUG + if (untyped is not TypedValueExpression typed) + { + return untyped; + } + + if (typed.Type.IsFrameworkType) + { + if (typed.Type.FrameworkType.IsGenericTypeDefinition && type.IsGenericType) + { + if (typed.Type.FrameworkType.MakeGenericType(type.GetGenericArguments()).IsAssignableTo(type)) + { + return typed.Untyped; + } + } + else if (typed.Type.FrameworkType.IsAssignableTo(type)) + { + return typed.Untyped; + } + } + + throw new InvalidOperationException($"Expression with return type {typed.Type.Name} is cast to type {type.Name}"); +#else + return untyped; +#endif + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/UnaryOperatorExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/UnaryOperatorExpression.cs new file mode 100644 index 0000000..9c31c41 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/UnaryOperatorExpression.cs @@ -0,0 +1,17 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record UnaryOperatorExpression(string Operator, ValueExpression Operand, bool OperandOnTheLeft) : ValueExpression + { + public sealed override void Write(CodeWriter writer) + { + writer.AppendRawIf(Operator, !OperandOnTheLeft); + Operand.Write(writer); + writer.AppendRawIf(Operator, OperandOnTheLeft); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ValueExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ValueExpression.cs new file mode 100644 index 0000000..350d100 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/ValueExpression.cs @@ -0,0 +1,70 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal record ValueExpression + { + public virtual void Write(CodeWriter writer) { } + + public static implicit operator ValueExpression(Type type) => new TypeReference(type); + public static implicit operator ValueExpression(CSharpType type) => new TypeReference(type); + public static implicit operator ValueExpression(Parameter parameter) => new ParameterReference(parameter); + public static implicit operator ValueExpression(FieldDeclaration name) => new VariableReference(name.Type, name.Declaration); + public static implicit operator ValueExpression(PropertyDeclaration name) => new VariableReference(name.PropertyType, name.Declaration); + + public ValueExpression NullableStructValue(CSharpType candidateType) => this is not ConstantExpression && candidateType is { IsNullable: true, IsValueType: true } ? new MemberExpression(this, nameof(Nullable.Value)) : this; + public StringExpression InvokeToString() => new(Invoke(nameof(ToString))); + public ValueExpression InvokeGetType() => Invoke(nameof(GetType)); + + public BoolExpression InvokeEquals(ValueExpression other) => new(Invoke(nameof(Equals), other)); + + public virtual ValueExpression Property(string propertyName, bool nullConditional = false) + => new MemberExpression(nullConditional ? new NullConditionalExpression(this) : this, propertyName); + + public InvokeInstanceMethodExpression Invoke(string methodName) + => new InvokeInstanceMethodExpression(this, methodName, Array.Empty(), null, false); + + public InvokeInstanceMethodExpression Invoke(string methodName, ValueExpression arg) + => new InvokeInstanceMethodExpression(this, methodName, new[] { arg }, null, false); + + public InvokeInstanceMethodExpression Invoke(string methodName, ValueExpression arg1, ValueExpression arg2) + => new InvokeInstanceMethodExpression(this, methodName, new[] { arg1, arg2 }, null, false); + + public InvokeInstanceMethodExpression Invoke(string methodName, IReadOnlyList arguments) + => new InvokeInstanceMethodExpression(this, methodName, arguments, null, false); + + public InvokeInstanceMethodExpression Invoke(MethodSignature method) + => new InvokeInstanceMethodExpression(this, method.Name, method.Parameters.Select(p => (ValueExpression)p).ToList(), null, method.Modifiers.HasFlag(MethodSignatureModifiers.Async)); + + public InvokeInstanceMethodExpression Invoke(MethodSignature method, IReadOnlyList arguments, bool addConfigureAwaitFalse = true) + => new InvokeInstanceMethodExpression(this, method.Name, arguments, null, method.Modifiers.HasFlag(MethodSignatureModifiers.Async), AddConfigureAwaitFalse: addConfigureAwaitFalse); + + public InvokeInstanceMethodExpression Invoke(string methodName, bool async) + => new InvokeInstanceMethodExpression(this, methodName, Array.Empty(), null, async); + + public InvokeInstanceMethodExpression Invoke(string methodName, IReadOnlyList arguments, bool async) + => new InvokeInstanceMethodExpression(this, methodName, arguments, null, async); + + public CastExpression CastTo(CSharpType to) => new CastExpression(this, to); + + private string GetDebuggerDisplay() + { + using var writer = new DebuggerCodeWriter(); + Write(writer); + return writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/VariableReference.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/VariableReference.cs new file mode 100644 index 0000000..023defb --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/VariableReference.cs @@ -0,0 +1,21 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record VariableReference(CSharpType Type, CodeWriterDeclaration Declaration) : TypedValueExpression(Type, new UntypedVariableReference(Declaration)) + { + public VariableReference(CSharpType type, string name) : this(type, new CodeWriterDeclaration(name)) { } + + private record UntypedVariableReference(CodeWriterDeclaration Declaration) : ValueExpression + { + public override void Write(CodeWriter writer) + { + writer.Append(Declaration); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/WhereExpression.cs b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/WhereExpression.cs new file mode 100644 index 0000000..5701e93 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Expressions/ValueExpressions/WhereExpression.cs @@ -0,0 +1,29 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Common.Output.Expressions.ValueExpressions +{ + internal record WhereExpression(CSharpType Type, IReadOnlyList Constraints) : ValueExpression + { + public WhereExpression(CSharpType type, ValueExpression constraint) : this(type, new[] { constraint }) { } + + public WhereExpression And(ValueExpression constraint) => new(Type, new List(Constraints) { constraint }); + + public sealed override void Write(CodeWriter writer) + { + writer + .AppendRaw("where ") + .Append($"{Type} : "); + foreach (var constraint in Constraints) + { + constraint.Write(writer); + writer.AppendRaw(","); + } + writer.RemoveTrailingComma(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/ClientFields.cs b/logger/autorest.csharp/common/Output/Models/ClientFields.cs new file mode 100644 index 0000000..a82dfa4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/ClientFields.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.Core.Pipeline; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using static AutoRest.CSharp.Output.Models.FieldModifiers; + +namespace AutoRest.CSharp.Output.Models +{ + internal class ClientFields : IReadOnlyCollection + { + public FieldDeclaration? AuthorizationHeaderConstant { get; } + public FieldDeclaration? AuthorizationApiKeyPrefixConstant { get; } + public FieldDeclaration? ScopesConstant { get; } + + public FieldDeclaration ClientDiagnosticsProperty { get; } + public FieldDeclaration PipelineField { get; } + public FieldDeclaration? EndpointField { get; } + + public CodeWriterScopeDeclarations ScopeDeclarations { get; } + + private readonly FieldDeclaration? _keyAuthField; + private readonly FieldDeclaration? _tokenAuthField; + private readonly IReadOnlyList _fields; + private readonly IReadOnlyDictionary _parameterNamesToFields; + private static readonly FormattableString ClientDiagnosticsDescription = $"The ClientDiagnostics is used to provide tracing support for the client library."; + + public IReadOnlyList CredentialFields { get; } + + public static ClientFields CreateForClient(IEnumerable parameters, InputAuth authorization) => new(parameters, authorization); + + public static ClientFields CreateForRestClient(IEnumerable parameters) => new(parameters, null); + + private ClientFields(IEnumerable parameters, InputAuth? authorization) + { + ClientDiagnosticsProperty = new(ClientDiagnosticsDescription, Internal | ReadOnly, typeof(ClientDiagnostics), KnownParameters.ClientDiagnostics.Name.FirstCharToUpperCase(), SerializationFormat.Default, writeAsProperty: true); + PipelineField = new(Private | ReadOnly, Configuration.ApiTypes.HttpPipelineType, "_" + KnownParameters.Pipeline.Name); + + var parameterNamesToFields = new Dictionary(); + var fields = new List(); + var credentialFields = new List(); + var properties = new List(); + + if (authorization != null) + { + parameterNamesToFields[KnownParameters.Pipeline.Name] = PipelineField; + parameterNamesToFields[KnownParameters.ClientDiagnostics.Name] = ClientDiagnosticsProperty; + + if (authorization.ApiKey is not null) + { + AuthorizationHeaderConstant = new(Private | Const, typeof(string), "AuthorizationHeader", Literal(authorization.ApiKey.Name), SerializationFormat.Default); + _keyAuthField = new(Private | ReadOnly, KnownParameters.KeyAuth.Type.WithNullable(false), "_" + KnownParameters.KeyAuth.Name); + + fields.Add(AuthorizationHeaderConstant); + fields.Add(_keyAuthField); + if (authorization.ApiKey.Prefix is not null) + { + AuthorizationApiKeyPrefixConstant = new(Private | Const, typeof(string), "AuthorizationApiKeyPrefix", Literal(authorization.ApiKey.Prefix), SerializationFormat.Default); + fields.Add(AuthorizationApiKeyPrefixConstant); + } + credentialFields.Add(_keyAuthField); + parameterNamesToFields[KnownParameters.KeyAuth.Name] = _keyAuthField; + } + + if (authorization.OAuth2 is not null) + { + var scopeExpression = New.Array(typeof(string), true, authorization.OAuth2.Scopes.Select(Literal).ToArray()); + ScopesConstant = new(Private | Static | ReadOnly, typeof(string[]), "AuthorizationScopes", scopeExpression, SerializationFormat.Default); + _tokenAuthField = new(Private | ReadOnly, KnownParameters.TokenAuth.Type.WithNullable(false), "_" + KnownParameters.TokenAuth.Name); + + fields.Add(ScopesConstant); + fields.Add(_tokenAuthField); + credentialFields.Add(_tokenAuthField); + parameterNamesToFields[KnownParameters.TokenAuth.Name] = _tokenAuthField; + } + + fields.Add(PipelineField); + } + + foreach (Parameter parameter in parameters) + { + var field = parameter == KnownParameters.ClientDiagnostics ? ClientDiagnosticsProperty : parameter == KnownParameters.Pipeline ? PipelineField : parameter.IsResourceIdentifier + ? new FieldDeclaration($"{parameter.Description}", Public | ReadOnly, parameter.Type, parameter.Name.FirstCharToUpperCase(), SerializationFormat.Default, writeAsProperty: true) + : new FieldDeclaration(Private | ReadOnly, parameter.Type, "_" + parameter.Name); + + if (field.WriteAsProperty) + { + properties.Add(field); + } + else + { + fields.Add(field); + } + parameterNamesToFields.Add(parameter.Name, field); + + if (parameter.Name == KnownParameters.Endpoint.Name && parameter.Type.EqualsIgnoreNullable(KnownParameters.Endpoint.Type)) + { + EndpointField = field; + } + } + + fields.AddRange(properties); + if (authorization != null && Configuration.IsBranded) + { + fields.Add(ClientDiagnosticsProperty); + } + + _fields = fields; + _parameterNamesToFields = parameterNamesToFields; + CredentialFields = credentialFields; + ScopeDeclarations = new CodeWriterScopeDeclarations(fields.Select(f => f.Declaration)); + } + + public FieldDeclaration? GetFieldByParameter(string parameterName, CSharpType parameterType) + => parameterName switch + { + "credential" when _keyAuthField != null && parameterType.EqualsIgnoreNullable(_keyAuthField.Type) => _keyAuthField, + "credential" when _tokenAuthField != null && parameterType.EqualsIgnoreNullable(_tokenAuthField.Type) => _tokenAuthField, + var name => _parameterNamesToFields.TryGetValue(name, out var field) ? parameterType.Equals(field.Type) ? field : null : null + }; + + public FieldDeclaration? GetFieldByParameter(Parameter parameter) + => GetFieldByParameter(parameter.Name, parameter.Type); + + public FieldDeclaration? GetFieldByName(string name) + => _parameterNamesToFields.TryGetValue(name, out FieldDeclaration? field) ? field : null; + + public IEnumerator GetEnumerator() => _fields.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _fields.GetEnumerator(); + public int Count => _fields.Count; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/ClientMethod.cs b/logger/autorest.csharp/common/Output/Models/ClientMethod.cs new file mode 100644 index 0000000..e97f230 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/ClientMethod.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Requests; + +namespace AutoRest.CSharp.Output.Models +{ + internal class ClientMethod + { + public ClientMethod(string name, RestClientMethod restClientMethod, string? description, Diagnostic diagnostics, string accessibility) + { + Name = name; + RestClientMethod = restClientMethod; + Description = description; + Diagnostics = diagnostics; + Accessibility = accessibility; + } + + public string Name { get; } + public RestClientMethod RestClientMethod { get; } + public string? Description { get; } + public Diagnostic Diagnostics { get; } + public string Accessibility { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/CmcRestClient.cs b/logger/autorest.csharp/common/Output/Models/CmcRestClient.cs new file mode 100644 index 0000000..a3a1a32 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/CmcRestClient.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models +{ + internal abstract class CmcRestClient : TypeProvider + { + private readonly Lazy> _requestMethods; + private readonly Lazy> _nextPageRequestMethods; + private (InputOperation Operation, RestClientMethod Method)[]? _allMethods; + private ConstructorSignature? _constructor; + + internal InputClient InputClient { get; } + public IReadOnlyList Parameters { get; } + public (InputOperation Operation, RestClientMethod Method)[] Methods => _allMethods ??= BuildAllMethods().ToArray(); + public ConstructorSignature Constructor => _constructor ??= new ConstructorSignature(Type, $"Initializes a new instance of {Declaration.Name}", null, MethodSignatureModifiers.Public, Parameters); + + public string ClientPrefix { get; } + protected override string DefaultName { get; } + protected override string DefaultAccessibility => "internal"; + + protected CmcRestClient(InputClient inputClient, BuildContext context, string? clientName, IReadOnlyList parameters) : base(context) + { + InputClient = inputClient; + + _requestMethods = new Lazy>(EnsureNormalMethods); + _nextPageRequestMethods = new Lazy>(EnsureGetNextPageMethods); + + Parameters = parameters; + + var clientPrefix = ClientBuilder.GetClientPrefix(clientName ?? inputClient.Name, context); + ClientPrefix = clientPrefix; + DefaultName = clientPrefix + "Rest" + ClientBuilder.GetClientSuffix(); + } + + private IEnumerable<(InputOperation Operation, RestClientMethod Method)> BuildAllMethods() + { + foreach (var operation in InputClient.Operations) + { + RestClientMethod method = GetOperationMethod(operation); + yield return (operation, method); + } + + foreach (var operation in InputClient.Operations) + { + // remove duplicates when GetNextPage method is not autogenerated + if (GetNextOperationMethod(operation) is { } nextOperationMethod && + operation.Paging?.NextLinkOperation == null) + { + yield return (operation, nextOperationMethod); + } + } + } + + protected abstract Dictionary EnsureNormalMethods(); + + protected Dictionary EnsureGetNextPageMethods() + { + var nextPageMethods = new Dictionary(); + foreach (var operation in InputClient.Operations) + { + var paging = operation.Paging; + if (paging == null) + { + continue; + } + RestClientMethod? nextMethod = null; + if (paging.NextLinkOperation != null) + { + nextMethod = GetOperationMethod(paging.NextLinkOperation); + } + else if (paging.NextLinkName != null) + { + var method = GetOperationMethod(operation); + nextMethod = CmcRestClientBuilder.BuildNextPageMethod(operation, method); + } + + if (nextMethod != null) + { + nextPageMethods.Add(operation, nextMethod); + } + } + + return nextPageMethods; + } + + public RestClientMethod? GetNextOperationMethod(InputOperation operation) + { + _nextPageRequestMethods.Value.TryGetValue(operation, out RestClientMethod? value); + return value; + } + + public RestClientMethod GetOperationMethod(InputOperation operation) + { + return _requestMethods.Value[operation]; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/CmcRestClientBuilder.cs b/logger/autorest.csharp/common/Output/Models/CmcRestClientBuilder.cs new file mode 100644 index 0000000..d477c5b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/CmcRestClientBuilder.cs @@ -0,0 +1,576 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Request = AutoRest.CSharp.Output.Models.Requests.Request; +using Response = AutoRest.CSharp.Output.Models.Responses.Response; +using StatusCodes = AutoRest.CSharp.Output.Models.Responses.StatusCodes; + +namespace AutoRest.CSharp.Output.Models +{ + internal class CmcRestClientBuilder + { + private static readonly HashSet IgnoredRequestHeader = new(StringComparer.OrdinalIgnoreCase) + { + "x-ms-client-request-id", + "tracestate", + "traceparent" + }; + + private static readonly Dictionary ConditionRequestHeader = new(StringComparer.OrdinalIgnoreCase) + { + ["If-Match"] = RequestConditionHeaders.IfMatch, + ["If-None-Match"] = RequestConditionHeaders.IfNoneMatch, + ["If-Modified-Since"] = RequestConditionHeaders.IfModifiedSince, + ["If-Unmodified-Since"] = RequestConditionHeaders.IfUnmodifiedSince + }; + + protected readonly BuildContext _context; + private readonly OutputLibrary _library; + private readonly Dictionary _parameters; + protected readonly IEnumerable _clientParameters; + + public CmcRestClientBuilder(IEnumerable clientParameters, BuildContext context) + { + _context = context; + _library = context.BaseLibrary!; + _clientParameters = clientParameters; + _parameters = clientParameters.ToDictionary(p => p.Name, BuildConstructorParameter); + } + + /// + /// Get sorted parameters, required parameters are at the beginning. + /// + /// + public Parameter[] GetOrderedParametersByRequired() + { + return OrderParametersByRequired(_parameters.Values); + } + + private static string GetRequestParameterName(InputParameter requestParameter) => requestParameter.NameInRequest ?? requestParameter.Name; + + public IReadOnlyDictionary GetReferencesToOperationParameters(InputOperation operation, IEnumerable requestParameters, IEnumerable clientParameters) + { + var allParameters = GetOperationAllParameters(operation, requestParameters, clientParameters); + return allParameters.ToDictionary(kvp => GetRequestParameterName(kvp.Key), kvp => (CreateReference(kvp.Key, kvp.Value), kvp.Value.SkipUrlEncoding)); + } + + /// + /// Build CmcRestClientMethod for mgmt + /// + /// + /// + /// + /// + /// + /// + /// + public RestClientMethod BuildMethod(InputOperation operation, IEnumerable requestParameters, IEnumerable clientParameters, DataPlaneResponseHeaderGroupType? responseHeaderModel, string accessibility, Func? returnNullOn404Func = null) + { + var allParameters = GetOperationAllParameters(operation, requestParameters, clientParameters); + var methodParameters = BuildMethodParameters(allParameters); + var references = allParameters.ToDictionary(kvp => GetRequestParameterName(kvp.Key), kvp => new ParameterInfo(kvp.Key, CreateReference(kvp.Key, kvp.Value))); + var request = BuildRequest(operation, new RequestMethodBuildContext(methodParameters, references)); + + var isHeadAsBoolean = request.HttpMethod == RequestMethod.Head && Configuration.HeadAsBoolean; + Response[] responses = BuildResponses(operation, isHeadAsBoolean, out var responseType, returnNullOn404Func); + + return new RestClientMethod( + operation.CSharpName(), + BuilderHelpers.EscapeXmlDocDescription(operation.Summary ?? string.Empty), + BuilderHelpers.EscapeXmlDocDescription(operation.Description), + responseType, + request, + methodParameters, + responses, + responseHeaderModel, + operation.BufferResponse, + accessibility: accessibility, + operation + ); + } + + private Dictionary GetOperationAllParameters(InputOperation operation, IEnumerable requestParameters, IEnumerable clientParameters) + { + var parameters = operation.Parameters + .Concat(clientParameters) + .Concat(requestParameters) + .Where(rp => !IsIgnoredHeaderParameter(rp)) + .DistinctBy(p => p.NameInRequest ?? p.Name) + .ToArray(); + + // TODO: why are there duplicates in the list? + return parameters.Distinct().ToDictionary(rp => rp, requestParameter => BuildParameter(requestParameter, null, operation.KeepClientDefaultValue)); + } + + private Response[] BuildResponses(InputOperation operation, bool headAsBoolean, out CSharpType? responseType, Func? returnNullOn404Func = null) + { + if (headAsBoolean) + { + responseType = new CSharpType(typeof(bool)); + return new[] + { + new Response( + new ConstantResponseBody(new Constant(true, responseType)), + new[] {new StatusCodes(null, 2)}), + new Response( + new ConstantResponseBody(new Constant(false, responseType)), + new[] {new StatusCodes(null, 4)}), + }; + } + + List clientResponse = new List(); + foreach (var response in operation.Responses) + { + List statusCodes = new List(); + foreach (var statusCode in response.StatusCodes) + { + statusCodes.Add(new StatusCodes(statusCode, null)); + } + + clientResponse.Add(new Response( + operation.IsLongRunning ? null : BuildResponseBody(response), + statusCodes.ToArray() + )); + } + + if (returnNullOn404Func != null && returnNullOn404Func(clientResponse.FirstOrDefault()?.ResponseBody?.Type.Name)) + clientResponse.Add(new Response(null, new[] { new StatusCodes(404, null) })); + + responseType = ReduceResponses(clientResponse); + return clientResponse.ToArray(); + } + + private Request BuildRequest(InputOperation operation, RequestMethodBuildContext buildContext) + { + var uriParametersMap = new Dictionary(); + var pathParametersMap = new Dictionary(); + var queryParameters = new List(); + var headerParameters = new List(); + foreach (var (parameterName, (requestParameter, reference)) in buildContext.References) + { + if (requestParameter == null) + { + if (parameterName == KnownParameters.MatchConditionsParameter.Name || parameterName == KnownParameters.RequestConditionsParameter.Name) + { + headerParameters.Add(new RequestHeader(parameterName, reference, null, buildContext.ConditionalRequestSerializationFormat)); + } + continue; + } + + var serializationFormat = GetSerializationFormat(requestParameter); + var escape = !requestParameter.SkipUrlEncoding; + + switch (requestParameter.Location) + { + case RequestLocation.Uri: + uriParametersMap.Add(parameterName, new PathSegment(reference, escape, serializationFormat, isRaw: true)); + break; + case RequestLocation.Path: + pathParametersMap.Add(parameterName, new PathSegment(reference, escape, serializationFormat, isRaw: false)); + break; + case RequestLocation.Query: + queryParameters.Add(new QueryParameter(parameterName, reference, requestParameter.ArraySerializationDelimiter, escape, serializationFormat, requestParameter.Explode, requestParameter.IsApiVersion)); + break; + case RequestLocation.Header: + var headerName = requestParameter.HeaderCollectionPrefix ?? parameterName; + headerParameters.Add(new RequestHeader(headerName, reference, requestParameter.ArraySerializationDelimiter, serializationFormat)); + break; + } + } + + var uriParameters = GetPathSegments(operation.Uri, uriParametersMap, isRaw: true); + var pathParameters = GetPathSegments(operation.Path, pathParametersMap, isRaw: false); + + var body = buildContext.BodyParameter != null + ? new RequestContentRequestBody(buildContext.BodyParameter) + : operation.RequestBodyMediaType != BodyMediaType.None + ? BuildRequestBody(buildContext.References, operation.RequestBodyMediaType) + : null; + + return new Request( + operation.HttpMethod, + uriParameters.Concat(pathParameters).ToArray(), + queryParameters.ToArray(), + headerParameters.ToArray(), + body + ); + } + + protected virtual Parameter[] BuildMethodParameters(IReadOnlyDictionary allParameters) + { + List methodParameters = new(); + foreach (var (requestParameter, parameter) in allParameters) + { + // Grouped and flattened parameters shouldn't be added to methods + if (IsMethodParameter(requestParameter)) + { + methodParameters.Add(parameter); + } + } + + return OrderParametersByRequired(methodParameters); + } + + private RequestBody? BuildRequestBody(IReadOnlyDictionary allParameters, BodyMediaType mediaType) + { + RequestBody? body = null; + + Dictionary bodyParameters = new(); + foreach (var (_, (requestParameter, value)) in allParameters) + { + if (requestParameter?.Location == RequestLocation.Body) + { + bodyParameters[requestParameter] = value; + } + } + + if (bodyParameters.Count > 0) + { + if (mediaType == BodyMediaType.Multipart) + { + List value = new List(); + foreach (var parameter in bodyParameters) + { + var type = parameter.Value.Type; + RequestBody requestBody; + + if (type.Equals(typeof(string))) + { + requestBody = new TextRequestBody(parameter.Value); + } + else if (type.IsFrameworkType && type.FrameworkType == typeof(Stream)) + { + requestBody = new BinaryRequestBody(parameter.Value); + } + else if (type.IsList) + { + requestBody = new BinaryCollectionRequestBody(parameter.Value); + } + else + { + throw new NotImplementedException(); + } + + value.Add(new MultipartRequestBodyPart(parameter.Value.Reference.Name, requestBody)); + } + + body = new MultipartRequestBody(value.ToArray()); + } + else if (mediaType == BodyMediaType.Form) + { + UrlEncodedBody urlbody = new UrlEncodedBody(); + foreach (var (bodyRequestParameter, bodyParameterValue) in bodyParameters) + { + urlbody.Add(GetRequestParameterName(bodyRequestParameter), bodyParameterValue); + } + + body = urlbody; + } + else + { + Debug.Assert(bodyParameters.Count == 1); + var (bodyRequestParameter, bodyParameterValue) = bodyParameters.Single(); + if (mediaType == BodyMediaType.Binary) + { + body = new BinaryRequestBody(bodyParameterValue); + } + else if (mediaType == BodyMediaType.Text) + { + body = new TextRequestBody(bodyParameterValue); + } + else + { + var serialization = SerializationBuilder.Build( + mediaType, + bodyRequestParameter.Type, + bodyParameterValue.Type, + null); + + // This method has a flattened body + if (bodyRequestParameter.Kind == InputOperationParameterKind.Flattened) + { + var objectType = (SchemaObjectType)_context.TypeFactory.CreateType(bodyRequestParameter.Type).Implementation; + var initializationMap = new List(); + foreach (var (parameter, _) in allParameters.Values) + { + if (parameter?.FlattenedBodyProperty is null) + { + continue; + } + + initializationMap.Add(new ObjectPropertyInitializer( + objectType.GetPropertyForSchemaProperty(parameter.FlattenedBodyProperty, true), + allParameters[GetRequestParameterName(parameter!)].Reference)); + } + + body = new FlattenedSchemaRequestBody(objectType, initializationMap.ToArray(), serialization); + } + else + { + body = new SchemaRequestBody(bodyParameterValue, serialization); + } + } + } + } + + return body; + } + + private ReferenceOrConstant CreateReference(InputParameter requestParameter, Parameter parameter) + { + if (requestParameter.Kind == InputOperationParameterKind.Client) + { + return (ReferenceOrConstant)_parameters[requestParameter.Name]; + } + + if (requestParameter is { Kind: InputOperationParameterKind.Constant }) + { + if (requestParameter.Type is InputLiteralType { Value: not null } literalType) + { + return ParseConstant(literalType); + } + if (requestParameter.DefaultValue is not null) // TODO: we're using default value to store constant value, this should be fixed. + { + return ParseConstant(requestParameter.DefaultValue); + } + } + + var groupedByParameter = requestParameter.GroupedBy; + if (groupedByParameter == null) + { + return parameter; + } + + var groupModel = (SchemaObjectType)_context.TypeFactory.CreateType(groupedByParameter.Type).Implementation; + var property = groupModel.GetPropertyForGroupedParameter(requestParameter.Name); + + return new Reference($"{groupedByParameter.CSharpName()}.{property.Declaration.Name}", property.Declaration.Type); + } + + private static SerializationFormat GetSerializationFormat(InputParameter requestParameter) + => SerializationBuilder.GetSerializationFormat(requestParameter.Type); + + private ResponseBody? BuildResponseBody(OperationResponse response) + { + if (response.BodyMediaType == BodyMediaType.Text) + { + return new StringResponseBody(); + } + else if (response.BodyMediaType == BodyMediaType.Binary) + { + return new StreamResponseBody(); + } + else if (response.BodyType is not null) + { + CSharpType responseType = _context.TypeFactory.CreateType(response.BodyType).OutputType; + + ObjectSerialization serialization = SerializationBuilder.Build(response.BodyMediaType, response.BodyType, responseType, null); + + return new ObjectResponseBody(responseType, serialization); + } + + return null; + } + + private static IEnumerable GetPathSegments(string httpRequestUri, IReadOnlyDictionary parameters, bool isRaw) + { + var segments = new List(); + + foreach ((ReadOnlySpan span, bool isLiteral) in StringExtensions.GetPathParts(httpRequestUri)) + { + var text = span.ToString(); + if (isLiteral) + { + segments.Add(new PathSegment(BuilderHelpers.StringConstant(text), false, SerializationFormat.Default, isRaw)); + } + else + { + if (parameters.TryGetValue(text, out var parameter)) + { + segments.Add(parameter); + } + else + { + ErrorHelpers.ThrowError($"\n\nError while processing request '{httpRequestUri}'\n\n '{text}' in URI is missing a matching definition in the path parameters collection{ErrorHelpers.UpdateSwaggerOrFile}"); + } + } + } + + return segments; + } + + /// + /// Sort the parameters, move required parameters at the beginning, in order. + /// + /// Parameters to sort + /// + private static Parameter[] OrderParametersByRequired(IEnumerable parameters) => parameters.OrderBy(p => p.IsOptionalInSignature).ToArray(); + + // Merges operations without response types types together + private CSharpType? ReduceResponses(List responses) + { + foreach (var typeGroup in responses.GroupBy(r => r.ResponseBody)) + { + foreach (var individualResponse in typeGroup) + { + responses.Remove(individualResponse); + } + + responses.Add(new Response( + typeGroup.Key, + typeGroup.SelectMany(r => r.StatusCodes).Distinct().ToArray())); + } + + var bodyTypes = responses.Select(r => r.ResponseBody?.Type) + .OfType() + .Distinct() + .ToArray(); + + return bodyTypes.Length switch + { + 0 => null, + 1 => bodyTypes[0], + _ => typeof(object) + }; + } + + public virtual Parameter BuildConstructorParameter(InputParameter requestParameter) + { + var parameter = BuildParameter(requestParameter); + if (!requestParameter.IsEndpoint) + { + return parameter; + } + + var name = "endpoint"; + var type = new CSharpType(typeof(Uri)); + var defaultValue = parameter.DefaultValue; + var description = parameter.Description; + var location = parameter.RequestLocation; + + return defaultValue != null + ? new Parameter(name, description, type, Constant.Default(type.WithNullable(true)), ValidationType.None, $"new {typeof(Uri)}({defaultValue.Value.GetConstantFormattable()})", RequestLocation: location) + : new Parameter(name, description, type, null, parameter.Validation, null, RequestLocation: location); + } + + protected static bool IsMethodParameter(InputParameter requestParameter) + => requestParameter.Kind == InputOperationParameterKind.Method && requestParameter.GroupedBy == null; + + private static bool IsIgnoredHeaderParameter(InputParameter requestParameter) + => requestParameter.Location == RequestLocation.Header && IgnoredRequestHeader.Contains(GetRequestParameterName(requestParameter)); + + private Parameter BuildParameter(in InputParameter requestParameter, Type? typeOverride = null, bool keepClientDefaultValue = false) + { + var isNullable = requestParameter.Type is InputNullableType || !requestParameter.IsRequired; + CSharpType type = typeOverride != null + ? new CSharpType(typeOverride, isNullable) + : _context.TypeFactory.CreateType(requestParameter.Type); + return Parameter.FromInputParameter(requestParameter, type, _context.TypeFactory, keepClientDefaultValue); + } + + private Constant ParseConstant(InputConstant constant) => + BuilderHelpers.ParseConstant(constant.Value, _context.TypeFactory.CreateType(constant.Type)); + + public static RestClientMethod BuildNextPageMethod(InputOperation operation, RestClientMethod method) + { + var nextPageUrlParameter = new Parameter( + "nextLink", + $"The URL to the next page of results.", + typeof(string), + DefaultValue: null, + ValidationType.AssertNotNull, + null); + + PathSegment[] pathSegments = method.Request.PathSegments + .Where(ps => ps.IsRaw) + .Append(new PathSegment(nextPageUrlParameter, false, SerializationFormat.Default, isRaw: true)) + .ToArray(); + + var request = new Request( + RequestMethod.Get, + pathSegments, + Array.Empty(), + method.Request.Headers, + null); + + Parameter[] parameters = method.Parameters.Where(p => p.Name != nextPageUrlParameter.Name) + .Prepend(nextPageUrlParameter) + .ToArray(); + + var responses = method.Responses; + + // We hardcode 200 as expected response code for paged LRO results + if (operation.IsLongRunning) + { + responses = new[] + { + new Response(null, new[] { new StatusCodes(200, null) }) + }; + } + + return new RestClientMethod( + $"{method.Name}NextPage", + method.Summary, + method.Description, + method.ReturnType, + request, + parameters, + responses, + method.HeaderModel, + bufferResponse: true, + accessibility: "internal", + method.Operation); + } + + public static IEnumerable GetRequiredParameters(IEnumerable parameters) + => parameters.Where(parameter => !parameter.IsOptionalInSignature).ToList(); + + public static IEnumerable GetOptionalParameters(IEnumerable parameters, bool includeAPIVersion = false) + => parameters.Where(parameter => parameter.IsOptionalInSignature && (includeAPIVersion || !parameter.IsApiVersionParameter)).ToList(); + + public static IReadOnlyCollection GetConstructorParameters(IReadOnlyList parameters, CSharpType? credentialType, bool includeAPIVersion = false) + { + var constructorParameters = new List(); + + constructorParameters.AddRange(GetRequiredParameters(parameters)); + + if (credentialType != null) + { + var credentialParam = new Parameter( + "credential", + Configuration.ApiTypes.CredentialDescription, + credentialType, + null, + ValidationType.AssertNotNull, + null); + constructorParameters.Add(credentialParam); + } + + constructorParameters.AddRange(GetOptionalParameters(parameters, includeAPIVersion)); + + return constructorParameters; + } + + private record RequestMethodBuildContext(IReadOnlyList OrderedParameters, IReadOnlyDictionary References, Parameter? BodyParameter = null, SerializationFormat ConditionalRequestSerializationFormat = SerializationFormat.Default, RequestConditionHeaders RequestConditionFlag = RequestConditionHeaders.None); + + private readonly record struct ParameterInfo(InputParameter? Parameter, ReferenceOrConstant Reference); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/ConstructorInitializer.cs b/logger/autorest.csharp/common/Output/Models/ConstructorInitializer.cs new file mode 100644 index 0000000..4fe5abd --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/ConstructorInitializer.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models +{ + internal record ConstructorInitializer(bool IsBase, IReadOnlyList Arguments) + { + public ConstructorInitializer(bool isBase, IEnumerable arguments) : this(isBase, arguments.Select(p => (ValueExpression)p).ToArray()) { } + + public ConstructorInitializer(bool isBase, IEnumerable arguments) : this(isBase, arguments.Select(r => new FormattableStringToExpression(FormattableStringFactory.Create("{0:I}", r.Name))).ToArray()) { } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/ConstructorSignature.cs b/logger/autorest.csharp/common/Output/Models/ConstructorSignature.cs new file mode 100644 index 0000000..30a93c2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/ConstructorSignature.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models +{ + internal record ConstructorSignature(CSharpType Type, FormattableString? Summary, FormattableString? Description, MethodSignatureModifiers Modifiers, IReadOnlyList Parameters, IReadOnlyList? Attributes = null, ConstructorInitializer? Initializer = null) + : MethodSignatureBase(Type.Name, Summary, Description, null, Modifiers, Parameters, Attributes ?? Array.Empty()); +} diff --git a/logger/autorest.csharp/common/Output/Models/FieldDeclaration.cs b/logger/autorest.csharp/common/Output/Models/FieldDeclaration.cs new file mode 100644 index 0000000..af0bc6a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/FieldDeclaration.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Output.Models +{ + internal record FieldDeclaration(FormattableString? Description, FieldModifiers Modifiers, CSharpType Type, CSharpType ValueType, CodeWriterDeclaration Declaration, ValueExpression? InitializationValue, bool IsRequired, SerializationFormat SerializationFormat, bool IsField = false, bool WriteAsProperty = false, bool OptionalViaNullability = false, FieldModifiers? GetterModifiers = null, FieldModifiers? SetterModifiers = null) + { + public string Name => Declaration.ActualName; + public string Accessibility => (Modifiers & FieldModifiers.Public) > 0 ? "public" : "internal"; + + public FieldDeclaration(FieldModifiers modifiers, CSharpType type, string name, bool writeAsProperty = false) + : this(description: null, + modifiers: modifiers, + type: type, + name: name, + serializationFormat: SerializationFormat.Default, + writeAsProperty: writeAsProperty) + { } + + public FieldDeclaration(FormattableString description, FieldModifiers modifiers, CSharpType type, string name, ValueExpression? initializationValue = null) + : this(Description: description, + Modifiers: modifiers, + Type: type, + ValueType: type, + Declaration: new CodeWriterDeclaration(name), + IsRequired: false, + InitializationValue: initializationValue, + SerializationFormat: SerializationFormat.Default) + { } + + public FieldDeclaration(FieldModifiers modifiers, CSharpType type, string name, ValueExpression? initializationValue, SerializationFormat serializationFormat, bool writeAsProperty = false) + : this(Description: null, + Modifiers: modifiers, + Type: type, + ValueType: type, + Declaration: new CodeWriterDeclaration(name), + InitializationValue: initializationValue, + IsRequired: false, + SerializationFormat: serializationFormat, + IsField: false, + WriteAsProperty: writeAsProperty, + GetterModifiers: null, + SetterModifiers: null) + { } + + public FieldDeclaration(FormattableString? description, FieldModifiers modifiers, CSharpType type, string name, SerializationFormat serializationFormat, bool writeAsProperty = false) + : this(Description: description, + Modifiers: modifiers, + Type: type, + ValueType: type, + Declaration: new CodeWriterDeclaration(name), + InitializationValue: null, + IsRequired: false, + SerializationFormat: serializationFormat, + IsField: false, + WriteAsProperty: writeAsProperty) + { } + + private VariableReference? _reference; + public VariableReference Reference => _reference ??= new VariableReference(Type, Declaration); + } + + [Flags] + internal enum FieldModifiers + { + None = 0, + Public = 1 << 0, + Internal = 1 << 1, + Protected = 1 << 2, + Private = 1 << 3, + Static = 1 << 4, + ReadOnly = 1 << 5, + Const = 1 << 6 + } +} diff --git a/logger/autorest.csharp/common/Output/Models/IndexerDeclaration.cs b/logger/autorest.csharp/common/Output/Models/IndexerDeclaration.cs new file mode 100644 index 0000000..682d87b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/IndexerDeclaration.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal record IndexerDeclaration(FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType PropertyType, string Name, Parameter IndexerParameter, PropertyBody PropertyBody, IReadOnlyDictionary? Exceptions = null) + : PropertyDeclaration(Description, Modifiers, PropertyType, Name, PropertyBody, Exceptions); +} diff --git a/logger/autorest.csharp/common/Output/Models/LongRunningOperation.cs b/logger/autorest.csharp/common/Output/Models/LongRunningOperation.cs new file mode 100644 index 0000000..2cdfbd5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/LongRunningOperation.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Diagnostics; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class LongRunningOperation : TypeProvider + { + public LongRunningOperation(InputOperation operation, TypeFactory typeFactory, string accessibility, string clientPrefix, LongRunningOperationInfo lroInfo, SourceInputModel? sourceInputModel) : base(Configuration.Namespace, sourceInputModel) + { + Debug.Assert(operation.LongRunning != null); + + FinalStateVia = operation.LongRunning.FinalStateVia; + + var finalResponse = operation.LongRunning.FinalResponse; + var returnType = operation.LongRunning.ReturnType; + + if (returnType != null) + { + ResultType = typeFactory.CreateType(returnType).OutputType; + ResultSerialization = SerializationBuilder.Build(finalResponse.BodyMediaType, returnType, ResultType, null); + + var paging = operation.Paging; + if (paging != null) + { + NextPageMethod = lroInfo.NextOperationMethod; + PagingResponse = new PagingResponseInfo(paging.NextLinkName, paging.ItemName, ResultType); + ResultType = new CSharpType(typeof(AsyncPageable<>), PagingResponse.ItemType); + } + } + + DefaultName = clientPrefix + operation.Name.ToCleanName() + "Operation"; + Description = BuilderHelpers.EscapeXmlDocDescription(operation.Description); + DefaultAccessibility = accessibility; + } + + public CSharpType? ResultType { get; } + public OperationFinalStateVia FinalStateVia { get; } + public Diagnostic Diagnostics => new Diagnostic(Declaration.Name); + public ObjectSerialization? ResultSerialization { get; } + public RestClientMethod? NextPageMethod { get; } + public PagingResponseInfo? PagingResponse { get; } + public string Description { get; } + protected override string DefaultName { get; } + protected override string DefaultAccessibility { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/LongRunningOperationInfo.cs b/logger/autorest.csharp/common/Output/Models/LongRunningOperationInfo.cs new file mode 100644 index 0000000..8cb25e5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/LongRunningOperationInfo.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; +using AutoRest.CSharp.Output.Models.Requests; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal class LongRunningOperationInfo + { + public LongRunningOperationInfo(string accessibility, string clientPrefix, RestClientMethod? nextOperationMethod) + { + Accessibility = accessibility; + ClientPrefix = clientPrefix; + NextOperationMethod = nextOperationMethod; + } + + public string Accessibility { get; } + + public string ClientPrefix { get; } + + public RestClientMethod? NextOperationMethod { get; } + + } +} diff --git a/logger/autorest.csharp/common/Output/Models/LongRunningOperationMethod.cs b/logger/autorest.csharp/common/Output/Models/LongRunningOperationMethod.cs new file mode 100644 index 0000000..d053980 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/LongRunningOperationMethod.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class LongRunningOperationMethod + { + public LongRunningOperationMethod(string name, LongRunningOperation operation, RestClientMethod startMethod, Diagnostic diagnostics) + { + Operation = operation; + StartMethod = startMethod; + Diagnostics = diagnostics; + Name = name; + } + + public string Name { get; } + public LongRunningOperation Operation { get; } + + public RestClientMethod StartMethod { get; } + + public Diagnostic Diagnostics { get; } + public string Accessibility => "public"; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/LongRunningResultRetrievalMethod.cs b/logger/autorest.csharp/common/Output/Models/LongRunningResultRetrievalMethod.cs new file mode 100644 index 0000000..58a02c9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/LongRunningResultRetrievalMethod.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Models +{ + /// + /// Method to fetch return value specified by `logicalPath` from raw response. + /// + /// Type of the return value specified by . + /// Name of the raw response type. + /// `logicalPath`. + internal record LongRunningResultRetrievalMethod(CSharpType ReturnType, string ResponseTypeName, string ResultPath) + { + public MethodSignature MethodSignature => new( + Name: $"Fetch{ReturnType.Name}From{ResponseTypeName}", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Private, + ReturnType: ReturnType, + ReturnDescription: null, + Parameters: new List() { KnownParameters.Response } + ); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Method.cs b/logger/autorest.csharp/common/Output/Models/Method.cs new file mode 100644 index 0000000..ad3961e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Method.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Diagnostics; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Models +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal record Method + { + public MethodSignatureBase Signature { get; } + public MethodBodyStatement? Body { get; } + public ValueExpression? BodyExpression { get; } + + public Method(MethodSignatureBase signature, MethodBodyStatement body) + { + Signature = signature; + Body = body; + } + + public Method(MethodSignatureBase signature, ValueExpression bodyExpression) + { + Signature = signature; + BodyExpression = bodyExpression; + } + + private string GetDebuggerDisplay() + { + using var writer = new DebuggerCodeWriter(); + writer.WriteMethod(this); + return writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/MethodSignature.cs b/logger/autorest.csharp/common/Output/Models/MethodSignature.cs new file mode 100644 index 0000000..9cf7674 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/MethodSignature.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; +using Azure; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Output.Models +{ + internal record MethodSignature(string Name, FormattableString? Summary, FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType? ReturnType, FormattableString? ReturnDescription, IReadOnlyList Parameters, IReadOnlyList? Attributes = null, IReadOnlyList? GenericArguments = null, IReadOnlyList? GenericParameterConstraints = null, CSharpType? ExplicitInterface = null, string? NonDocumentComment = null, bool IsRawSummaryText = false) + : MethodSignatureBase(Name, Summary, Description, NonDocumentComment, Modifiers, Parameters, Attributes ?? Array.Empty(), IsRawSummaryText: IsRawSummaryText) + { + public static IEqualityComparer ParameterAndReturnTypeEqualityComparer = new MethodSignatureParameterAndReturnTypeEqualityComparer(); + + public MethodSignature WithAsync(bool isAsync) => isAsync ? MakeAsync() : MakeSync(); + + public MethodSignature WithParametersRequired() + { + if (Parameters.All(p => p.DefaultValue is null)) + { + return this; + } + return this with { Parameters = Parameters.Select(p => p.ToRequired()).ToList() }; + } + + private MethodSignature MakeAsync() + { + if (Modifiers.HasFlag(Async) || ReturnType is { IsAsyncPageable: true }) + { + return this; + } + + if (ReturnType is { IsPageable: true }) + { + return this with + { + Name = Name + "Async", + ReturnType = new CSharpType(typeof(AsyncPageable<>), ReturnType.Arguments) + }; + } + + if (ReturnType is { IsIEnumerableOfT: true }) + { + return this with + { + Name = Name + "Async", + Modifiers = Modifiers | Async, + ReturnType = new CSharpType(typeof(IAsyncEnumerable<>), ReturnType.Arguments), + Parameters = Parameters.Append(KnownParameters.EnumeratorCancellationTokenParameter).ToArray() + }; + } + + return this with + { + Name = Name + "Async", + Modifiers = Modifiers | Async, + ReturnType = ReturnType != null + ? ReturnType.IsOperationOfPageable + ? new CSharpType(typeof(Task<>), new CSharpType(typeof(Operation<>), new CSharpType(typeof(AsyncPageable<>), ReturnType.Arguments[0].Arguments[0]))) + : new CSharpType(typeof(Task<>), ReturnType) + : typeof(Task) + }; + } + + private MethodSignature MakeSync() + { + if (!Modifiers.HasFlag(Async) && (ReturnType == null || !ReturnType.IsAsyncPageable)) + { + return this; + } + + if (ReturnType is { IsAsyncPageable: true }) + { + return this with + { + Name = Name[..^5], + ReturnType = new CSharpType(typeof(Pageable<>), ReturnType.Arguments) + }; + } + + if (ReturnType is { IsIAsyncEnumerableOfT: true }) + { + return this with + { + Name = Name[..^5], + Modifiers = Modifiers ^ Async, + ReturnType = new CSharpType(typeof(IEnumerable<>), ReturnType.Arguments), + Parameters = Parameters.Where(p => p != KnownParameters.EnumeratorCancellationTokenParameter).ToArray() + }; + } + + return this with + { + Name = Name[..^5], + Modifiers = Modifiers ^ Async, + ReturnType = ReturnType?.Arguments.Count == 1 + ? ReturnType.Arguments[0].IsOperationOfAsyncPageable + ? new CSharpType(typeof(Operation<>), new CSharpType(typeof(Pageable<>), ReturnType.Arguments[0].Arguments[0].Arguments[0])) + : ReturnType.Arguments[0] + : null + }; + } + + public FormattableString GetCRef() => $"{Name}({Parameters.GetTypesFormattable()})"; + + private class MethodSignatureParameterAndReturnTypeEqualityComparer : IEqualityComparer + { + public bool Equals(MethodSignature? x, MethodSignature? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + var result = x.Name == x.Name + && x.ReturnType == y.ReturnType + && x.Parameters.SequenceEqual(y.Parameters, Parameter.TypeAndNameEqualityComparer); + return result; + } + + public int GetHashCode([DisallowNull] MethodSignature obj) + { + return HashCode.Combine(obj.Name, obj.ReturnType); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/MethodSignatureBase.cs b/logger/autorest.csharp/common/Output/Models/MethodSignatureBase.cs new file mode 100644 index 0000000..6af2a23 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/MethodSignatureBase.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models +{ + internal abstract record MethodSignatureBase(string Name, FormattableString? Summary, FormattableString? Description, string? NonDocumentComment, MethodSignatureModifiers Modifiers, IReadOnlyList Parameters, IReadOnlyList Attributes, bool IsRawSummaryText = false) + { + public FormattableString? SummaryText => Summary.IsNullOrEmpty() ? Description : Summary; + public FormattableString? DescriptionText => Summary.IsNullOrEmpty() || Description == Summary || Description?.ToString() == Summary?.ToString() ? $"" : Description; + } + + [Flags] + internal enum MethodSignatureModifiers + { + None = 0, + Public = 1, + Internal = 2, + Protected = 4, + Private = 8, + Static = 16, + Extension = 32, + Virtual = 64, + Async = 128, + New = 256, + Override = 512 + } +} diff --git a/logger/autorest.csharp/common/Output/Models/OverloadMethodSignature.cs b/logger/autorest.csharp/common/Output/Models/OverloadMethodSignature.cs new file mode 100644 index 0000000..b72186e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/OverloadMethodSignature.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models +{ + internal record OverloadMethodSignature(MethodSignature MethodSignature, MethodSignature PreviousMethodSignature, IReadOnlyList MissingParameters, FormattableString? Description) + { + } +} diff --git a/logger/autorest.csharp/common/Output/Models/PropertyBody.cs b/logger/autorest.csharp/common/Output/Models/PropertyBody.cs new file mode 100644 index 0000000..ad50c9e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/PropertyBody.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Models +{ + internal abstract record PropertyBody; + + internal record AutoPropertyBody(bool HasSetter, MethodSignatureModifiers SetterModifiers = MethodSignatureModifiers.None, ConstantExpression? InitializationExpression = null) : PropertyBody; + + internal record MethodPropertyBody(MethodBodyStatement Getter, MethodBodyStatement? Setter = null, MethodSignatureModifiers SetterModifiers = MethodSignatureModifiers.None) : PropertyBody; + + internal record ExpressionPropertyBody(ValueExpression Getter) : PropertyBody; +} diff --git a/logger/autorest.csharp/common/Output/Models/PropertyDeclaration.cs b/logger/autorest.csharp/common/Output/Models/PropertyDeclaration.cs new file mode 100644 index 0000000..0ec5f56 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/PropertyDeclaration.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models; + +namespace AutoRest.CSharp.Common.Output.Models +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal record PropertyDeclaration(FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType PropertyType, CodeWriterDeclaration Declaration, PropertyBody PropertyBody, IReadOnlyDictionary? Exceptions = null, CSharpType? ExplicitInterface = null) + { + public PropertyDeclaration(FormattableString? description, MethodSignatureModifiers modifiers, CSharpType propertyType, string name, PropertyBody propertyBody, IReadOnlyDictionary? exceptions = null, CSharpType? explicitInterface = null) : this(description, modifiers, propertyType, new CodeWriterDeclaration(name), propertyBody, exceptions, explicitInterface) + { + Declaration.SetActualName(name); + } + + private string GetDebuggerDisplay() + { + using var writer = new DebuggerCodeWriter(); + writer.WriteProperty(this); + return writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/RequestConditionHeaders.cs b/logger/autorest.csharp/common/Output/Models/RequestConditionHeaders.cs new file mode 100644 index 0000000..26bd923 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/RequestConditionHeaders.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AutoRest.CSharp.Common.Output.Models +{ + [Flags] + internal enum RequestConditionHeaders + { + None = 0, + IfMatch = 1, + IfNoneMatch = 2, + IfModifiedSince = 4, + IfUnmodifiedSince = 8 + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/BinaryCollectionRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/BinaryCollectionRequestBody.cs new file mode 100644 index 0000000..64f7ad5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/BinaryCollectionRequestBody.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class BinaryCollectionRequestBody : RequestBody + { + public ReferenceOrConstant Value { get; } + + public BinaryCollectionRequestBody(ReferenceOrConstant value) + { + Value = value; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/BinaryRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/BinaryRequestBody.cs new file mode 100644 index 0000000..2d74472 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/BinaryRequestBody.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class BinaryRequestBody : RequestBody + { + public ReferenceOrConstant Value { get; } + + public BinaryRequestBody(ReferenceOrConstant value) + { + Value = value; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/ConstructedSchemaRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/ConstructedSchemaRequestBody.cs new file mode 100644 index 0000000..6b64c2e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/ConstructedSchemaRequestBody.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class ConstructedSchemaRequestBody : RequestBody + { + public ReferenceOrConstant[] Parameters { get; } + public ObjectSerialization Serialization { get; } + + public ConstructedSchemaRequestBody(ReferenceOrConstant[] parameters, ObjectSerialization serialization) + { + Parameters = parameters; + Serialization = serialization; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/Diagnostic.cs b/logger/autorest.csharp/common/Output/Models/Requests/Diagnostic.cs new file mode 100644 index 0000000..5596ad2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/Diagnostic.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class Diagnostic + { + public Diagnostic(string scopeName, DiagnosticAttribute[]? attributes = null) + { + ScopeName = scopeName; + Attributes = attributes ?? Array.Empty(); + } + + public string ScopeName { get; } + public DiagnosticAttribute[] Attributes { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/DiagnosticAttribute.cs b/logger/autorest.csharp/common/Output/Models/Requests/DiagnosticAttribute.cs new file mode 100644 index 0000000..d1d668f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/DiagnosticAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class DiagnosticAttribute + { + public DiagnosticAttribute(string name, ReferenceOrConstant value) + { + Name = name; + Value = value; + } + + public string Name { get; } + public ReferenceOrConstant Value { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/FlattenedSchemaRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/FlattenedSchemaRequestBody.cs new file mode 100644 index 0000000..7e20095 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/FlattenedSchemaRequestBody.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class FlattenedSchemaRequestBody : RequestBody + { + public ObjectType ObjectType { get; } + public ObjectPropertyInitializer[] Initializers { get; } + public ObjectSerialization Serialization { get; } + + public FlattenedSchemaRequestBody(ObjectType objectType, ObjectPropertyInitializer[] initializers, ObjectSerialization serialization) + { + ObjectType = objectType; + Initializers = initializers; + Serialization = serialization; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/MultipartRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/MultipartRequestBody.cs new file mode 100644 index 0000000..d1f2118 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/MultipartRequestBody.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Reflection.Metadata; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class MultipartRequestBody : RequestBody + { + public MultipartRequestBodyPart[] RequestBodyParts { get; } + + public MultipartRequestBody(MultipartRequestBodyPart[] requestBodyParts) + { + RequestBodyParts = requestBodyParts; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/MultipartRequestBodyPart.cs b/logger/autorest.csharp/common/Output/Models/Requests/MultipartRequestBodyPart.cs new file mode 100644 index 0000000..2529b33 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/MultipartRequestBodyPart.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class MultipartRequestBodyPart + { + public string Name { get; } + public RequestBody Content { get; } + + public MultipartRequestBodyPart(string name, RequestBody content) + { + Name = name; + Content = content; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/PagingMethod.cs b/logger/autorest.csharp/common/Output/Models/Requests/PagingMethod.cs new file mode 100644 index 0000000..02c38f9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/PagingMethod.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Collections.Generic; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class PagingMethod + { + public PagingMethod(RestClientMethod method, RestClientMethod? nextPageMethod, string name, Diagnostic diagnostics, PagingResponseInfo pagingResponse) + { + Method = method; + NextPageMethod = nextPageMethod; + Name = name; + Diagnostics = diagnostics; + PagingResponse = pagingResponse; + } + + public string Name { get; } + public RestClientMethod Method { get; } + public RestClientMethod? NextPageMethod { get; } + public PagingResponseInfo PagingResponse { get; } + public Diagnostic Diagnostics { get; } + public string Accessibility => "public"; + + /// + /// Check whether the given parameter name is like page size + /// + /// Parameter name to check + /// + public static bool IsPageSizeName(string name) + { + var n = name.ToLower(); + return (n.EndsWith("pagesize") || n.EndsWith("page_size")); + } + + public static bool IsPageSizeType(Type type) => Type.GetTypeCode(type) switch + { + TypeCode.Single => true, + TypeCode.Double => true, + TypeCode.Decimal => true, + TypeCode.Int64 => true, + TypeCode.Int32 => true, + _ => false + }; + + /// + /// Check whether the given parameter is a page size parameter + /// + /// Parameter to check + /// true if the given parameter is a page size parameter; otherwise false + public static bool IsPageSizeParameter(Parameter p) + { + return IsPageSizeName(p.Name) && IsPageSizeType(p.Type.FrameworkType); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/PagingResponseInfo.cs b/logger/autorest.csharp/common/Output/Models/Requests/PagingResponseInfo.cs new file mode 100644 index 0000000..fb69e9b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/PagingResponseInfo.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; +using Azure; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class PagingResponseInfo + { + public PagingResponseInfo(string? nextLinkName, string? itemName, CSharpType type) + { + ResponseType = type; + NextLinkPropertyName = nextLinkName; + ItemPropertyName = itemName ?? "value"; + + ObjectTypeProperty itemProperty = GetPropertyBySerializedName(type, ItemPropertyName); + if (!itemProperty.Declaration.Type.IsList) + { + throw new InvalidOperationException($"'{itemName}' property must be be an array schema instead of '{itemProperty.InputModelProperty?.Type}'"); + } + ItemType = itemProperty.Declaration.Type.ElementType; + } + + public CSharpType ResponseType { get; } + public string? NextLinkPropertyName { get; } + public string ItemPropertyName { get; } + public CSharpType PageType => new CSharpType(typeof(Page<>), ItemType); + public CSharpType ItemType { get; } + + private ObjectTypeProperty GetPropertyBySerializedName(CSharpType type, string name) + { + TypeProvider implementation = type.Implementation; + + if (implementation is SchemaObjectType objectType) + { + return objectType.GetPropertyBySerializedName(name, true); + } + + if (implementation is ModelTypeProvider modelType) + { + return modelType.GetPropertyBySerializedName(name, true); + } + + throw new InvalidOperationException($"The type '{type}' has to be an object schema to be used in paging"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/PathSegment.cs b/logger/autorest.csharp/common/Output/Models/Requests/PathSegment.cs new file mode 100644 index 0000000..771f0b1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/PathSegment.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class PathSegment + { + public PathSegment(ReferenceOrConstant value, bool escape, SerializationFormat format, bool isRaw = false) + { + Value = value; + Escape = escape; + Format = format; + IsRaw = isRaw; + } + + public ReferenceOrConstant Value { get; } + public bool Escape { get; } + public SerializationFormat Format { get; } + public bool IsRaw { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/QueryParameter.cs b/logger/autorest.csharp/common/Output/Models/Requests/QueryParameter.cs new file mode 100644 index 0000000..d428da2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/QueryParameter.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal record QueryParameter(string Name, ReferenceOrConstant Value, string? Delimiter, bool Escape, SerializationFormat SerializationFormat, bool Explode, bool IsApiVersion); +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/Reference.cs b/logger/autorest.csharp/common/Output/Models/Requests/Reference.cs new file mode 100644 index 0000000..51291c4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/Reference.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal readonly struct Reference + { + public Reference(string name, CSharpType type) + { + Name = name; + Type = type; + } + + public string Name { get; } + public CSharpType Type { get; } + + public static implicit operator Reference(Parameter parameter) => new Reference(parameter.Name, parameter.Type); + public static implicit operator Reference(FieldDeclaration field) => new Reference(field.Name, field.Type); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/ReferenceOrConstant.cs b/logger/autorest.csharp/common/Output/Models/Requests/ReferenceOrConstant.cs new file mode 100644 index 0000000..6e5c744 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/ReferenceOrConstant.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; + + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal readonly struct ReferenceOrConstant + { + private readonly Constant? _constant; + private readonly Reference? _reference; + + private ReferenceOrConstant(Constant constant) + { + Type = constant.Type; + _constant = constant; + _reference = null; + } + + private ReferenceOrConstant(Reference reference) + { + Type = reference.Type; + _reference = reference; + _constant = null; + } + + public CSharpType Type { get; } + public bool IsConstant => _constant.HasValue; + + public Constant Constant => _constant ?? throw new InvalidOperationException("Not a constant"); + public Reference Reference => _reference ?? throw new InvalidOperationException("Not a reference"); + + public static implicit operator ReferenceOrConstant(Constant constant) => new ReferenceOrConstant(constant); + public static implicit operator ReferenceOrConstant(Reference reference) => new ReferenceOrConstant(reference); + public static implicit operator ReferenceOrConstant(Parameter parameter) => new ReferenceOrConstant(new Reference(parameter.Name, parameter.Type)); + public static implicit operator ReferenceOrConstant(FieldDeclaration field) => new ReferenceOrConstant(new Reference(field.Name, field.Type)); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/Request.cs b/logger/autorest.csharp/common/Output/Models/Requests/Request.cs new file mode 100644 index 0000000..54ba6d4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/Request.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using System.Collections.Generic; +using Azure.Core; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class Request + { + public Request(RequestMethod httpMethod, PathSegment[] pathSegments, QueryParameter[] query, RequestHeader[] headers, RequestBody? body) + { + HttpMethod = httpMethod; + PathSegments = pathSegments; + Query = query; + Headers = headers; + Body = body; + } + + public RequestMethod HttpMethod { get; } + public PathSegment[] PathSegments { get; } + public QueryParameter[] Query { get; } + public RequestHeader[] Headers { get; } + public RequestBody? Body { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/RequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/RequestBody.cs new file mode 100644 index 0000000..d9b576b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/RequestBody.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class RequestBody + { + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/RequestContentRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/RequestContentRequestBody.cs new file mode 100644 index 0000000..d9034c4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/RequestContentRequestBody.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; + + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class RequestContentRequestBody : RequestBody + { + public Parameter Parameter { get; } + + public RequestContentRequestBody(Parameter parameter) + { + Parameter = parameter; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/RequestHeader.cs b/logger/autorest.csharp/common/Output/Models/Requests/RequestHeader.cs new file mode 100644 index 0000000..c3d51f8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/RequestHeader.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class RequestHeader + { + private static HashSet ContentHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Allow", + "Content-Disposition", + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-MD5", + "Content-Range", + "Content-Type", + "Expires", + "Last-Modified", + }; + + public const string RepeatabilityRequestId = "Repeatability-Request-ID"; + public const string RepeatabilityFirstSent = "Repeatability-First-Sent"; + + public static HashSet RepeatabilityRequestHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + RepeatabilityRequestId, + RepeatabilityFirstSent, + }; + public static bool IsRepeatabilityRequestHeader(string headerName) => RepeatabilityRequestHeaders.Contains(headerName); + + public static HashSet ClientRequestIdHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "client-request-id", + }; + + public static HashSet ReturnClientRequestIdResponseHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "return-client-request-id", + }; + + public static bool IsClientRequestIdHeader(string headerName) => ClientRequestIdHeaders.Contains(headerName) || ReturnClientRequestIdResponseHeaders.Contains(headerName); + + public string Name { get; } + public ReferenceOrConstant Value { get; } + public string? Delimiter { get; } + public SerializationFormat Format { get; } + public bool IsContentHeader { get; } + + public RequestHeader(string name, ReferenceOrConstant value, string? delimiter, SerializationFormat format = SerializationFormat.Default) + { + Name = name; + Value = value; + Format = format; + Delimiter = delimiter; + IsContentHeader = ContentHeaders.Contains(name); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/RestClientMethod.cs b/logger/autorest.csharp/common/Output/Models/Requests/RestClientMethod.cs new file mode 100644 index 0000000..61ac6d5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/RestClientMethod.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Responses; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Output.Models; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class RestClientMethod + { + public RestClientMethod(string name, string? summary, string? description, CSharpType? returnType, Request request, IReadOnlyList parameters, Response[] responses, DataPlaneResponseHeaderGroupType? headerModel, bool bufferResponse, string accessibility, InputOperation operation) + { + Name = name; + Request = request; + Parameters = parameters; + Responses = responses; + Summary = summary; + Description = description; + ReturnType = returnType; + HeaderModel = headerModel; + BufferResponse = bufferResponse; + Accessibility = GetAccessibility(accessibility); + Operation = operation; + + var statusCodes = Responses + .SelectMany(r => r.StatusCodes) + .Distinct() + .OrderBy(c => c.Code ?? c.Family * 100); + ResponseClassifierType = new ResponseClassifierType(statusCodes); + + PropertyBag = null; + // By default, we enable property bag feature in management plane and the real behavior will be determined later. + if (Configuration.AzureArm) + { + // At this point we can't finalize the name for the property bag model + // So we pass in the empty string here + PropertyBag = new MgmtPropertyBag(string.Empty, operation); + } + } + + private static MethodSignatureModifiers GetAccessibility(string accessibility) => + accessibility switch + { + "public" => MethodSignatureModifiers.Public, + "internal" => MethodSignatureModifiers.Internal, + "protected" => MethodSignatureModifiers.Protected, + "private" => MethodSignatureModifiers.Private, + _ => throw new NotSupportedException() + }; + + public string Name { get; } + public string? Summary { get; } + public string? Description { get; } + public string? SummaryText => Summary.IsNullOrEmpty() ? Description : Summary; + public string? DescriptionText => Summary.IsNullOrEmpty() || Description == Summary ? string.Empty : Description; + public Request Request { get; } + public IReadOnlyList Parameters { get; } + public Response[] Responses { get; } + public DataPlaneResponseHeaderGroupType? HeaderModel { get; } + public bool BufferResponse { get; } + public CSharpType? ReturnType { get; } + public MethodSignatureModifiers Accessibility { get; } + public InputOperation Operation { get; } + + public ResponseClassifierType ResponseClassifierType { get; } + + public PropertyBag? PropertyBag { get; } + + public bool ShouldEnableRedirect => Responses.Any(r => r.StatusCodes.Any(r => IsRedirectResponseCode(r.Code))); + + private bool IsRedirectResponseCode(int? code) => code switch + { + 300 or 301 or 302 or 303 or 307 or 308 => true, + _ => false, + }; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/SchemaRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/SchemaRequestBody.cs new file mode 100644 index 0000000..ba0e318 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/SchemaRequestBody.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class SchemaRequestBody : RequestBody + { + public ObjectSerialization Serialization { get; } + public ReferenceOrConstant Value { get; } + + public SchemaRequestBody(ReferenceOrConstant value, ObjectSerialization serialization) + { + Value = value; + Serialization = serialization; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/TextRequestBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/TextRequestBody.cs new file mode 100644 index 0000000..e5f8246 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/TextRequestBody.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class TextRequestBody : RequestBody + { + public ReferenceOrConstant Value { get; } + + public TextRequestBody(ReferenceOrConstant value) + { + Value = value; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Requests/UrlEncodedBody.cs b/logger/autorest.csharp/common/Output/Models/Requests/UrlEncodedBody.cs new file mode 100644 index 0000000..40ca405 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Requests/UrlEncodedBody.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Output.Models.Requests +{ + internal class UrlEncodedBody : RequestBody + { + public struct NamedReferenceOrConstant + { + public string Name { get; } + public ReferenceOrConstant Value { get; } + + public NamedReferenceOrConstant (string name, ReferenceOrConstant value) + { + Name = name; + Value = value; + } + + public void Deconstruct (out string name, out ReferenceOrConstant value) + { + name = Name; + value = Value; + } + } + + public List Values { get; set; }= new List(); + + public void Add (string parameter, ReferenceOrConstant value) + { + Values.Add(new NamedReferenceOrConstant(parameter, value)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/ConstantResponseBody.cs b/logger/autorest.csharp/common/Output/Models/Responses/ConstantResponseBody.cs new file mode 100644 index 0000000..13a4e5b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/ConstantResponseBody.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.IO; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class ConstantResponseBody : ResponseBody + { + public ConstantResponseBody(Constant value) + { + Value = value; + } + + public override CSharpType Type { get; } = typeof(Stream); + + public Constant Value { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/ObjectResponseBody.cs b/logger/autorest.csharp/common/Output/Models/Responses/ObjectResponseBody.cs new file mode 100644 index 0000000..2156ebe --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/ObjectResponseBody.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; + + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class ObjectResponseBody: ResponseBody + { + public ObjectResponseBody(CSharpType type, ObjectSerialization serialization) + { + Serialization = serialization; + Type = type; + } + + public ObjectSerialization Serialization { get; } + public override CSharpType Type { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/Response.cs b/logger/autorest.csharp/common/Output/Models/Responses/Response.cs new file mode 100644 index 0000000..8fd2155 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/Response.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class Response + { + public Response(ResponseBody? responseBody, StatusCodes[] statusCodes) + { + ResponseBody = responseBody; + StatusCodes = statusCodes; + } + + public ResponseBody? ResponseBody { get; } + public StatusCodes[] StatusCodes { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/ResponseBody.cs b/logger/autorest.csharp/common/Output/Models/Responses/ResponseBody.cs new file mode 100644 index 0000000..6608a9c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/ResponseBody.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal abstract class ResponseBody + { + public abstract CSharpType Type { get; } + + protected bool Equals(ResponseBody other) + { + return Type.Equals(other.Type); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ResponseBody) obj); + } + + public override int GetHashCode() + { + return Type.GetHashCode(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/ResponseClassifierType.cs b/logger/autorest.csharp/common/Output/Models/Responses/ResponseClassifierType.cs new file mode 100644 index 0000000..b86db53 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/ResponseClassifierType.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Output.Models.Responses; + +namespace AutoRest.CSharp.Common.Output.Models.Responses +{ + internal record ResponseClassifierType(string Name, StatusCodes[] StatusCodes) + { + public ResponseClassifierType(IOrderedEnumerable statusCodes) : this(ComposeName(statusCodes), statusCodes.ToArray()) + { + } + + public virtual bool Equals(ResponseClassifierType? other) => (other == null ? false : Name == other.Name); + + public override int GetHashCode() => Name.GetHashCode(); + + private static string ComposeName(IOrderedEnumerable statusCodes) => Configuration.ApiTypes.ResponseClassifierType.Name + string.Join("", statusCodes.Select(c => c.Code?.ToString() ?? $"{c.Family * 100}To{(c.Family + 1) * 100}")); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/ResponseHeader.cs b/logger/autorest.csharp/common/Output/Models/Responses/ResponseHeader.cs new file mode 100644 index 0000000..a3b4e08 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/ResponseHeader.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class ResponseHeader + { + public ResponseHeader(string name, string serializedName, CSharpType type, string description) + { + Name = name; + SerializedName = serializedName; + Type = type; + Description = description; + } + + public string Description { get; } + public string Name { get; } + public string SerializedName { get; } + public CSharpType Type { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/StatusCodes.cs b/logger/autorest.csharp/common/Output/Models/Responses/StatusCodes.cs new file mode 100644 index 0000000..37a7e00 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/StatusCodes.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal record StatusCodes(int? Code, int? Family); +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/StreamResponseBody.cs b/logger/autorest.csharp/common/Output/Models/Responses/StreamResponseBody.cs new file mode 100644 index 0000000..96ac0c7 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/StreamResponseBody.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.IO; +using AutoRest.CSharp.Generation.Types; + + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class StreamResponseBody : ResponseBody + { + public override CSharpType Type { get; } = typeof(Stream); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Responses/StringResponseBody.cs b/logger/autorest.csharp/common/Output/Models/Responses/StringResponseBody.cs new file mode 100644 index 0000000..68fd481 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Responses/StringResponseBody.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class StringResponseBody: ResponseBody + { + public override CSharpType Type { get; } = typeof(string); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/RestClient.cs b/logger/autorest.csharp/common/Output/Models/RestClient.cs new file mode 100644 index 0000000..08aea12 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/RestClient.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models +{ + internal abstract class RestClient : TypeProvider + { + private readonly Lazy> _requestMethods; + private readonly Lazy> _nextPageRequestMethods; + private RestClientMethod[]? _allMethods; + private ConstructorSignature? _constructor; + + internal InputClient InputClient { get; } + public IReadOnlyList Parameters { get; } + public RestClientMethod[] Methods => _allMethods ??= BuildAllMethods().ToArray(); + public ConstructorSignature Constructor => _constructor ??= new ConstructorSignature(Type, $"Initializes a new instance of {Declaration.Name}", null, MethodSignatureModifiers.Public, Parameters.ToArray()); + + protected override string DefaultName { get; } + protected override string DefaultAccessibility => "internal"; + + protected RestClient(InputClient inputClient, string restClientName, IReadOnlyList parameters, SourceInputModel? sourceInputModel) : base(Configuration.Namespace, sourceInputModel) + { + InputClient = inputClient; + + _requestMethods = new Lazy>(EnsureNormalMethods); + _nextPageRequestMethods = new Lazy>(EnsureGetNextPageMethods); + + Parameters = parameters; + DefaultName = restClientName; + } + + private IEnumerable BuildAllMethods() + { + foreach (var operation in InputClient.Operations) + { + yield return GetOperationMethod(operation); + } + + foreach (var operation in InputClient.Operations) + { + // remove duplicates when GetNextPage method is not autogenerated + if (GetNextOperationMethod(operation) is { } nextOperationMethod && operation.Paging is {NextLinkOperation: null, SelfNextLink: false}) + { + yield return nextOperationMethod; + } + } + } + + protected abstract Dictionary EnsureNormalMethods(); + + protected Dictionary EnsureGetNextPageMethods() + { + var nextPageMethods = new Dictionary(); + foreach (var operation in InputClient.Operations) + { + var paging = operation.Paging; + if (paging == null) + { + continue; + } + + RestClientMethod? nextMethod = null; + if (paging.NextLinkOperation != null) + { + nextMethod = GetOperationMethod(paging.NextLinkOperation); + } + else if (paging.NextLinkName != null) + { + var method = GetOperationMethod(operation); + nextMethod = RestClientBuilder.BuildNextPageMethod(method); + } + + if (nextMethod != null) + { + nextPageMethods.Add(operation, nextMethod); + } + } + + return nextPageMethods; + } + + public RestClientMethod? GetNextOperationMethod(InputOperation request) + { + RestClientMethod? value; + if (request.Paging is { SelfNextLink: true }) + { + _requestMethods.Value.TryGetValue(request, out value); + } + else + { + _nextPageRequestMethods.Value.TryGetValue(request, out value); + } + + return value; + } + + public RestClientMethod GetOperationMethod(InputOperation request) + => _requestMethods.Value[request]; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/RestClientBuilder.cs b/logger/autorest.csharp/common/Output/Models/RestClientBuilder.cs new file mode 100644 index 0000000..1509570 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/RestClientBuilder.cs @@ -0,0 +1,590 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Request = AutoRest.CSharp.Output.Models.Requests.Request; +using Response = AutoRest.CSharp.Output.Models.Responses.Response; +using StatusCodes = AutoRest.CSharp.Output.Models.Responses.StatusCodes; + +namespace AutoRest.CSharp.Output.Models +{ + internal class RestClientBuilder + { + private static readonly HashSet IgnoredRequestHeader = new(StringComparer.OrdinalIgnoreCase) + { + "x-ms-client-request-id", + "tracestate", + "traceparent" + }; + + private readonly OutputLibrary? _library; + private readonly TypeFactory _typeFactory; + private readonly Dictionary _parameters; + private IEnumerable ClientParameters { get; } + + public RestClientBuilder(IEnumerable clientParameters, IEnumerable operations, TypeFactory typeFactory) + { + _typeFactory = typeFactory; + var allClientParameters = clientParameters.Concat(operations.SelectMany(op => op.Parameters).Where(p => p.Kind == InputOperationParameterKind.Client)).DistinctBy(p => p.Name); + _parameters = allClientParameters.DistinctBy(p => p.Name).ToDictionary(p => p.Name, BuildConstructorParameter); + ClientParameters = clientParameters; + } + + public RestClientBuilder(IEnumerable clientParameters, IEnumerable operations, TypeFactory typeFactory, OutputLibrary library) + { + _typeFactory = typeFactory; + _library = library; + var allClientParameters = clientParameters.Concat(operations.SelectMany(op => op.Parameters).Where(p => p.Kind == InputOperationParameterKind.Client)).DistinctBy(p => p.Name); + _parameters = allClientParameters.ToDictionary(p => p.Name, BuildConstructorParameter); + ClientParameters = clientParameters; + } + + /// + /// Get sorted parameters, required parameters are at the beginning. + /// + /// + public Parameter[] GetOrderedParametersByRequired() + { + return OrderParametersByRequired(_parameters.Values); + } + + public static IEnumerable GetParametersFromClient(InputClient client) + { + return client.Parameters + .Concat(client.Operations.SelectMany(op => op.Parameters).Where(p => p.Kind == InputOperationParameterKind.Client)) + .DistinctBy(p => p.Name); + } + + public static RestClientMethod BuildRequestMethod(InputOperation operation, Parameter[] parameters, IReadOnlyCollection requestParts, Parameter? bodyParameter, TypeFactory typeFactory) + { + Request request = BuildRequest(operation, requestParts, bodyParameter, typeFactory); + Response[] responses = BuildResponses(operation, typeFactory, out var responseType); + + return new RestClientMethod( + operation.CleanName, + operation.Summary != null ? BuilderHelpers.EscapeXmlDocDescription(operation.Summary) : null, + BuilderHelpers.EscapeXmlDocDescription(operation.Description), + responseType, + request, + parameters, + responses, + null, + operation.BufferResponse, + accessibility: operation.Accessibility ?? "public", + operation + ); + } + + /// + /// Build RestClientMethod for HLC + /// + /// + /// + /// + public RestClientMethod BuildMethod(InputOperation operation, DataPlaneResponseHeaderGroupType? responseHeaderModel) + { + var allParameters = GetOperationAllParameters(operation); + var methodParameters = BuildMethodParameters(allParameters); + var requestParts = allParameters.Select(kvp => new RequestPartSource(kvp.Key.NameInRequest, (InputParameter?)kvp.Key, CreateReference(kvp.Key, kvp.Value), SerializationBuilder.GetSerializationFormat(kvp.Key.Type))) + .Concat(ClientParameters.Select(p => new RequestPartSource(p.NameInRequest, p, CreateReference(p, _parameters[p.Name]), SerializationBuilder.GetSerializationFormat(p.Type)))) + .DistinctBy(r => r.NameInRequest) + .ToList(); + + var request = BuildRequest(operation, requestParts, null, _typeFactory, _library); + Response[] responses = BuildResponses(operation, _typeFactory, out var responseType); + + return new RestClientMethod( + operation.CleanName, + operation.Summary != null ? BuilderHelpers.EscapeXmlDocDescription(operation.Summary) : null, + BuilderHelpers.EscapeXmlDocDescription(operation.Description), + responseType, + request, + methodParameters, + responses, + responseHeaderModel, + operation.BufferResponse, + accessibility: operation.Accessibility ?? "public", + operation + ); + } + + private Dictionary GetOperationAllParameters(InputOperation operation) + => FilterOperationAllParameters(operation.Parameters) + .ToDictionary(p => p, parameter => BuildParameter(parameter)); + + public static IEnumerable FilterOperationAllParameters(IReadOnlyList parameters) + => parameters + .Where(rp => !IsIgnoredHeaderParameter(rp)) + // change the type to constant so that it won't show up in the method signature + .Select(p => RequestHeader.IsRepeatabilityRequestHeader(p.NameInRequest) || RequestHeader.IsClientRequestIdHeader(p.NameInRequest) ? p with { Kind = InputOperationParameterKind.Constant } : p); + + public static Response[] BuildResponses(InputOperation operation, TypeFactory typeFactory, out CSharpType? responseType) + { + if (operation.HttpMethod == RequestMethod.Head && Configuration.HeadAsBoolean) + { + responseType = new CSharpType(typeof(bool)); + return new[] + { + new Response( + new ConstantResponseBody(new Constant(true, responseType)), + new[] {new StatusCodes(null, 2)}), + new Response( + new ConstantResponseBody(new Constant(false, responseType)), + new[] {new StatusCodes(null, 4)}), + }; + } + + List clientResponse = new List(); + foreach (var response in operation.Responses.Where(r => !r.IsErrorResponse)) + { + List statusCodes = new List(); + foreach (var statusCode in response.StatusCodes) + { + statusCodes.Add(new StatusCodes(statusCode, null)); + } + + var responseBody = operation.LongRunning != null ? null : BuildResponseBody(response, typeFactory); + clientResponse.Add(new Response(responseBody, statusCodes.ToArray())); + } + + responseType = ReduceResponses(clientResponse); + return clientResponse.ToArray(); + } + + private static Request BuildRequest(InputOperation operation, IReadOnlyCollection requestParts, Parameter? bodyParameter, TypeFactory typeFactory, OutputLibrary? library = null) + { + var uriParametersMap = new Dictionary(); + var pathParametersMap = new Dictionary(); + var queryParameters = new List(); + var headerParameters = new List(); + foreach (var (parameterName, operationParameter, reference, serializationFormat) in requestParts) + { + if (operationParameter == null) + { + Debug.Assert(parameterName == KnownParameters.MatchConditionsParameter.Name || parameterName == KnownParameters.RequestConditionsParameter.Name); + headerParameters.Add(new RequestHeader(parameterName, reference, null, serializationFormat)); + continue; + } + + var escape = !operationParameter.SkipUrlEncoding; + + switch (operationParameter.Location) + { + case RequestLocation.Uri: + uriParametersMap.Add(parameterName, new PathSegment(reference, escape, serializationFormat, isRaw: true)); + break; + case RequestLocation.Path: + pathParametersMap.Add(parameterName, new PathSegment(reference, escape, serializationFormat, isRaw: false)); + break; + case RequestLocation.Query: + queryParameters.Add(new QueryParameter(parameterName, reference, operationParameter.ArraySerializationDelimiter, escape, serializationFormat, operationParameter.Explode, operationParameter.IsApiVersion)); + break; + case RequestLocation.Header: + var headerName = operationParameter.HeaderCollectionPrefix ?? parameterName; + headerParameters.Add(new RequestHeader(headerName, reference, operationParameter.ArraySerializationDelimiter, serializationFormat)); + break; + } + } + + var uriParameters = GetPathSegments(operation.Uri, uriParametersMap, isRaw: true); + var pathParameters = GetPathSegments(operation.Path, pathParametersMap, isRaw: false); + + var body = bodyParameter != null + ? new RequestContentRequestBody(bodyParameter) + : operation.RequestBodyMediaType != BodyMediaType.None + ? BuildRequestBody(requestParts, operation.RequestBodyMediaType, library, typeFactory) + : null; + + return new Request( + operation.HttpMethod, + uriParameters.Concat(pathParameters).ToArray(), + queryParameters.ToArray(), + headerParameters.ToArray(), + body + ); + } + + protected virtual Parameter[] BuildMethodParameters(IReadOnlyDictionary allParameters) + { + List methodParameters = new(); + foreach (var (operationParameter, parameter) in allParameters) + { + // Grouped and flattened parameters shouldn't be added to methods + if (operationParameter.Kind == InputOperationParameterKind.Method) + { + methodParameters.Add(parameter); + } + } + + return OrderParametersByRequired(methodParameters); + } + + private static RequestBody? BuildRequestBody(IReadOnlyCollection allParameters, BodyMediaType bodyMediaType, OutputLibrary? library, TypeFactory typeFactory) + { + RequestBody? body = null; + + var references = new Dictionary(); + var bodyParameters = new List<(InputParameter, ReferenceOrConstant)>(); + foreach (var (_, inputParameter, value, _) in allParameters) + { + if (inputParameter is not null) + { + references[inputParameter.NameInRequest] = value; + } + + if (inputParameter is { Location: RequestLocation.Body }) + { + bodyParameters.Add((inputParameter, value)); + } + } + + if (bodyParameters.Count > 0) + { + if (bodyMediaType == BodyMediaType.Multipart) + { + List value = new List(); + foreach (var (_, reference) in bodyParameters) + { + var type = reference.Type; + RequestBody requestBody; + + if (type.Equals(typeof(string))) + { + requestBody = new TextRequestBody(reference); + } + else if (type.IsFrameworkType && type.FrameworkType == typeof(Stream)) + { + requestBody = new BinaryRequestBody(reference); + } + else if (type.IsList) + { + requestBody = new BinaryCollectionRequestBody(reference); + } + else + { + throw new NotImplementedException(); + } + + value.Add(new MultipartRequestBodyPart(reference.Reference.Name, requestBody)); + } + + body = new MultipartRequestBody(value.ToArray()); + } + else if (bodyMediaType == BodyMediaType.Form) + { + UrlEncodedBody urlbody = new UrlEncodedBody(); + foreach (var (inputParameter, reference) in bodyParameters) + { + urlbody.Add(inputParameter.NameInRequest, reference); + } + + body = urlbody; + } + else + { + Debug.Assert(bodyParameters.Count == 1); + var (bodyRequestParameter, bodyParameterValue) = bodyParameters[0]; + if (bodyMediaType == BodyMediaType.Binary || + // WORKAROUND: https://github.com/Azure/autorest.modelerfour/issues/360 + bodyRequestParameter.Type is InputPrimitiveType { Kind: InputPrimitiveTypeKind.Stream }) + { + body = new BinaryRequestBody(bodyParameterValue); + } + else if (bodyMediaType == BodyMediaType.Text) + { + body = new TextRequestBody(bodyParameterValue); + } + else + { + var serialization = SerializationBuilder.Build( + bodyMediaType, + bodyRequestParameter.Type, + bodyParameterValue.Type, + null); + + // This method has a flattened body + if (bodyRequestParameter.Kind == InputOperationParameterKind.Flattened && library != null) + { + (InputType inputType, bool isNullable) = bodyRequestParameter.Type is InputNullableType nullableType ? (nullableType.Type, true) : (bodyRequestParameter.Type, false); + var objectType = inputType switch + { + InputModelType inputModelType => typeFactory.CreateType(inputModelType).Implementation as SerializableObjectType, + _ => null + }; + + if (objectType == null) + { + throw new InvalidOperationException("Unexpected flattened type"); + } + + var properties = objectType.EnumerateHierarchy().SelectMany(o => o.Properties).ToList(); + var initializationMap = new List(); + foreach ((_, InputParameter? inputParameter, _, _) in allParameters) + { + if (inputParameter is { FlattenedBodyProperty: { } flattenedProperty }) + { + var property = properties.First(p => p.InputModelProperty?.SerializedName == flattenedProperty.SerializedName); + initializationMap.Add(new ObjectPropertyInitializer(property, references[inputParameter.NameInRequest].Reference)); + } + } + + body = new FlattenedSchemaRequestBody(objectType, initializationMap.ToArray(), serialization); + } + else + { + body = new SchemaRequestBody(bodyParameterValue, serialization); + } + } + } + } + + return body; + } + + private ReferenceOrConstant CreateReference(InputParameter operationParameter, Parameter parameter) + { + if (operationParameter.Kind == InputOperationParameterKind.Client) + { + return (ReferenceOrConstant)_parameters[operationParameter.Name]; + } + + if (operationParameter is { Kind: InputOperationParameterKind.Constant } && parameter.DefaultValue is not null) + { + return (ReferenceOrConstant)parameter.DefaultValue; + } + + var groupedByParameter = operationParameter.GroupedBy; + if (groupedByParameter == null) + { + return parameter; + } + + var groupedByParameterType = _typeFactory.CreateType(groupedByParameter.Type); + var (propertyName, propertyType) = groupedByParameterType.Implementation switch + { + ModelTypeProvider modelType when modelType.Fields.GetFieldByParameterName(parameter.Name) is { } field => (field.Name, field.Type), + SchemaObjectType schemaObjectType when schemaObjectType.GetPropertyForGroupedParameter(operationParameter.Name).Declaration is { } declaration => (declaration.Name, declaration.Type), + _ => throw new InvalidOperationException($"Unable to find object property for grouped parameter {parameter.Name} in {groupedByParameterType.Name}") + }; + + // need to handle case like renaming by [CodeGenModel] + var groupedByParameterName = Parameter.FromInputParameter(groupedByParameter, groupedByParameterType, _typeFactory).Name; + + return new Reference($"{groupedByParameterName.ToVariableName()}.{propertyName}", propertyType); + } + + private static ResponseBody? BuildResponseBody(OperationResponse response, TypeFactory typeFactory) + { + var bodyType = response.BodyType; + if (bodyType == null) + { + return null; + } + + if (response.BodyMediaType == BodyMediaType.Text) + { + return new StringResponseBody(); + } + + if (bodyType is InputPrimitiveType { Kind: InputPrimitiveTypeKind.Stream }) + { + return new StreamResponseBody(); + } + + CSharpType responseType = typeFactory.CreateType(bodyType).OutputType; + ObjectSerialization serialization = SerializationBuilder.Build(response.BodyMediaType, bodyType, responseType, null); + + return new ObjectResponseBody(responseType, serialization); + } + + private static IEnumerable GetPathSegments(string httpRequestUri, IReadOnlyDictionary parameters, bool isRaw) + { + var segments = new List(); + + foreach ((ReadOnlySpan span, bool isLiteral) in StringExtensions.GetPathParts(httpRequestUri)) + { + var text = span.ToString(); + if (isLiteral) + { + segments.Add(new PathSegment(BuilderHelpers.StringConstant(text), false, SerializationFormat.Default, isRaw)); + } + else + { + if (parameters.TryGetValue(text, out var parameter)) + { + segments.Add(parameter); + } + else + { + ErrorHelpers.ThrowError($"\n\nError while processing request '{httpRequestUri}'\n\n '{text}' in URI is missing a matching definition in the path parameters collection{ErrorHelpers.UpdateSwaggerOrFile}"); + } + } + } + + return segments; + } + + /// + /// Sort the parameters, move required parameters at the beginning, in order. + /// + /// Parameters to sort + /// + private static Parameter[] OrderParametersByRequired(IEnumerable parameters) => parameters.OrderBy(p => p.IsOptionalInSignature).ToArray(); + + // Merges operations without response types types together + private static CSharpType? ReduceResponses(List responses) + { + foreach (var typeGroup in responses.GroupBy(r => r.ResponseBody)) + { + foreach (var individualResponse in typeGroup) + { + responses.Remove(individualResponse); + } + + responses.Add(new Response( + typeGroup.Key, + typeGroup.SelectMany(r => r.StatusCodes).Distinct().ToArray())); + } + + var bodyTypes = responses.Select(r => r.ResponseBody?.Type) + .OfType() + .Distinct() + .ToArray(); + + return bodyTypes.Length switch + { + 0 => null, + 1 => bodyTypes[0], + _ => typeof(object) + }; + } + + public virtual Parameter BuildConstructorParameter(InputParameter operationParameter) + { + var parameter = BuildParameter(operationParameter); + if (!operationParameter.IsEndpoint) + { + return parameter; + } + + var defaultValue = parameter.DefaultValue; + var description = parameter.Description; + var location = parameter.RequestLocation; + + return defaultValue != null + ? KnownParameters.Endpoint with { Description = description, RequestLocation = location, DefaultValue = Constant.Default(new CSharpType(typeof(Uri), true)), Initializer = $"new {typeof(Uri)}({defaultValue.Value.GetConstantFormattable()})" } + : KnownParameters.Endpoint with { Description = description, RequestLocation = location, Validation = parameter.Validation }; + } + + public static bool IsIgnoredHeaderParameter(InputParameter operationParameter) + => operationParameter.Location == RequestLocation.Header && IgnoredRequestHeader.Contains(operationParameter.NameInRequest); + + private Parameter BuildParameter(in InputParameter operationParameter, Type? typeOverride = null) + { + CSharpType type = typeOverride != null ? new CSharpType(typeOverride, operationParameter.Type is InputNullableType) : + // for apiVersion, we still convert enum type to enum value type + operationParameter is { IsApiVersion: true, Type: InputEnumType enumType } ? _typeFactory.CreateType(enumType.ValueType) : _typeFactory.CreateType(operationParameter.Type); + return Parameter.FromInputParameter(operationParameter, type, _typeFactory); + } + + public static RestClientMethod BuildNextPageMethod(RestClientMethod method) + { + var nextPageUrlParameter = new Parameter( + "nextLink", + $"The URL to the next page of results.", + typeof(string), + DefaultValue: null, + ValidationType.AssertNotNull, + null); + + PathSegment[] pathSegments = method.Request.PathSegments + .Where(ps => ps.IsRaw) + .Append(new PathSegment(nextPageUrlParameter, false, SerializationFormat.Default, isRaw: true)) + .ToArray(); + + var request = new Request( + RequestMethod.Get, + pathSegments, + Array.Empty(), + method.Request.Headers, + null); + + Parameter[] parameters = method.Parameters.Where(p => p.Name != nextPageUrlParameter.Name) + .Prepend(nextPageUrlParameter) + .ToArray(); + + var responses = method.Responses; + + // We hardcode 200 as expected response code for paged LRO results + if (method.Operation.LongRunning != null) + { + responses = new[] + { + new Response(null, new[] { new StatusCodes(200, null) }) + }; + } + + return new RestClientMethod( + $"{method.Name}NextPage", + method.Summary, + method.Description, + method.ReturnType, + request, + parameters, + responses, + method.HeaderModel, + bufferResponse: true, + accessibility: "internal", + method.Operation); + } + + public static IEnumerable GetRequiredParameters(IEnumerable parameters) + => parameters.Where(parameter => !parameter.IsOptionalInSignature).ToList(); + + public static IEnumerable GetOptionalParameters(IEnumerable parameters, bool includeAPIVersion = false) + => parameters.Where(parameter => parameter.IsOptionalInSignature && (includeAPIVersion || !parameter.IsApiVersionParameter)).ToList(); + + public static IReadOnlyCollection GetConstructorParameters(IReadOnlyList parameters, CSharpType? credentialType, bool includeAPIVersion = false) + { + var constructorParameters = new List(); + + constructorParameters.AddRange(GetRequiredParameters(parameters)); + + if (credentialType != null) + { + var credentialParam = new Parameter( + "credential", + Configuration.ApiTypes.CredentialDescription, + credentialType, + null, + ValidationType.AssertNotNull, + null); + constructorParameters.Add(credentialParam); + } + + constructorParameters.AddRange(GetOptionalParameters(parameters, includeAPIVersion)); + + return constructorParameters; + } + } + + internal record RequestPartSource(string NameInRequest, InputParameter? InputParameter, ReferenceOrConstant Reference, SerializationFormat SerializationFormat); +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepArraySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepArraySerialization.cs new file mode 100644 index 0000000..1f06f47 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepArraySerialization.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization.Json; + +namespace AutoRest.CSharp.Output.Models.Serialization.Bicep +{ + internal class BicepArraySerialization : BicepSerialization + { + public BicepArraySerialization(JsonArraySerialization serialization) : base(serialization) + { + ImplementationType = serialization.Type; + ValueSerialization = Create(serialization.ValueSerialization); + } + + public CSharpType ImplementationType { get; } + + public BicepSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepDictionarySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepDictionarySerialization.cs new file mode 100644 index 0000000..46aa342 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepDictionarySerialization.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization.Json; + +namespace AutoRest.CSharp.Output.Models.Serialization.Bicep +{ + internal class BicepDictionarySerialization : BicepSerialization + { + public BicepDictionarySerialization(JsonDictionarySerialization serialization) : base(serialization) + { + Type = serialization.Type; + ValueSerialization = Create(serialization.ValueSerialization); + } + + public CSharpType Type { get; } + + public BicepSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepObjectSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepObjectSerialization.cs new file mode 100644 index 0000000..78398ea --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepObjectSerialization.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Types; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Models.Serialization.Bicep +{ + internal class BicepObjectSerialization + { + public BicepObjectSerialization(SerializableObjectType objectType, JsonObjectSerialization jsonObjectSerialization) + { + IsResourceData = EvaluateIsResourceData(jsonObjectSerialization); + CustomizationType = objectType.GetExistingType(); + + Serializations = jsonObjectSerialization.Properties.Select(p => + new BicepPropertySerialization(p, p.SerializationHooks?.BicepSerializationMethodName)); + + Properties = objectType.Properties; + FlattenedProperties = objectType.Properties + .Where(p => p.FlattenedProperty != null) + .Select(p => p.FlattenedProperty!).ToList(); + } + + public INamedTypeSymbol? CustomizationType { get; set; } + + public IReadOnlyList Properties { get; set; } + + private static bool EvaluateIsResourceData(JsonObjectSerialization jsonObjectSerialization) + { + bool hasName = false; + bool hasType = false; + bool hasId = false; + foreach (var property in jsonObjectSerialization.Properties) + { + switch (property.SerializedName) + { + case "name": + hasName = true; + break; + case "type": + hasType = true; + break; + case "id": + hasId = true; + break; + } + if (hasId && hasName && hasType) + { + return true; + } + } + + return false; + } + + public IList FlattenedProperties { get; } + + public IEnumerable Serializations { get; } + + public bool IsResourceData { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepPropertySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepPropertySerialization.cs new file mode 100644 index 0000000..26b9a74 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepPropertySerialization.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Output.Models.Serialization.Json; + +namespace AutoRest.CSharp.Output.Models.Serialization.Bicep +{ + internal class BicepPropertySerialization : PropertySerialization + { + public BicepPropertySerialization(JsonPropertySerialization serialization, string? customSerializationMethodName) + : base( + serialization.SerializationConstructorParameterName, + serialization.Value, + serialization.SerializedName, + serialization.SerializedType, + serialization.IsRequired, + serialization.ShouldExcludeInWireSerialization, + serialization.Property, + serialization.EnumerableValue) + { + ValueSerialization = serialization.ValueSerialization switch + { + null => null, + JsonSerialization json => BicepSerialization.Create(json) + }; + + if (serialization.PropertySerializations != null) + { + PropertySerializations = serialization.PropertySerializations.Select(p => + new BicepPropertySerialization(p, customSerializationMethodName)); + } + + CustomSerializationMethodName = customSerializationMethodName; + } + + public BicepSerialization? ValueSerialization { get; } + public string? CustomSerializationMethodName { get; } + + public IEnumerable? PropertySerializations { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepSerialization.cs new file mode 100644 index 0000000..0347b08 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepSerialization.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Output.Models.Serialization.Json; + +namespace AutoRest.CSharp.Output.Models.Serialization.Bicep +{ + internal abstract class BicepSerialization : ObjectSerialization + { + protected BicepSerialization(JsonSerialization serialization) + { + IsNullable = serialization.IsNullable; + } + + public bool IsNullable { get; } + + public static BicepSerialization Create(JsonSerialization serialization) + { + return serialization switch + { + JsonValueSerialization valueSerialization => new BicepValueSerialization(valueSerialization), + JsonArraySerialization arraySerialization => new BicepArraySerialization(arraySerialization), + JsonDictionarySerialization objectSerialization => new BicepDictionarySerialization(objectSerialization), + _ => throw new InvalidOperationException($"Unknown serialization type {serialization.GetType()}") + }; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepValueSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepValueSerialization.cs new file mode 100644 index 0000000..3833ffd --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Bicep/BicepValueSerialization.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization.Json; + +namespace AutoRest.CSharp.Output.Models.Serialization.Bicep +{ + internal class BicepValueSerialization : BicepSerialization + { + public BicepValueSerialization(JsonValueSerialization serialization) : base(serialization) + { + Type = serialization.Type; + } + + public CSharpType Type { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/CustomSerializationHooks.cs b/logger/autorest.csharp/common/Output/Models/Serialization/CustomSerializationHooks.cs new file mode 100644 index 0000000..fb0082e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/CustomSerializationHooks.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal struct CustomSerializationHooks + { + public CustomSerializationHooks(string? jsonSerializationMethodName, string? jsonDeserializationMethodName, string? bicepSerializationMethodName) + { + JsonSerializationMethodName = jsonSerializationMethodName; + JsonDeserializationMethodName = jsonDeserializationMethodName; + BicepSerializationMethodName = bicepSerializationMethodName; + } + + public string? BicepSerializationMethodName { get; } + + public string? JsonDeserializationMethodName { get; } + + public string? JsonSerializationMethodName { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonAdditionalPropertiesSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonAdditionalPropertiesSerialization.cs new file mode 100644 index 0000000..aaef15a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonAdditionalPropertiesSerialization.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal class JsonAdditionalPropertiesSerialization : JsonPropertySerialization + { + /// + /// The implementation type of the additional properties, which should usually be `Dictionary{string, T}`. + /// This is not the type of the property, which is usually `IDictionary{string, T}`, or `IReadOnlyDictionary{string, T}`. + /// + public CSharpType ImplementationType { get; } + + public JsonAdditionalPropertiesSerialization(ObjectTypeProperty property, JsonSerialization valueSerialization, CSharpType type, bool shouldExcludeInWireSerialization) + : base(property.Declaration.Name.ToVariableName(), new TypedMemberExpression(null, property.Declaration.Name, property.Declaration.Type), property.Declaration.Name, property.ValueType, valueSerialization, true, shouldExcludeInWireSerialization, property) + { + ImplementationType = type; + } + + public JsonAdditionalPropertiesSerialization(ObjectTypeProperty property, JsonSerialization valueSerialization, CSharpType type, bool shouldExcludeInWireSerialization, bool shouldExcludeInWireDeserialization) + : base(property.Declaration.Name.ToVariableName(), new TypedMemberExpression(null, property.Declaration.Name, property.Declaration.Type), property.Declaration.Name, property.ValueType, valueSerialization, true, shouldExcludeInWireSerialization, shouldExcludeInWireDeserialization, property) + { + ImplementationType = type; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonArraySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonArraySerialization.cs new file mode 100644 index 0000000..ec55fcc --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonArraySerialization.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal class JsonArraySerialization: JsonSerialization + { + public JsonArraySerialization(CSharpType implementationType, JsonSerialization valueSerialization, bool isNullable) : base(implementationType, isNullable) + { + ValueSerialization = valueSerialization; + } + + public JsonSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonDictionarySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonDictionarySerialization.cs new file mode 100644 index 0000000..4e7fb42 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonDictionarySerialization.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal class JsonDictionarySerialization : JsonSerialization + { + public JsonDictionarySerialization(CSharpType type, JsonSerialization valueSerialization, bool isNullable) : base(type, isNullable) + { + ValueSerialization = valueSerialization; + } + + public JsonSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonDynamicPropertiesSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonDynamicPropertiesSerialization.cs new file mode 100644 index 0000000..2d33cf4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonDynamicPropertiesSerialization.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal class JsonDynamicPropertiesSerialization + { + public JsonDynamicPropertiesSerialization(JsonSerialization valueSerialization) + { + ValueSerialization = valueSerialization; + } + + public JsonSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonObjectSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonObjectSerialization.cs new file mode 100644 index 0000000..dc24713 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonObjectSerialization.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal record JsonObjectSerialization + { + public JsonObjectSerialization(SerializableObjectType model, IReadOnlyList constructorParameters, IReadOnlyList properties, IReadOnlyList selfProperties, JsonAdditionalPropertiesSerialization? additionalProperties, JsonAdditionalPropertiesSerialization? rawDataField, ObjectTypeDiscriminator? discriminator, JsonConverterProvider? jsonConverter) + { + Type = model.Type; + ConstructorParameters = constructorParameters; + Properties = properties; + SelfProperties = selfProperties; + AdditionalProperties = additionalProperties; + RawDataField = rawDataField; + Discriminator = discriminator; + JsonConverter = jsonConverter; + } + + public CSharpType Type { get; } + public IReadOnlyList ConstructorParameters { get; } + public IReadOnlyList Properties { get; } + public IReadOnlyList SelfProperties { get; } + public JsonAdditionalPropertiesSerialization? AdditionalProperties { get; } + public JsonAdditionalPropertiesSerialization? RawDataField { get; } + public ObjectTypeDiscriminator? Discriminator { get; } + public JsonConverterProvider? JsonConverter { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonPropertySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonPropertySerialization.cs new file mode 100644 index 0000000..2fb8d67 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonPropertySerialization.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal class JsonPropertySerialization : PropertySerialization + { + public JsonPropertySerialization( + string parameterName, + TypedValueExpression value, + string serializedName, + CSharpType? serializedType, + JsonSerialization valueSerialization, + bool isRequired, + bool shouldExcludeInWireSerialization, + ObjectTypeProperty property, + CustomSerializationHooks? serializationHooks = null, + TypedValueExpression? enumerableExpression = null) + : this(parameterName, value, serializedName, serializedType, valueSerialization, isRequired, shouldExcludeInWireSerialization, shouldExcludeInWireSerialization, property, serializationHooks, enumerableExpression) + { } + + public JsonPropertySerialization( + string parameterName, + TypedValueExpression value, + string serializedName, + CSharpType? serializedType, + JsonSerialization valueSerialization, + bool isRequired, + bool shouldExcludeInWireSerialization, + bool shouldExcludeInWireDeserialization, + ObjectTypeProperty property, + CustomSerializationHooks? serializationHooks = null, + TypedValueExpression? enumerableExpression = null) + : base(parameterName, value, serializedName, serializedType, isRequired, shouldExcludeInWireSerialization, shouldExcludeInWireDeserialization, property, enumerableExpression, serializationHooks) + { + ValueSerialization = valueSerialization; + CustomSerializationMethodName = serializationHooks?.JsonSerializationMethodName; + CustomDeserializationMethodName = serializationHooks?.JsonDeserializationMethodName; + } + + public JsonPropertySerialization(string serializedName, JsonPropertySerialization[] propertySerializations) + : base(string.Empty, new TypedMemberExpression(null, serializedName, typeof(object)), serializedName, null, false, false, null) + { + PropertySerializations = propertySerializations; + } + + public JsonSerialization? ValueSerialization { get; } + /// + /// This is not null when the property is flattened in generated client SDK `x-ms-client-flatten: true` + /// + public JsonPropertySerialization[]? PropertySerializations { get; } + + public string? CustomSerializationMethodName { get; } + + public string? CustomDeserializationMethodName { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonSerialization.cs new file mode 100644 index 0000000..e291d00 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonSerialization.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal abstract class JsonSerialization : ObjectSerialization + { + protected JsonSerialization(CSharpType type, bool isNullable, JsonSerializationOptions options = JsonSerializationOptions.None) + { + Type = type; + IsNullable = isNullable; + Options = options; + } + + public CSharpType Type { get; } + + public bool IsNullable { get; } + + public JsonSerializationOptions Options { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonSerializationOptions.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonSerializationOptions.cs new file mode 100644 index 0000000..d1eebfc --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonSerializationOptions.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal enum JsonSerializationOptions + { + None, + UseManagedServiceIdentityV3 + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonValueSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonValueSerialization.cs new file mode 100644 index 0000000..0a9b1ae --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Json/JsonValueSerialization.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Json +{ + internal class JsonValueSerialization : JsonSerialization + { + public JsonValueSerialization(CSharpType type, SerializationFormat format, bool isNullable, JsonSerializationOptions options = JsonSerializationOptions.None) : base(type, isNullable, options) + { + Format = format; + } + + public SerializationFormat Format { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartAdditionalPropertiesSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartAdditionalPropertiesSerialization.cs new file mode 100644 index 0000000..d3fafad --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartAdditionalPropertiesSerialization.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartAdditionalPropertiesSerialization : MultipartPropertySerialization + { + public CSharpType Type { get; } + + public MultipartAdditionalPropertiesSerialization(ObjectTypeProperty property, CSharpType type, MultipartSerialization valueSerialization, bool shouldExcludeInWireSerialization) + : base(property.Declaration.Name.ToVariableName(), new TypedMemberExpression(null, property.Declaration.Name, property.Declaration.Type), property.Declaration.Name, property.ValueType, valueSerialization, true, shouldExcludeInWireSerialization) + { + Type = type; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartArraySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartArraySerialization.cs new file mode 100644 index 0000000..507298e --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartArraySerialization.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartArraySerialization : MultipartSerialization + { + public MultipartArraySerialization(CSharpType implementationType, MultipartSerialization valueSerialization, bool isNullable) : base(isNullable, implementationType) + { + ValueSerialization = valueSerialization; + } + public MultipartSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartDictionarySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartDictionarySerialization.cs new file mode 100644 index 0000000..631bba1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartDictionarySerialization.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartDictionarySerialization : MultipartSerialization + { + public MultipartDictionarySerialization(CSharpType type, MultipartSerialization valueSerialization, bool isNullable) : base(isNullable, type) + { + ValueSerialization = valueSerialization; + } + public MultipartSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartFormDataObjectSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartFormDataObjectSerialization.cs new file mode 100644 index 0000000..a5fe85d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartFormDataObjectSerialization.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartFormDataObjectSerialization + { + public MultipartFormDataObjectSerialization(SerializableObjectType model, IReadOnlyList constructorParameters, IReadOnlyList properties, MultipartAdditionalPropertiesSerialization? additionalProperties, ObjectTypeDiscriminator? discriminator, bool includeConverter) + { + Type = model.Type; + ConstructorParameters = constructorParameters; + Properties = properties; + AdditionalProperties = additionalProperties; + Discriminator = discriminator; + // select interface model type here + var modelType = model.IsUnknownDerivedType && model.Inherits is { IsFrameworkType: false, Implementation: { } baseModel } ? baseModel.Type : model.Type; + } + public CSharpType Type { get; } + public IReadOnlyList ConstructorParameters { get; } + public IReadOnlyList Properties { get; } + public MultipartAdditionalPropertiesSerialization? AdditionalProperties { get; } + public ObjectTypeDiscriminator? Discriminator { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartFormDataSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartFormDataSerialization.cs new file mode 100644 index 0000000..fbec41d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartFormDataSerialization.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartFormDataSerialization : MultipartSerialization + { + public MultipartFormDataSerialization(bool isNullable, CSharpType type) : base(isNullable, type) + { + } + public string subType { get; } = "form-data"; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartObjectSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartObjectSerialization.cs new file mode 100644 index 0000000..80ea6d5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartObjectSerialization.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartObjectSerialization + { + public MultipartObjectSerialization(SerializableObjectType model, IReadOnlyList constructorParameters, IReadOnlyList properties, MultipartAdditionalPropertiesSerialization? additionalProperties, ObjectTypeDiscriminator? discriminator, bool includeConverter) + { + Type = model.Type; + ConstructorParameters = constructorParameters; + Properties = properties; + AdditionalProperties = additionalProperties; + Discriminator = discriminator; + // select interface model type here + var modelType = model.IsUnknownDerivedType && model.Inherits is { IsFrameworkType: false, Implementation: { } baseModel } ? baseModel.Type : model.Type; + } + public CSharpType Type { get; } + public IReadOnlyList ConstructorParameters { get; } + public IReadOnlyList Properties { get; } + public MultipartAdditionalPropertiesSerialization? AdditionalProperties { get; } + public ObjectTypeDiscriminator? Discriminator { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartPropertySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartPropertySerialization.cs new file mode 100644 index 0000000..99928fb --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartPropertySerialization.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartPropertySerialization : PropertySerialization + { + public MultipartPropertySerialization( + string parameterName, + TypedValueExpression value, + string serializedName, + CSharpType? serializedType, + MultipartSerialization valueSerialization, + bool isRequired, + bool shouldExcludeInWireSerialization, + ObjectTypeProperty? property = null, + TypedValueExpression? enumerableExpression = null) + : base(parameterName, value, serializedName, serializedType, isRequired, shouldExcludeInWireSerialization, property, enumerableExpression) + { + ValueSerialization = valueSerialization; + } + + public MultipartSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartSerialization.cs new file mode 100644 index 0000000..8a96f04 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartSerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal abstract class MultipartSerialization : ObjectSerialization + { + protected MultipartSerialization(bool isNullable, CSharpType type) + { + IsNullable = isNullable; + Type = type; + } + public bool IsNullable { get; } + public CSharpType Type { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartValueSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartValueSerialization.cs new file mode 100644 index 0000000..eb98dad --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Multipart/MultipartValueSerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; + +namespace AutoRest.CSharp.Common.Output.Models.Serialization.Multipart +{ + internal class MultipartValueSerialization : MultipartSerialization + { + public MultipartValueSerialization(CSharpType type, SerializationFormat format, bool isNullable) : base(isNullable, type) + { + Format = format; + } + public string? ContentType { get; set; } + public string? FileName { get; set; } + public SerializationFormat Format { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/ObjectSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/ObjectSerialization.cs new file mode 100644 index 0000000..befe594 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/ObjectSerialization.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal abstract class ObjectSerialization + { + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/ObjectTypeSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/ObjectTypeSerialization.cs new file mode 100644 index 0000000..b1e5355 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/ObjectTypeSerialization.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Serialization.Multipart; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal class ObjectTypeSerialization + { + public ObjectTypeSerialization(SerializableObjectType model, JsonObjectSerialization? json, XmlObjectSerialization? xml, BicepObjectSerialization? bicep, MultipartObjectSerialization? multipart, SerializationInterfaces? interfaces) + { + Json = json; + Xml = xml; + Bicep = bicep; + Multipart = multipart; + WireFormat = Xml != null ? Serializations.XmlFormat : Multipart != null ? Serializations.MultipartFormat : Serializations.JsonFormat; + Interfaces = interfaces; + + if (Configuration.UseModelReaderWriter && model.Declaration.IsAbstract && model.Discriminator is { } discriminator) + { + PersistableModelProxyType = discriminator.DefaultObjectType.Type; + } + } + + public JsonObjectSerialization? Json { get; } + + public XmlObjectSerialization? Xml { get; } + + public BicepObjectSerialization? Bicep { get; } + public MultipartObjectSerialization? Multipart { get; } + + public bool HasSerializations => Json != null || Xml != null || Bicep != null; + + public ValueExpression WireFormat { get; } + + public SerializationInterfaces? Interfaces { get; } + + public CSharpType? PersistableModelProxyType { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/PropertySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/PropertySerialization.cs new file mode 100644 index 0000000..2a3e357 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/PropertySerialization.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal abstract class PropertySerialization + { + /// + /// Name of the parameter in serialization constructor. Used in deserialization logic only + /// + public string SerializationConstructorParameterName { get; } + + /// + /// Value expression to be serialized. Used in serialization logic only. + /// + public TypedValueExpression Value { get; } + + /// + /// Value expression to be enumerated over. Used in serialization logic only. + /// + public TypedValueExpression? EnumerableValue { get; } + + /// + /// Name of the property in serialized string + /// + public string SerializedName { get; } + /// + /// 'Original' type of the serialized value + /// + public CSharpType? SerializedType { get; } + + public bool IsRequired { get; } + + /// + /// Should this property be excluded in wire serialization + /// + public bool ShouldExcludeInWireSerialization { get; } + + /// + /// Should this property be excluded in wire deserialization + /// + public bool ShouldExcludeInWireDeserialization { get; } + + public CustomSerializationHooks? SerializationHooks { get; } + + public ObjectTypeProperty? Property { get; } + + protected PropertySerialization(string parameterName, TypedValueExpression value, string serializedName, CSharpType? serializedType, bool isRequired, bool shouldExcludeInWireSerialization, ObjectTypeProperty? property, TypedValueExpression? enumerableValue = null, CustomSerializationHooks? serializationHooks = null) : this(parameterName, value, serializedName, serializedType, isRequired, shouldExcludeInWireSerialization, shouldExcludeInWireSerialization, property, enumerableValue, serializationHooks) + { } + + protected PropertySerialization(string parameterName, TypedValueExpression value, string serializedName, CSharpType? serializedType, bool isRequired, bool shouldExcludeInWireSerialization, bool shouldExcludeInWireDeserialization, ObjectTypeProperty? property, TypedValueExpression? enumerableValue = null, CustomSerializationHooks? serializationHooks = null) + { + SerializationConstructorParameterName = parameterName; + Value = value; + SerializedName = serializedName; + SerializedType = serializedType; + IsRequired = isRequired; + ShouldExcludeInWireSerialization = shouldExcludeInWireSerialization; + ShouldExcludeInWireDeserialization = shouldExcludeInWireDeserialization; + EnumerableValue = enumerableValue; + SerializationHooks = serializationHooks; + Property = property; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/SerializationFormat.cs b/logger/autorest.csharp/common/Output/Models/Serialization/SerializationFormat.cs new file mode 100644 index 0000000..b642dd6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/SerializationFormat.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal enum SerializationFormat + { + Default, + DateTime_RFC1123, + DateTime_RFC3339, + DateTime_RFC7231, + DateTime_ISO8601, + DateTime_Unix, + Date_ISO8601, + Duration_ISO8601, + Duration_Constant, + Duration_Seconds, + Duration_Seconds_Float, + Duration_Seconds_Double, + Time_ISO8601, + Bytes_Base64Url, + Bytes_Base64, + Int_String + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/SerializationFormatExtensions.cs b/logger/autorest.csharp/common/Output/Models/Serialization/SerializationFormatExtensions.cs new file mode 100644 index 0000000..a235df5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/SerializationFormatExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal static class SerializationFormatExtensions + { + public static string? ToFormatSpecifier(this SerializationFormat format) => format switch + { + SerializationFormat.DateTime_RFC1123 => "R", + SerializationFormat.DateTime_RFC3339 => "O", + SerializationFormat.DateTime_RFC7231 => "R", + SerializationFormat.DateTime_ISO8601 => "O", + SerializationFormat.Date_ISO8601 => "D", + SerializationFormat.DateTime_Unix => "U", + SerializationFormat.Bytes_Base64Url => "U", + SerializationFormat.Bytes_Base64 => "D", + SerializationFormat.Duration_ISO8601 => "P", + SerializationFormat.Duration_Constant => "c", + SerializationFormat.Duration_Seconds => "%s", + SerializationFormat.Duration_Seconds_Float => "s\\.FFF", + SerializationFormat.Duration_Seconds_Double => "s\\.FFFFFF", + SerializationFormat.Time_ISO8601 => "T", + _ => null + }; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/SerializationInterfaces.cs b/logger/autorest.csharp/common/Output/Models/Serialization/SerializationInterfaces.cs new file mode 100644 index 0000000..48aa0c4 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/SerializationInterfaces.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using Azure.Core; + +namespace AutoRest.CSharp.Output.Models.Serialization +{ + internal record SerializationInterfaces : IEnumerable + { + public SerializationInterfaces(bool includeSerializer, bool isStruct, CSharpType modelType, bool hasJson, bool hasXml) + { + // TODO -- includeSerializer could be removed after the use-model-reader-writer configuration is removed because it is always true + if (includeSerializer) + { + if (hasJson) + { + if (Configuration.IsBranded) + { + IJsonInterface = typeof(IUtf8JsonSerializable); + } + if (Configuration.UseModelReaderWriter) + { + IJsonModelTInterface = new CSharpType(typeof(IJsonModel<>), modelType); + IPersistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), modelType); + // we only need this interface when the model is a struct + IJsonModelObjectInterface = isStruct ? (CSharpType)typeof(IJsonModel) : null; + IPersistableModelObjectInterface = isStruct ? (CSharpType)typeof(IPersistableModel) : null; + } + } + if (hasXml) + { + IXmlInterface = Configuration.ApiTypes.IXmlSerializableType; + if (Configuration.UseModelReaderWriter) + { + IPersistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), modelType); + IPersistableModelObjectInterface = isStruct ? (CSharpType)typeof(IPersistableModel) : null; + } + } + } + } + + public CSharpType? IJsonInterface { get; init; } + + public CSharpType? IXmlInterface { get; init; } + + public CSharpType? IJsonModelTInterface { get; init; } + + public CSharpType? IJsonModelObjectInterface { get; init; } + + public CSharpType? IPersistableModelTInterface { get; init; } + + public CSharpType? IPersistableModelObjectInterface { get; init; } + + private IReadOnlyList? _interfaces; + private IReadOnlyList Interfaces => _interfaces ??= BuildInterfaces(); + + private IReadOnlyList BuildInterfaces() + { + bool hasIJsonT = false; + bool hasIJsonObject = false; + var interfaces = new List(); + if (IJsonInterface is not null) + { + interfaces.Add(IJsonInterface); + } + if (IJsonModelTInterface is not null) + { + interfaces.Add(IJsonModelTInterface); + hasIJsonT = true; + } + if (IJsonModelObjectInterface is not null) + { + interfaces.Add(IJsonModelObjectInterface); + hasIJsonObject = true; + } + if (IXmlInterface is not null) + { + interfaces.Add(IXmlInterface); + } + if (!hasIJsonT && IPersistableModelTInterface is not null) + { + interfaces.Add(IPersistableModelTInterface); + } + if (!hasIJsonObject && IPersistableModelObjectInterface is not null) + { + interfaces.Add(IPersistableModelObjectInterface); + } + return interfaces; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Interfaces.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Interfaces.GetEnumerator(); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlArraySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlArraySerialization.cs new file mode 100644 index 0000000..9dc66e0 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlArraySerialization.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlArraySerialization : XmlElementSerialization + { + public XmlArraySerialization(CSharpType type, XmlElementSerialization valueSerialization, string name, bool wrapped) + { + Type = type; + ValueSerialization = valueSerialization; + Name = name; + Wrapped = wrapped; + } + + public CSharpType Type { get; } + public XmlElementSerialization ValueSerialization { get; } + public override string Name { get; } + public bool Wrapped { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlDictionarySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlDictionarySerialization.cs new file mode 100644 index 0000000..bbc58c8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlDictionarySerialization.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlDictionarySerialization : XmlElementSerialization + { + public XmlDictionarySerialization(CSharpType type, XmlElementSerialization valueSerialization, string name) + { + Type = type; + ValueSerialization = valueSerialization; + Name = name; + } + + public override string Name { get; } + public CSharpType Type { get; } + public XmlElementSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlElementSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlElementSerialization.cs new file mode 100644 index 0000000..efaba59 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlElementSerialization.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal abstract class XmlElementSerialization: ObjectSerialization + { + public abstract string Name { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlElementValueSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlElementValueSerialization.cs new file mode 100644 index 0000000..bc1b8b3 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlElementValueSerialization.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlElementValueSerialization: XmlElementSerialization + { + public XmlElementValueSerialization(string name, XmlValueSerialization value) + { + Name = name; + Value = value; + } + + public override string Name { get; } + public XmlValueSerialization Value { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectArraySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectArraySerialization.cs new file mode 100644 index 0000000..75ad96a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectArraySerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlObjectArraySerialization : XmlPropertySerialization + { + public XmlObjectArraySerialization(string serializedName, CSharpType serializedType, ObjectTypeProperty property, XmlArraySerialization arraySerialization) + : base(serializedName, serializedType, property) + { + ArraySerialization = arraySerialization; + } + + public XmlArraySerialization ArraySerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectAttributeSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectAttributeSerialization.cs new file mode 100644 index 0000000..7970b4f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectAttributeSerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlObjectAttributeSerialization : XmlPropertySerialization + { + public XmlObjectAttributeSerialization(string serializedName, CSharpType serializedType, ObjectTypeProperty property, XmlValueSerialization valueSerialization) + : base(serializedName, serializedType, property) + { + ValueSerialization = valueSerialization; + } + + public XmlValueSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectContentSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectContentSerialization.cs new file mode 100644 index 0000000..dfc3550 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectContentSerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlObjectContentSerialization : XmlPropertySerialization + { + public XmlObjectContentSerialization(string serializedName, CSharpType serializedType, ObjectTypeProperty property, XmlValueSerialization valueSerialization) + : base(serializedName, serializedType, property) + { + ValueSerialization = valueSerialization; + } + + public XmlValueSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectElementSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectElementSerialization.cs new file mode 100644 index 0000000..dbab60a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectElementSerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlObjectElementSerialization : XmlPropertySerialization + { + public XmlObjectElementSerialization(string serializedName, CSharpType serializedType, ObjectTypeProperty property, XmlElementSerialization valueSerialization) + : base(serializedName, serializedType, property) + { + ValueSerialization = valueSerialization; + } + + public XmlElementSerialization ValueSerialization { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectSerialization.cs new file mode 100644 index 0000000..d7accd6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlObjectSerialization.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlObjectSerialization + { + public XmlObjectSerialization(string name, + SerializableObjectType model, + XmlObjectElementSerialization[] elements, + XmlObjectAttributeSerialization[] attributes, + XmlObjectArraySerialization[] embeddedArrays, + XmlObjectContentSerialization? contentSerialization, + string? writeXmlMethodName = null) + { + Type = model.Type; + Elements = elements; + Attributes = attributes; + Name = name; + EmbeddedArrays = embeddedArrays; + ContentSerialization = contentSerialization; + WriteXmlMethodName = writeXmlMethodName ?? "WriteInternal"; + } + + public string Name { get; } + public XmlObjectElementSerialization[] Elements { get; } + public XmlObjectAttributeSerialization[] Attributes { get; } + public XmlObjectArraySerialization[] EmbeddedArrays { get; } + public XmlObjectContentSerialization? ContentSerialization { get; } + public CSharpType Type { get; } + + public string WriteXmlMethodName { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlPropertySerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlPropertySerialization.cs new file mode 100644 index 0000000..733a11a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlPropertySerialization.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal abstract class XmlPropertySerialization : PropertySerialization + { + public string PropertyName { get; } + + protected XmlPropertySerialization(string serializedName, CSharpType serializedType, ObjectTypeProperty property) + : base(property.Declaration.Name.ToVariableName(), new TypedMemberExpression(null, property.Declaration.Name, property.Declaration.Type), serializedName, serializedType, property.InputModelProperty?.IsRequired ?? false, property.InputModelProperty?.IsReadOnly ?? false, property) + { + PropertyName = property.Declaration.Name; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlValueSerialization.cs b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlValueSerialization.cs new file mode 100644 index 0000000..0f678bb --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Serialization/Xml/XmlValueSerialization.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Serialization.Xml +{ + internal class XmlValueSerialization + { + public XmlValueSerialization(CSharpType type, SerializationFormat format) + { + Type = type; + Format = format; + } + public CSharpType Type { get; } + public SerializationFormat Format { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Shared/CSharpAttribute.cs b/logger/autorest.csharp/common/Output/Models/Shared/CSharpAttribute.cs new file mode 100644 index 0000000..81b63e6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Shared/CSharpAttribute.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Shared +{ + internal record CSharpAttribute(CSharpType Type, params ValueExpression[] Arguments); +} diff --git a/logger/autorest.csharp/common/Output/Models/Shared/Constant.cs b/logger/autorest.csharp/common/Output/Models/Shared/Constant.cs new file mode 100644 index 0000000..796bd36 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Shared/Constant.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Shared +{ + internal readonly struct Constant + { + public static object NewInstanceSentinel { get; } = new object(); + + public Constant(object? value, CSharpType type) + { + Value = value; + Type = type; + + if (value == null) + { + if (!type.IsNullable) + { + throw new InvalidOperationException($"Null constant with non-nullable type {type}"); + } + } + + if (value == NewInstanceSentinel || value is Constant.Expression) + { + return; + } + + if (!type.IsFrameworkType && + type.Implementation is EnumType && + value != null && + !(value is EnumTypeValue || value is string)) + { + throw new InvalidOperationException($"Unexpected value '{value}' for enum type '{type}'"); + } + + if (value != null && type.IsFrameworkType && value.GetType() != type.FrameworkType) + { + throw new InvalidOperationException($"Constant type mismatch. Value type is '{value.GetType()}'. CSharpType is '{type}'."); + } + } + + public object? Value { get; } + public CSharpType Type { get; } + public bool IsNewInstanceSentinel => Value == NewInstanceSentinel; + + public static Constant NewInstanceOf(CSharpType type) + { + return new Constant(NewInstanceSentinel, type); + } + + public static Constant FromExpression(FormattableString expression, CSharpType type) => new Constant(new Constant.Expression(expression), type); + + public static Constant Default(CSharpType type) + => type.IsValueType && !type.IsNullable ? new Constant(NewInstanceSentinel, type) : new Constant(null, type); + + /// + /// A value type. It represents an expression without any reference (e.g. 'DateTimeOffset.Now') + /// which looks like a constant. + /// + public class Expression + { + internal Expression(FormattableString expressionValue) + { + ExpressionValue = expressionValue; + } + + public FormattableString ExpressionValue { get; } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Shared/KnownParameters.Serialization.cs b/logger/autorest.csharp/common/Output/Models/Shared/KnownParameters.Serialization.cs new file mode 100644 index 0000000..9b259c9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Shared/KnownParameters.Serialization.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; +using System.Xml; +using System.Xml.Linq; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Shared +{ + internal static partial class KnownParameters + { + public static class Serializations + { + private static readonly CSharpType modelReaderWriterOptionsType = typeof(ModelReaderWriterOptions); + private static readonly CSharpType nullableModelReaderWriterOptionsType = new CSharpType(typeof(ModelReaderWriterOptions), isNullable: true); + + public static readonly Parameter XmlWriter = new Parameter("writer", null, typeof(XmlWriter), null, ValidationType.None, null); + public static readonly Parameter NameHint = new Parameter("nameHint", null, typeof(string), null, ValidationType.None, null); + public static readonly Parameter XElement = new Parameter("element", null, typeof(XElement), null, ValidationType.None, null); + + public static readonly Parameter Utf8JsonWriter = new Parameter("writer", null, typeof(Utf8JsonWriter), null, ValidationType.None, null); + public static readonly Parameter Utf8JsonWriterWithDescription = new Parameter("writer", $"The JSON writer.", typeof(Utf8JsonWriter), null, ValidationType.None, null); + public static readonly Parameter Utf8JsonReader = new Parameter("reader", null, typeof(Utf8JsonReader), null, ValidationType.None, null, IsRef: true); + public static readonly Parameter JsonOptions = new Parameter("options", null, typeof(JsonSerializerOptions), null, ValidationType.None, null); + public static readonly Parameter Options = new Parameter("options", null, modelReaderWriterOptionsType, null, ValidationType.None, null); + public static readonly Parameter OptionsWithDescription = new Parameter("options", $"The client options for reading and writing models.", typeof(ModelReaderWriterOptions), null, ValidationType.None, null); + public static readonly Parameter OptionalOptions = new Parameter("options", null, nullableModelReaderWriterOptionsType, Constant.Default(nullableModelReaderWriterOptionsType), ValidationType.None, null); + public static readonly Parameter JsonElement = new Parameter("element", null, typeof(JsonElement), null, ValidationType.None, null); + public static readonly Parameter Data = new Parameter("data", null, typeof(BinaryData), null, ValidationType.None, null); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Shared/KnownParameters.cs b/logger/autorest.csharp/common/Output/Models/Shared/KnownParameters.cs new file mode 100644 index 0000000..515c871 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Shared/KnownParameters.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Output.Models.Shared +{ + internal static partial class KnownParameters + { + private static readonly CSharpType MatchConditionsType = new(typeof(MatchConditions), true); + private static readonly CSharpType RequestConditionsType = new(typeof(RequestConditions), true); + private static readonly CSharpType RequestContentType = new(Configuration.ApiTypes.RequestContentType); + private static readonly CSharpType RequestContentNullableType = new(Configuration.ApiTypes.RequestContentType, true); + private static readonly string RequestContextName = Configuration.ApiTypes.RequestContextName; + private static readonly string RequestContextDescription = Configuration.ApiTypes.RequestContextDescription; + private static readonly CSharpType RequestContextType = new(Configuration.ApiTypes.RequestContextType); + private static readonly CSharpType RequestContextNullableType = new(Configuration.ApiTypes.RequestContextType, true); + private static readonly CSharpType ResponseType = new(Configuration.ApiTypes.ResponseType); + + public static readonly Parameter ClientDiagnostics = new("clientDiagnostics", $"The handler for diagnostic messaging in the client.", new CSharpType(typeof(ClientDiagnostics)), null, ValidationType.AssertNotNull, null); + public static readonly Parameter Pipeline = new("pipeline", $"The HTTP pipeline for sending and receiving REST requests and responses", new CSharpType(Configuration.ApiTypes.HttpPipelineType), null, ValidationType.AssertNotNull, null); + public static readonly Parameter KeyAuth = new("keyCredential", $"The key credential to copy", new CSharpType(Configuration.ApiTypes.KeyCredentialType), null, ValidationType.None, null); + public static readonly Parameter TokenAuth = new("tokenCredential", $"The token credential to copy", new CSharpType(typeof(TokenCredential)), null, ValidationType.None, null); + public static readonly Parameter Endpoint = new("endpoint", $"Service endpoint", new CSharpType(typeof(Uri)), null, ValidationType.None, null, RequestLocation: RequestLocation.Uri, IsEndpoint: true); + + public static readonly Parameter PageSizeHint = new("pageSizeHint", $"The number of items per {typeof(Page<>):C} that should be requested (from service operations that support it). It's not guaranteed that the value will be respected.", new CSharpType(typeof(int), true), null, ValidationType.None, null); + public static readonly Parameter NextLink = new("nextLink", $"Continuation token", typeof(string), null, ValidationType.None, null); + + public static readonly Parameter RequestContent = new("content", $"The content to send as the body of the request.", RequestContentType, null, ValidationType.AssertNotNull, null, RequestLocation: RequestLocation.Body); + public static readonly Parameter RequestContentNullable = new("content", $"The content to send as the body of the request.", RequestContentNullableType, /*Constant.Default(RequestContentNullableType)*/null, ValidationType.None, null, RequestLocation: RequestLocation.Body); + + public static readonly Parameter MatchConditionsParameter = new("matchConditions", $"The content to send as the request conditions of the request.", MatchConditionsType, Constant.Default(RequestConditionsType), ValidationType.None, null, RequestLocation: RequestLocation.Header); + public static readonly Parameter RequestConditionsParameter = new("requestConditions", $"The content to send as the request conditions of the request.", RequestConditionsType, Constant.Default(RequestConditionsType), ValidationType.None, null, RequestLocation: RequestLocation.Header); + + public static readonly Parameter RequestContext = new(RequestContextName, FormattableStringHelpers.FromString(RequestContextDescription), RequestContextNullableType, Constant.Default(RequestContextNullableType), ValidationType.None, null); + public static readonly Parameter RequestContextRequired = new(RequestContextName, FormattableStringHelpers.FromString(RequestContextDescription), RequestContextType, null, ValidationType.None, null); + + public static readonly Parameter WaitForCompletion = new("waitUntil", $" if the method should wait to return until the long-running operation has completed on the service; if it should return after starting the operation. For more information on long-running operations, please see Azure.Core Long-Running Operation samples.", new CSharpType(typeof(WaitUntil)), null, ValidationType.None, null); + + public static readonly Parameter CancellationTokenParameter = new("cancellationToken", $"The cancellation token to use", new CSharpType(typeof(CancellationToken)), Constant.NewInstanceOf(typeof(CancellationToken)), ValidationType.None, null); + public static readonly Parameter EnumeratorCancellationTokenParameter = new("cancellationToken", $"Enumerator cancellation token", typeof(CancellationToken), Constant.NewInstanceOf(typeof(CancellationToken)), ValidationType.None, null) { Attributes = new[] { new CSharpAttribute(typeof(EnumeratorCancellationAttribute)) } }; + + public static readonly Parameter Response = new("response", $"Response returned from backend service", ResponseType, null, ValidationType.None, null); + + public static readonly Parameter ArmClient = new( + Name: "client", + Description: $"The client parameters to use in these operations.", + Type: typeof(ArmClient), + DefaultValue: null, + Validation: ValidationType.None, + Initializer: null); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Shared/Parameter.cs b/logger/autorest.csharp/common/Output/Models/Shared/Parameter.cs new file mode 100644 index 0000000..25e545a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Shared/Parameter.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Models.Shared +{ + internal record Parameter(string Name, FormattableString? Description, CSharpType Type, Constant? DefaultValue, ValidationType Validation, FormattableString? Initializer, bool IsApiVersionParameter = false, bool IsEndpoint = false, bool IsResourceIdentifier = false, bool SkipUrlEncoding = false, RequestLocation RequestLocation = RequestLocation.None, SerializationFormat SerializationFormat = SerializationFormat.Default, bool IsPropertyBag = false, bool IsRef = false, bool IsOut = false) + { + public bool IsRawData { get; init; } + + public static IEqualityComparer TypeAndNameEqualityComparer = new ParameterTypeAndNameEqualityComparer(); + public CSharpAttribute[] Attributes { get; init; } = Array.Empty(); + public bool IsOptionalInSignature => DefaultValue != null; + + public Parameter WithRef(bool isRef = true) => IsRef == isRef ? this : this with { IsRef = isRef }; + + public Parameter ToRequired() + { + return this with { DefaultValue = null }; + } + + public static Parameter FromInputParameter(in InputParameter operationParameter, CSharpType type, TypeFactory typeFactory, bool shouldKeepClientDefaultValue = false) + { + var name = ConstructParameterVariableName(operationParameter, type); + var skipUrlEncoding = operationParameter.SkipUrlEncoding; + var requestLocation = operationParameter.Location; + + bool keepClientDefaultValue = shouldKeepClientDefaultValue || operationParameter.Kind == InputOperationParameterKind.Constant || operationParameter.IsApiVersion || operationParameter.IsContentType || operationParameter.IsEndpoint; + Constant? clientDefaultValue = GetDefaultValue(operationParameter, typeFactory); + + var defaultValue = keepClientDefaultValue + ? clientDefaultValue + : (Constant?)null; + + var initializer = (FormattableString?)null; + + if (defaultValue != null && operationParameter.Kind != InputOperationParameterKind.Constant && !type.CanBeInitializedInline(defaultValue)) + { + initializer = type.GetParameterInitializer(defaultValue.Value); + type = type.WithNullable(true); + defaultValue = Constant.Default(type); + } + + if (!operationParameter.IsRequired && defaultValue == null) + { + type = type.WithNullable(true); + defaultValue = Constant.Default(type); + } + + var validation = operationParameter.IsRequired && initializer == null + ? GetValidation(type, requestLocation, skipUrlEncoding) + : ValidationType.None; + + var inputType = type.InputType; + return new Parameter( + name, + CreateDescription(operationParameter, inputType, (operationParameter.Type.GetImplementType() as InputEnumType)?.Values.Select(c => c.GetValueString()), keepClientDefaultValue ? null : clientDefaultValue), + inputType, + defaultValue, + validation, + initializer, + IsApiVersionParameter: operationParameter.IsApiVersion, + IsEndpoint: operationParameter.IsEndpoint, + IsResourceIdentifier: operationParameter.IsResourceParameter, + SkipUrlEncoding: skipUrlEncoding, + RequestLocation: requestLocation, + SerializationFormat: SerializationBuilder.GetSerializationFormat(operationParameter.Type)); + } + + private static Constant? GetDefaultValue(InputParameter operationParameter, TypeFactory typeFactory) => operationParameter switch + { + { NameInRequest: var nameInRequest } when RequestHeader.ClientRequestIdHeaders.Contains(nameInRequest) => Constant.FromExpression($"message.{Configuration.ApiTypes.HttpMessageRequestName}.ClientRequestId", new CSharpType(typeof(string))), + { NameInRequest: var nameInRequest } when RequestHeader.ReturnClientRequestIdResponseHeaders.Contains(nameInRequest) => new Constant("true", new CSharpType(typeof(string))), + { DefaultValue: not null } => BuilderHelpers.ParseConstant(operationParameter.DefaultValue.Value, typeFactory.CreateType(operationParameter.DefaultValue.Type)), + { Type: InputLiteralType { Value: not null} literalValue } => BuilderHelpers.ParseConstant(literalValue.Value, typeFactory.CreateType(literalValue.ValueType)), + { NameInRequest: var nameInRequest } when nameInRequest.Equals(RequestHeader.RepeatabilityRequestId, StringComparison.OrdinalIgnoreCase) => + // Guid.NewGuid() + Constant.FromExpression($"{nameof(Guid)}.{nameof(Guid.NewGuid)}()", new CSharpType(typeof(string))), + { NameInRequest: var nameInRequest } when nameInRequest.Equals(RequestHeader.RepeatabilityFirstSent, StringComparison.OrdinalIgnoreCase) => + // DateTimeOffset.Now + Constant.FromExpression($"{nameof(DateTimeOffset)}.{nameof(DateTimeOffset.Now)}", new CSharpType(typeof(DateTimeOffset))), + _ => (Constant?)null, + }; + + public static FormattableString CreateDescription(InputParameter operationParameter, CSharpType type, IEnumerable? values, Constant? defaultValue = null) + { + FormattableString description = string.IsNullOrWhiteSpace(operationParameter.Description) + ? (FormattableString)$"The {type:C} to use." + : $"{BuilderHelpers.EscapeXmlDocDescription(operationParameter.Description)}"; + if (defaultValue != null) + { + var defaultValueString = defaultValue?.Value is string s ? $"\"{s}\"" : $"{defaultValue?.Value}"; + description = $"{description}{(description.ToString().EndsWith(".") ? "" : ".")} The default value is {defaultValueString}"; + } + + if (!type.IsFrameworkType || values == null) + { + return description; + } + + var allowedValues = string.Join(" | ", values.Select(v => $"\"{v}\"")); + return $"{description}{(description.ToString().EndsWith(".") ? "" : ".")} Allowed values: {BuilderHelpers.EscapeXmlDocDescription(allowedValues)}"; + } + + /// + /// This method constructs the variable name for an input parameter. If the input parameter type is an input model type, + /// and the input parameter name is the same as the input parameter type name, the variable name is constructed using the supplied CSharpType name. Otherwise, + /// it will use the input parameter name by default. + /// + /// The input parameter. + /// The constructed CSharpType for the input parameter. + /// A string representing the variable name for the input parameter. + private static string ConstructParameterVariableName(InputParameter param, CSharpType type) + { + string paramName = param.Name; + string variableName = paramName.ToVariableName(); + + if (param.Type.GetImplementType() is InputModelType paramInputType) + { + var paramInputTypeName = paramInputType.Name; + + if (paramName.Equals(paramInputTypeName) || // remove this after adoption of TCGC + paramName.Equals(paramInputTypeName.ToVariableName())) + { + variableName = !string.IsNullOrEmpty(type.Name) ? type.Name.ToVariableName() : variableName; + } + + } + + return variableName; + } + + public static ValidationType GetValidation(CSharpType type, RequestLocation requestLocation, bool skipUrlEncoding) + { + if (requestLocation is RequestLocation.Uri or RequestLocation.Path or RequestLocation.Body && type.EqualsIgnoreNullable(typeof(string)) && !skipUrlEncoding) + { + return ValidationType.AssertNotNullOrEmpty; + } + + if (!type.IsValueType) + { + return ValidationType.AssertNotNull; + } + + return ValidationType.None; + } + + private static FormattableString CreateDescription(InputParameter requestParameter, CSharpType type, Constant? defaultValue = null) + { + FormattableString description = string.IsNullOrWhiteSpace(requestParameter.Description) ? + (FormattableString)$"The {type:C} to use." : + $"{BuilderHelpers.EscapeXmlDocDescription(requestParameter.Description)}"; + if (defaultValue != null) + { + var defaultValueString = defaultValue?.Value is string s ? $"\"{s}\"" : $"{defaultValue?.Value}"; + description = $"{description}{(description.ToString().EndsWith(".") ? "" : ".")} The default value is {defaultValueString}"; + } + + return requestParameter.Type switch + { + InputEnumType choiceSchema when type.IsFrameworkType => AddAllowedValues(description, choiceSchema.Values), + InputNullableType { Type: InputEnumType ie } => AddAllowedValues(description, ie.Values), + _ => description + }; + + static FormattableString AddAllowedValues(FormattableString description, IReadOnlyList choices) + { + var allowedValues = choices.Select(c => (FormattableString)$"{c.Value:L}").ToArray().Join(" | "); + + return allowedValues.IsNullOrEmpty() + ? description + : $"{description}{(description.ToString().EndsWith(".") ? "" : ".")} Allowed values: {BuilderHelpers.EscapeXmlDocDescription(allowedValues.ToString())}"; + } + } + + private static Constant? ParseConstant(InputParameter parameter, TypeFactory typeFactory) + { + if (parameter.Location == RequestLocation.Header) + { + if (RequestHeader.ClientRequestIdHeaders.Contains(parameter.NameInRequest ?? parameter.Name)) + { + return Constant.FromExpression($"message.{Configuration.ApiTypes.HttpMessageRequestName}.ClientRequestId", new CSharpType(typeof(string))); + } + else if (RequestHeader.ReturnClientRequestIdResponseHeaders.Contains(parameter.NameInRequest ?? parameter.Name)) + { + return new Constant("true", new CSharpType(typeof(string))); + } + } + if (parameter.Kind == InputOperationParameterKind.Constant && parameter.IsRequired) + { + return GetDefaultValue(parameter, typeFactory); + } + + return null; + } + + public static readonly IEqualityComparer EqualityComparerByType = new ParameterByTypeEqualityComparer(); + private struct ParameterByTypeEqualityComparer : IEqualityComparer + { + public bool Equals(Parameter? x, Parameter? y) + { + return object.Equals(x?.Type, y?.Type); + } + + public int GetHashCode([DisallowNull] Parameter obj) => obj.Type.GetHashCode(); + } + + private class ParameterTypeAndNameEqualityComparer : IEqualityComparer + { + public bool Equals(Parameter? x, Parameter? y) + { + if (object.ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + // We can't use CsharpType.Equals here because they can have different implementations from different versions + var result = x.Type.AreNamesEqual(y.Type) && x.Name == y.Name; + return result; + } + + public int GetHashCode([DisallowNull] Parameter obj) + { + // remove type as part of the hash code generation as the type might have changes between versions + return HashCode.Combine(obj.Name); + } + } + } + + internal enum ValidationType + { + None, + AssertNotNull, + AssertNotNullOrEmpty + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Shared/PropertyBag.cs b/logger/autorest.csharp/common/Output/Models/Shared/PropertyBag.cs new file mode 100644 index 0000000..e0169c9 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Shared/PropertyBag.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models.Shared +{ + internal abstract class PropertyBag + { + protected PropertyBag(string name) + { + Name = name; + } + + protected string Name { get; } + + private bool? _shouldValidateParameter; + private bool ShouldValidateParameter => _shouldValidateParameter ??= EnsureShouldValidateParameter(); + + protected abstract bool EnsureShouldValidateParameter(); + + private TypeProvider? _packModel; + public TypeProvider PackModel => _packModel ??= EnsurePackModel(); + + protected abstract TypeProvider EnsurePackModel(); + + private Parameter? _packParameter; + public Parameter PackParameter => _packParameter ??= EnsurePackParameter(); + + private Parameter EnsurePackParameter() + { + return new Parameter( + "options", + $"A property bag which contains all the parameters of this method except the LRO qualifier and request context parameter.", + PackModel.Type.InputType, + null, + ShouldValidateParameter ? ValidationType.AssertNotNull : ValidationType.None, + ShouldValidateParameter ? (FormattableString?)null : $"new {PackModel.Type.Name}()") with { IsPropertyBag = true }; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/TypeSignatureModifiers.cs b/logger/autorest.csharp/common/Output/Models/TypeSignatureModifiers.cs new file mode 100644 index 0000000..8ca5cad --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/TypeSignatureModifiers.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace AutoRest.CSharp.Output.Models +{ + [Flags] + internal enum TypeSignatureModifiers + { + None = 0, + Public = 1, + Internal = 2, + Private = 8, + Static = 16, + Partial = 32, + Sealed = 64, + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/AspDotNetExtensionTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/AspDotNetExtensionTypeProvider.cs new file mode 100644 index 0000000..a8f0423 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/AspDotNetExtensionTypeProvider.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Azure.Core.Extensions; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Common.Output.Models.Types +{ + internal class AspDotNetExtensionTypeProvider : TypeProvider + { + private const string AspDotNetExtensionNamespace = "Microsoft.Extensions.Azure"; + + internal string FullName => $"{Type.Namespace}.{Type.Name}"; + + private IReadOnlyList _clients; + private IReadOnlyList _topLevelClients; + + public AspDotNetExtensionTypeProvider(IReadOnlyList clients, string clientNamespace, SourceInputModel? sourceInputModel) : base(AspDotNetExtensionNamespace, sourceInputModel) + { + DefaultName = $"{ClientBuilder.GetRPName(clientNamespace)}ClientBuilderExtensions".ToCleanName(); + //TODO: very bad design that this list is empty when we leave the constructor and is filled in at some point in the future. + //creates lots of opportunity run into issues with iterators + _clients = clients; + _topLevelClients = _clients.Where(client => client is { IsSubClient: false, Declaration.Accessibility: "public" }).ToList(); + } + + public FormattableString Description => $"Extension methods to add {_topLevelClients.GetClientTypesFormattable()} to client builder"; + + protected override string DefaultName { get; } + + protected override string DefaultAccessibility => "public"; + + private Dictionary Parameters, IEnumerable ParameterValues)>? _extensionMethods; + public IReadOnlyDictionary Parameters, IEnumerable ParameterValues)> ExtesnsionMethods => _extensionMethods ??= EnsureExtensionMethods(); + + private Dictionary Parameters, IEnumerable ParameterValues)> EnsureExtensionMethods() + { + var result = new Dictionary Parameters, IEnumerable ParameterValues)>(); + foreach (var client in _topLevelClients) + { + var returnType = new CSharpType(typeof(IAzureClientBuilder<,>), client.Type, client.ClientOptions.Type); + foreach (var ctor in client.PrimaryConstructors) + { + var signatureParameters = new List() { FactoryBuilderParameter }; + var parameterDeclarations = new List(); + var parameterValues = new List(); + var includeCredential = false; + foreach (var parameter in ctor.Parameters) + { + if (parameter.Type.EqualsIgnoreNullable(client.ClientOptions.Type)) + { + // do not put the ClientOptions on the signature + var options = new CodeWriterDeclaration("options"); + parameterDeclarations.Insert(0, $"{options:D}"); // options are always the first in the callback signature + parameterValues.Add($"{options:I}"); + } + else if (parameter.Type.EqualsIgnoreNullable(typeof(TokenCredential))) + { + // do not put the TokenCredential on the signature + includeCredential = true; + var cred = new CodeWriterDeclaration("cred"); + parameterDeclarations.Add($"{cred:D}"); + parameterValues.Add($"{cred:I}"); + } + else + { + // for other parameters, we do not put it on the declarations because they are on the method signature + signatureParameters.Add(parameter); + parameterValues.Add($"{parameter.Name:I}"); + } + } + + FormattableString summary = $"Registers a {client.Type:C} instance"; + var constraint = includeCredential + ? Where.Implements(TBuilderType, typeof(IAzureClientFactoryBuilderWithCredential)) + : Where.Implements(TBuilderType, typeof(IAzureClientFactoryBuilder)); + var signature = new MethodSignature( + $"Add{client.Declaration.Name}", + summary, + summary, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension, + returnType, + null, + signatureParameters, + GenericArguments: new[] { TBuilderType }, + GenericParameterConstraints: new[] { constraint }); + result.Add(signature, (parameterDeclarations, parameterValues)); + } + } + + return result; + } + + private IEnumerable? _extensionMethodsWithoutCallback; + public IEnumerable ExtensionMethodsWithoutCallback => _extensionMethodsWithoutCallback ??= EnsureExtensionMethodsWithoutCallback(); + + private IEnumerable EnsureExtensionMethodsWithoutCallback() + { + foreach (var client in _topLevelClients) + { + var returnType = new CSharpType(typeof(IAzureClientBuilder<,>), client.Type, client.ClientOptions.Type); + + FormattableString summary = $"Registers a {client.Type:C} instance"; + yield return new MethodSignature( + $"Add{client.Declaration.Name}", + summary, + summary, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension, + returnType, + null, + new[] { FactoryBuilderParameter, ConfigurationParameter }, + GenericArguments: new[] { TBuilderType, TConfigurationType }, + GenericParameterConstraints: new[] + { + Where.Implements(TBuilderType, new CSharpType(typeof(IAzureClientFactoryBuilderWithConfiguration<>), TConfigurationType)) + }); + } + } + + private Parameter? _factoryBuilderParameter; + public Parameter FactoryBuilderParameter => _factoryBuilderParameter ??= new Parameter( + "builder", + $"The builder to register with.", + TBuilderType, + null, + ValidationType.None, + null); + + private Parameter? _configurationParameter; + public Parameter ConfigurationParameter => _configurationParameter ??= new Parameter( + "configuration", + $"The configuration values.", + TConfigurationType, + null, + ValidationType.None, + null); + + private static CSharpType? _builderType; + private static CSharpType? _configurationType; + // these two properties are getting the open generic parameter type of `TBuilder` and `TConfiguration` so that we could use them on the generated generic method + // since there is no method to manually construct this kind of open generic argument types. + private static CSharpType TBuilderType => _builderType ??= typeof(Template<>).GetGenericArguments()[0]; + private static CSharpType TConfigurationType => _configurationType ??= typeof(IAzureClientFactoryBuilderWithConfiguration<>).GetGenericArguments()[0]; + + // this is a private type that provides the open generic type argument "TBuilder" for us to use in the geenrated code + private class Template { } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/BuildContext.cs b/logger/autorest.csharp/common/Output/Models/Types/BuildContext.cs new file mode 100644 index 0000000..825e846 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/BuildContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class BuildContext + { + public BuildContext(InputNamespace inputNamespace, SourceInputModel? sourceInputModel) : this(inputNamespace, sourceInputModel,Configuration.LibraryName, Configuration.Namespace) + { } + + public BuildContext(InputNamespace inputNamespace, SourceInputModel? sourceInputModel, string defaultLibraryName, string defaultNamespace) + { + SourceInputModel = sourceInputModel; + DefaultLibraryName = defaultLibraryName; + DefaultNamespace = defaultNamespace; + InputNamespace = inputNamespace; + } + + public OutputLibrary? BaseLibrary { get; protected set; } + + public InputNamespace InputNamespace { get; } + public string DefaultNamespace { get; } + public string DefaultLibraryName { get; } + public SourceInputModel? SourceInputModel { get; } + public virtual TypeFactory TypeFactory { get; } = null!; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/BuildContextOfT.cs b/logger/autorest.csharp/common/Output/Models/Types/BuildContextOfT.cs new file mode 100644 index 0000000..952aa20 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/BuildContextOfT.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; + +namespace AutoRest.CSharp.Output.Models.Types +{ +#pragma warning disable SA1649 // File name should match first type name + internal class BuildContext : BuildContext where T : OutputLibrary +#pragma warning restore SA1649 // File name should match first type name + { + private TypeFactory? _typeFactory; + private InputNamespace _inputNamespace; + + private T? _library; + public T Library => _library ??= EnsureLibrary(); + + private T EnsureLibrary() + { + T library; + if (Configuration.AzureArm) + { + library = (T)(object)new MgmtOutputLibrary(_inputNamespace); + } + else + { + throw new InvalidOperationException($"{nameof(BuildContext)} is supported only in MPG"); + } + + BaseLibrary = library; + return library; + } + + public BuildContext(InputNamespace inputNamespace, SourceInputModel? sourceInputModel) + : base(inputNamespace, sourceInputModel) + { + _inputNamespace = inputNamespace; + } + + public override TypeFactory TypeFactory => _typeFactory ??= new TypeFactory(Library, typeof(BinaryData)); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ClientOptionsTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/ClientOptionsTypeProvider.cs new file mode 100644 index 0000000..384b0af --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ClientOptionsTypeProvider.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal sealed class ClientOptionsTypeProvider : TypeProvider + { + private static TextInfo TextInfo = CultureInfo.InvariantCulture.TextInfo; + + public FormattableString Description { get; } + public IReadOnlyList? ApiVersions { get; } + public IReadOnlyList AdditionalParameters { get; init; } + protected override string DefaultName { get; } + protected override string DefaultAccessibility { get; } + + public ClientOptionsTypeProvider(IReadOnlyList? versions, string name, string ns, FormattableString description, SourceInputModel? sourceInputModel) : base(ns, sourceInputModel) + { + DefaultName = name; + DefaultAccessibility = "public"; + Description = description; + + if (versions is not null) + ApiVersions = ConvertApiVersions(versions); + + AdditionalParameters = Array.Empty(); + } + + private static ApiVersion[] ConvertApiVersions(IReadOnlyList versions) => + versions.Select((v, i) => new ApiVersion(NormalizeVersion(v), $"Service version \"{v}\"", i + 1, v)).ToArray(); + + public record ApiVersion(string Name, string Description, int Value, string StringValue); + + internal static string NormalizeVersion(string version) => + TextInfo.ToTitleCase(new StringBuilder("V") + .Append(version.StartsWith("v", true, CultureInfo.InvariantCulture) ? version.Substring(1) : version) + .Replace('-', '_') + .Replace('.', '_') + .ToString()); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/EnumType.cs b/logger/autorest.csharp/common/Output/Models/Types/EnumType.cs new file mode 100644 index 0000000..aa4bfcc --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/EnumType.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class EnumType : TypeProvider + { + private readonly IEnumerable _allowedValues; + private readonly ModelTypeMapping? _typeMapping; + private readonly TypeFactory _typeFactory; + private IList? _values; + public EnumType(InputEnumType enumType, BuildContext context) + : this(enumType, GetDefaultModelNamespace(null, context.DefaultNamespace), enumType.Accessibility ?? "public", context.TypeFactory, context.SourceInputModel) + { + } + + public EnumType(InputEnumType input, string defaultNameSpace, string defaultAccessibility, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + : base(defaultNameSpace, sourceInputModel) + { + IsEnum = true; + _allowedValues = input.Values; + _typeFactory = typeFactory; + _deprecation = input.Deprecated; + + DefaultName = input.Name.ToCleanName(); + DefaultAccessibility = input.Accessibility ?? defaultAccessibility; + IsAccessibilityOverridden = input.Accessibility != null; + + var isExtensible = input.IsExtensible; + if (ExistingType != null) + { + isExtensible = ExistingType.TypeKind switch + { + TypeKind.Enum => false, + TypeKind.Struct => true, + _ => throw new InvalidOperationException( + $"{ExistingType.ToDisplayString()} cannot be mapped to enum," + + $" expected enum or struct got {ExistingType.TypeKind}") + }; + + _typeMapping = sourceInputModel?.CreateForModel(ExistingType); + } + + Description = string.IsNullOrWhiteSpace(input.Description) ? $"The {input.Name}." : input.Description; + IsExtensible = isExtensible; + ValueType = typeFactory.CreateType(input.ValueType); + IsStringValueType = ValueType.Equals(typeof(string)); + IsIntValueType = ValueType.Equals(typeof(int)) || ValueType.Equals(typeof(long)); + IsFloatValueType = ValueType.Equals(typeof(float)) || ValueType.Equals(typeof(double)); + IsNumericValueType = IsIntValueType || IsFloatValueType; + SerializationMethodName = IsStringValueType && IsExtensible ? "ToString" : $"ToSerial{ValueType.Name.FirstCharToUpperCase()}"; + } + + public CSharpType ValueType { get; } + public bool IsExtensible { get; } + public bool IsIntValueType { get; } + public bool IsFloatValueType { get; } + public bool IsStringValueType { get; } + public bool IsNumericValueType { get; } + public string SerializationMethodName { get; } + + public string? Description { get; } + protected override string DefaultName { get; } + protected override string DefaultAccessibility { get; } + protected override TypeKind TypeKind => IsExtensible ? TypeKind.Struct : TypeKind.Enum; + public bool IsAccessibilityOverridden { get; } + + public IList Values => _values ??= BuildValues(); + + private List BuildValues() + { + var values = new List(); + foreach (var value in _allowedValues) + { + var name = BuilderHelpers.DisambiguateName(Type.Name, value.Name.ToCleanName(), "Value"); + var existingMember = _typeMapping?.GetMemberByOriginalName(name); + values.Add(new EnumTypeValue( + BuilderHelpers.CreateMemberDeclaration(name, Type, "public", existingMember, _typeFactory), + CreateDescription(value), + BuilderHelpers.ParseConstant(value.Value, ValueType))); + } + + return values; + } + + private static string CreateDescription(InputEnumTypeValue value) + { + var description = string.IsNullOrWhiteSpace(value.Description) + ? value.GetValueString() + : value.Description; + return BuilderHelpers.EscapeXmlDocDescription(description); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/EnumTypeValue.cs b/logger/autorest.csharp/common/Output/Models/Types/EnumTypeValue.cs new file mode 100644 index 0000000..a537576 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/EnumTypeValue.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class EnumTypeValue + { + public EnumTypeValue(MemberDeclarationOptions declaration, string description, Constant value) + { + Description = description; + Value = value; + Declaration = declaration; + } + + public MemberDeclarationOptions Declaration { get; } + public Constant Value { get; } + public string Description { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ExpressionTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/ExpressionTypeProvider.cs new file mode 100644 index 0000000..c28a095 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ExpressionTypeProvider.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types.HelperTypeProviders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Types.System; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Models.Types +{ + // TODO -- eventually we should combine everything in this class into TypeProvider + internal abstract class ExpressionTypeProvider : TypeProvider + { + internal static IEnumerable GetHelperProviders() + { + yield return ChangeTrackingListProvider.Instance; + yield return OptionalTypeProvider.Instance; + yield return ArgumentProvider.Instance; + yield return ChangeTrackingDictionaryProvider.Instance; + yield return ModelSerializationExtensionsProvider.Instance; + if (!Configuration.IsBranded) + { + yield return ErrorResultProvider.Instance; + yield return ClientPipelineExtensionsProvider.Instance; + yield return ClientUriBuilderProvider.Instance; + } + yield return MultipartFormDataRequestContentProvider.Instance; + yield return RequestContentHelperProvider.Instance; + yield return Utf8JsonRequestContentProvider.Instance; + if (Configuration.EnableBicepSerialization) + { + yield return BicepSerializationTypeProvider.Instance; + } + } + + protected ExpressionTypeProvider(string defaultNamespace, SourceInputModel? sourceInputModel) + : base(defaultNamespace, sourceInputModel) + { + DeclarationModifiers = TypeSignatureModifiers.Partial | TypeSignatureModifiers.Public; + } + + public override bool IsEnum => TypeKind is TypeKind.Enum; + + public bool IsStruct => TypeKind is TypeKind.Struct; + + private IReadOnlyList? _usings; + public IReadOnlyList Usings => _usings ??= BuildUsings().ToArray(); + + public TypeSignatureModifiers DeclarationModifiers { get; protected init; } + + protected virtual IEnumerable BuildUsings() + { + yield break; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected override string DefaultAccessibility { get; } = "public"; + + public CSharpType? Inherits { get; protected init; } + + public virtual WhereExpression? WhereClause { get; protected init; } + + private IReadOnlyList? _implements; + public IReadOnlyList Implements => _implements ??= BuildImplements().ToArray(); + + private IReadOnlyList? _properties; + public IReadOnlyList Properties => _properties ??= BuildProperties().ToArray(); + + private IReadOnlyList? _methods; + public IReadOnlyList Methods => _methods ??= BuildMethods().ToArray(); + + private IReadOnlyList? _constructors; + public IReadOnlyList Constructors => _constructors ??= BuildConstructors().ToArray(); + + private IReadOnlyList? _fields; + public IReadOnlyList Fields => _fields ??= BuildFields().ToArray(); + + private IReadOnlyList? _nestedTypes; + public IReadOnlyList NestedTypes => _nestedTypes ??= BuildNestedTypes().ToArray(); + + protected virtual IEnumerable BuildProperties() + { + yield break; + } + + protected virtual IEnumerable BuildFields() + { + yield break; + } + + protected virtual IEnumerable BuildImplements() + { + yield break; + } + + protected virtual IEnumerable BuildMethods() + { + yield break; + } + + protected virtual IEnumerable BuildConstructors() + { + yield break; + } + + protected virtual IEnumerable BuildNestedTypes() + { + yield break; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/FlattenedObjectTypeProperty.cs b/logger/autorest.csharp/common/Output/Models/Types/FlattenedObjectTypeProperty.cs new file mode 100644 index 0000000..e7e62d1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/FlattenedObjectTypeProperty.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class FlattenedObjectTypeProperty : ObjectTypeProperty + { + // The flattened object type property does not participate in the serialization or deserialization process, therefore we pass in null for SchemaProperty. + internal FlattenedObjectTypeProperty(MemberDeclarationOptions declaration, string parameterDescription, ObjectTypeProperty underlyingProperty, bool isReadOnly, bool? includeGetterNullCheck, bool includeSetterNullCheck, string childPropertyName, bool isOverriddenValueType, CSharpType? valueType = null, bool optionalViaNullability = false) + : base(declaration, parameterDescription, isReadOnly, null, valueType, optionalViaNullability) + { + UnderlyingProperty = underlyingProperty; + IncludeGetterNullCheck = includeGetterNullCheck; + IncludeSetterNullCheck = includeSetterNullCheck; + IsUnderlyingPropertyNullable = underlyingProperty.IsReadOnly; + ChildPropertyName = childPropertyName; + IsOverriddenValueType = isOverriddenValueType; + } + + // This is not immutable therefore we have to build this everytime we call it + public override Stack BuildHierarchyStack() => GetHierarchyStack(UnderlyingProperty); + + public ObjectTypeProperty UnderlyingProperty { get; } + + public bool? IncludeGetterNullCheck { get; } + + public bool IncludeSetterNullCheck { get; } + + public bool IsUnderlyingPropertyNullable { get; } + + public bool IsOverriddenValueType { get; } + + public string ChildPropertyName { get; } + + public override string SerializedName => GetSerializedName(); + + public override IEnumerable? FlattenedNames => UnderlyingProperty.InputModelProperty?.FlattenedNames; + + private string GetSerializedName() + { + StringBuilder result = new(); + foreach (var property in BuildHierarchyStack()) + { + if (result.Length > 0) + { + result.Insert(0, '.'); + } + result.Insert(0, property.SerializedName); + } + return result.ToString(); + } + + internal static (bool IsReadOnly, bool? IncludeGetterNullCheck, bool IncludeSetterNullCheck) GetFlags(ObjectTypeProperty property, ObjectTypeProperty innerProperty) + { + if (!property.IsReadOnly && innerProperty.IsReadOnly) + { + if (HasDefaultPublicCtor(property.Declaration.Type)) + { + if (innerProperty.Declaration.Type.Arguments.Count > 0) + return (true, true, false); + else + return (true, false, false); + } + else + { + return (false, false, false); + } + } + else if (!property.IsReadOnly && !innerProperty.IsReadOnly) + { + if (HasDefaultPublicCtor(property.Declaration.Type)) + return (false, false, true); + else + return (false, false, false); + } + + return (true, null, false); + } + + internal static bool HasDefaultPublicCtor(CSharpType type) + { + if (type is not { IsFrameworkType: false, Implementation: ObjectType objType }) + return true; + + foreach (var ctor in objType.Constructors) + { + if (ctor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && !ctor.Signature.Parameters.Any()) + return true; + } + + return false; + } + + internal static Stack GetHierarchyStack(ObjectTypeProperty property) + { + var hierarchyStack = new Stack(); + var visited = new HashSet(); + hierarchyStack.Push(property); + visited.Add(property); + BuildHeirarchy(property, hierarchyStack, visited); + return hierarchyStack; + } + + private static void BuildHeirarchy(ObjectTypeProperty property, Stack heirarchyStack, HashSet visited) + { + //if we get back the same property exit early since this means we are getting into a loop of references + if (IsSinglePropertyObject(property, out var childProp) && !visited.Contains(childProp)) + { + heirarchyStack.Push(childProp); + visited.Add(childProp); + BuildHeirarchy(childProp, heirarchyStack, visited); + } + } + + public static bool IsSinglePropertyObject(ObjectTypeProperty property, [MaybeNullWhen(false)] out ObjectTypeProperty innerProperty) + { + innerProperty = null; + + if (property.Declaration.Type is not { IsFrameworkType: false, Implementation: ObjectType objType }) + return false; + + var properties = objType.EnumerateHierarchy().SelectMany(obj => obj.Properties).Where(property => property is not FlattenedObjectTypeProperty).ToArray(); + bool isSingleProperty = properties.Length == 1 && objType.Discriminator == null; + + if (isSingleProperty) + innerProperty = properties.First(); + + return isSingleProperty; + } + + internal static string GetCombinedPropertyName(ObjectTypeProperty innerProperty, ObjectTypeProperty immediateParentProperty) + { + var immediateParentPropertyName = GetPropertyName(immediateParentProperty.Declaration); + + if (innerProperty.Declaration.Type.Equals(typeof(bool)) || innerProperty.Declaration.Type.Equals(typeof(bool?))) + { + return innerProperty.Declaration.Name.Equals("Enabled", StringComparison.Ordinal) ? $"{immediateParentPropertyName}{innerProperty.Declaration.Name}" : innerProperty.Declaration.Name; + } + + if (innerProperty.Declaration.Name.Equals("Id", StringComparison.Ordinal)) + return $"{immediateParentPropertyName}{innerProperty.Declaration.Name}"; + + if (immediateParentPropertyName.EndsWith(innerProperty.Declaration.Name, StringComparison.Ordinal)) + return immediateParentPropertyName; + + var parentWords = immediateParentPropertyName.SplitByCamelCase(); + if (immediateParentPropertyName.EndsWith("Profile", StringComparison.Ordinal) || + immediateParentPropertyName.EndsWith("Policy", StringComparison.Ordinal) || + immediateParentPropertyName.EndsWith("Configuration", StringComparison.Ordinal) || + immediateParentPropertyName.EndsWith("Properties", StringComparison.Ordinal) || + immediateParentPropertyName.EndsWith("Settings", StringComparison.Ordinal)) + { + parentWords = parentWords.Take(parentWords.Count() - 1); + } + + var parentWordArray = parentWords.ToArray(); + var parentWordsHash = new HashSet(parentWordArray); + var nameWords = innerProperty.Declaration.Name.SplitByCamelCase().ToArray(); + var lastWord = string.Empty; + for (int i = 0; i < nameWords.Length; i++) + { + var word = nameWords[i]; + lastWord = word; + if (parentWordsHash.Contains(word)) + { + if (i == nameWords.Length - 2 && parentWordArray.Length >= 2 && word.Equals(parentWordArray[parentWordArray.Length - 2], StringComparison.Ordinal)) + { + parentWords = parentWords.Take(parentWords.Count() - 2); + break; + } + { + return innerProperty.Declaration.Name; + } + } + + //need to depluralize the last word and check + if (i == nameWords.Length - 1 && parentWordsHash.Contains(lastWord.ToSingular(false))) + return innerProperty.Declaration.Name; + } + + immediateParentPropertyName = string.Join("", parentWords); + + return $"{immediateParentPropertyName}{innerProperty.Declaration.Name}"; + } + + private static string GetPropertyName(MemberDeclarationOptions property) + { + const string properties = "Properties"; + if (property.Name.Equals(properties, StringComparison.Ordinal)) + { + string typeName = property.Type.Name; + int index = typeName.IndexOf(properties); + if (index > -1 && index + properties.Length == typeName.Length) + return typeName.Substring(0, index); + + return typeName; + } + return property.Name; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ArgumentProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ArgumentProvider.cs new file mode 100644 index 0000000..ac16e7f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ArgumentProvider.cs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ArgumentProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new ArgumentProvider()); + + private class Template { } + + private const string AssertNotNullMethodName = "AssertNotNull"; + private const string AssertNotNullOrEmptyMethodName = "AssertNotNullOrEmpty"; + private const string AssertNotNullOrWhiteSpaceMethodName = "AssertNotNullOrWhiteSpace"; + + private readonly CSharpType _t = typeof(Template<>).GetGenericArguments()[0]; + private readonly Parameter _nameParam = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + private readonly CSharpType _nullableT; + private readonly ParameterReference _nameParamRef; + + public static ArgumentProvider Instance => _instance.Value; + + private ArgumentProvider() : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + _nameParamRef = new ParameterReference(_nameParam); + _nullableT = _t.WithNullable(true); + } + + protected override string DefaultName => "Argument"; + + private MethodSignature GetSignature( + string name, + IReadOnlyList parameters, + IReadOnlyList? genericArguments = null, + IReadOnlyList? whereExpressions = null, + CSharpType? returnType = null) + { + return new MethodSignature( + name, + null, + null, + MethodSignatureModifiers.Static | MethodSignatureModifiers.Public, + returnType, + null, + parameters, + GenericArguments: genericArguments, + GenericParameterConstraints: whereExpressions); + } + + protected override IEnumerable BuildMethods() + { + yield return BuildAssertNotNull(); + yield return BuildAssertNotNullStruct(); + yield return BuildAssertNotNullOrEmptyCollection(); + yield return BuildAssertNotNullOrEmptyString(); + yield return BuildAssertNotNullOrWhiteSpace(); + yield return BuildAssertNotDefault(); + yield return BuildAssertInRange(); + yield return BuildAssertEnumDefined(); + yield return BuildCheckNotNull(); + yield return BuildCheckNotNullOrEmptyString(); + yield return BuildAssertNull(); + } + + private Method BuildAssertNull() + { + var valueParam = new Parameter("value", null, _t, null, ValidationType.None, null); + var messageParam = new Parameter("message", null, typeof(string), Constant.Default(new CSharpType(typeof(string), true)), ValidationType.None, null); + var signature = GetSignature("AssertNull", new[] { valueParam, _nameParam, messageParam }, new[] { _t }); + var value = new ParameterReference(valueParam); + var message = new ParameterReference(messageParam); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(NotEqual(value, Null)) + { + ThrowArgumentException(NullCoalescing(message, Literal("Value must be null."))) + } + }); + } + + private Method BuildCheckNotNullOrEmptyString() + { + var valueParam = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var signature = GetSignature("CheckNotNullOrEmpty", new[] { valueParam, _nameParam }, returnType: typeof(string)); + var value = new ParameterReference(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + AssertNotNullOrEmpty(value, _nameParamRef), + Return(value) + }); + } + + + private Method BuildCheckNotNull() + { + var valueParam = new Parameter("value", null, _t, null, ValidationType.None, null); + var signature = GetSignature("CheckNotNull", new[] { valueParam, _nameParam }, new[] { _t }, new[] { Where.Class(_t) }, _t); + var value = new ParameterReference(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + AssertNotNull(value, _nameParamRef), + Return(value) + }); + } + + private Method BuildAssertEnumDefined() + { + var valueParam = new Parameter("value", null, typeof(object), null, ValidationType.None, null); + var enumTypeParam = new Parameter("enumType", null, typeof(Type), null, ValidationType.None, null); + var signature = GetSignature("AssertEnumDefined", new[] { enumTypeParam, valueParam, _nameParam }); + var enumType = new ParameterReference(enumTypeParam); + var value = new ParameterReference(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(Not(new BoolExpression(new InvokeStaticMethodExpression(typeof(Enum), "IsDefined", new[] { enumType, value })))) + { + ThrowArgumentException(new FormattableStringExpression("Value not defined for {0}.", new MemberExpression(enumType, "FullName"))) + } + }); + } + + private Method BuildAssertInRange() + { + var valueParam = new Parameter("value", null, _t, null, ValidationType.None, null); + var minParam = new Parameter("minimum", null, _t, null, ValidationType.None, null); + var maxParam = new Parameter("maximum", null, _t, null, ValidationType.None, null); + var whereExpressions = new WhereExpression[] { Where.NotNull(_t).And(new CSharpType(typeof(IComparable<>), _t)) }; + var signature = GetSignature("AssertInRange", new[] { valueParam, minParam, maxParam, _nameParam }, new[] { _t }, whereExpressions); + var value = new ParameterReference(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(GreaterThan(GetCompareToExpression(new ParameterReference(minParam), value), Literal(0))) + { + Throw(New.ArgumentOutOfRangeException(_nameParamRef, "Value is less than the minimum allowed.", false)) + }, + new IfStatement(LessThan(GetCompareToExpression(new ParameterReference(maxParam), value), Literal(0))) + { + Throw(New.ArgumentOutOfRangeException(_nameParamRef, "Value is greater than the maximum allowed.", false)) + } + }); + } + + private ValueExpression GetCompareToExpression(ValueExpression left, ValueExpression right) + { + return left.Invoke("CompareTo", right); + } + + private Method BuildAssertNotDefault() + { + var valueParam = new Parameter("value", null, _t, null, ValidationType.None, null); + var whereExpressions = new WhereExpression[] { Where.Struct(_t).And(new CSharpType(typeof(IEquatable<>), _t)) }; + var signature = GetSignature("AssertNotDefault", new[] { valueParam.WithRef(), _nameParam }, new[] { _t }, whereExpressions); + var value = new ParameterReference(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(new BoolExpression(value.Invoke("Equals", Default))) + { + ThrowArgumentException("Value cannot be empty.") + } + }); + } + + private Method BuildAssertNotNullOrWhiteSpace() + { + var valueParam = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var signature = GetSignature(AssertNotNullOrWhiteSpaceMethodName, new[] { valueParam, _nameParam }); + var value = new StringExpression(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam), + new IfStatement(StringExpression.IsNullOrWhiteSpace(value)) + { + ThrowArgumentException("Value cannot be empty or contain only white-space characters.") + } + }); + } + + private Method BuildAssertNotNullOrEmptyString() + { + var valueParam = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var signature = GetSignature(AssertNotNullOrEmptyMethodName, new[] { valueParam, _nameParam }); + var value = new StringExpression(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam), + new IfStatement(Equal(value.Length, Literal(0))) + { + ThrowArgumentException("Value cannot be an empty string.") + } + }); + } + + private Method BuildAssertNotNullOrEmptyCollection() + { + const string throwMessage = "Value cannot be an empty collection."; + var valueParam = new Parameter("value", null, new CSharpType(typeof(IEnumerable<>), _t), null, ValidationType.None, null); + var signature = GetSignature(AssertNotNullOrEmptyMethodName, new[] { valueParam, _nameParam }, new[] { _t }); + return new Method(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam), + new IfStatement(IsCollectionEmpty(valueParam, new VariableReference(new CSharpType(typeof(ICollection<>), _t), new CodeWriterDeclaration("collectionOfT")))) + { + ThrowArgumentException(throwMessage) + }, + new IfStatement(IsCollectionEmpty(valueParam, new VariableReference(typeof(ICollection), new CodeWriterDeclaration("collection")))) + { + ThrowArgumentException(throwMessage) + }, + UsingDeclare("e", new CSharpType(typeof(IEnumerator<>), _t), new ParameterReference(valueParam).Invoke("GetEnumerator"), out var eVar), + new IfStatement(Not(new BoolExpression(eVar.Invoke("MoveNext")))) + { + ThrowArgumentException(throwMessage) + } + }); + } + + private static BoolExpression IsCollectionEmpty(Parameter valueParam, VariableReference collection) + { + return BoolExpression.Is(valueParam, new DeclarationExpression(collection, false)).And(Equal(new MemberExpression(collection, "Count"), Literal(0))); + } + + private MethodBodyStatement ThrowArgumentException(ValueExpression expression) + { + return Throw(New.ArgumentException(_nameParamRef, expression, false)); + } + + private MethodBodyStatement ThrowArgumentException(string message) => ThrowArgumentException(Literal(message)); + + private Method BuildAssertNotNullStruct() + { + var valueParam = new Parameter("value", null, _nullableT, null, ValidationType.None, null); + var signature = GetSignature(AssertNotNullMethodName, new[] { valueParam, _nameParam }, new[] { _t }, new[] { Where.Struct(_t) }); + var value = new ParameterReference(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(Not(new BoolExpression(new MemberExpression(value, "HasValue")))) + { + Throw(New.ArgumentNullException(_nameParamRef, false)) + } + }); + } + + private Method BuildAssertNotNull() + { + var valueParam = new Parameter("value", null, _t, null, ValidationType.None, null); + var signature = GetSignature(AssertNotNullMethodName, new[] { valueParam, _nameParam }, new[] { _t }); + return new Method(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam) + }); + } + + private IfStatement AssertNotNullSnippet(Parameter valueParam) + { + return new IfStatement(Is(new ParameterReference(valueParam), Null)) + { + Throw(New.ArgumentNullException(_nameParamRef, false)) + }; + } + + internal MethodBodyStatement AssertNotNull(ValueExpression variable, ValueExpression? name = null) + { + return new InvokeStaticMethodStatement(Type, AssertNotNullMethodName, variable, name ?? Nameof(variable)); + } + + internal MethodBodyStatement AssertNotNullOrEmpty(ValueExpression variable, ValueExpression? name = null) + { + return new InvokeStaticMethodStatement(Type, AssertNotNullOrEmptyMethodName, variable, name ?? Nameof(variable)); + } + + internal MethodBodyStatement AssertNotNullOrWhiteSpace(ValueExpression variable, ValueExpression? name = null) + { + return new InvokeStaticMethodStatement(Type, AssertNotNullOrWhiteSpaceMethodName, variable, name ?? Nameof(variable)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/BicepSerializationTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/BicepSerializationTypeProvider.cs new file mode 100644 index 0000000..426ec71 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/BicepSerializationTypeProvider.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class BicepSerializationTypeProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new BicepSerializationTypeProvider()); + public static BicepSerializationTypeProvider Instance => _instance.Value; + private const string AppendChildObjectMethodName = "AppendChildObject"; + + private readonly Parameter StringBuilderParameter = + new Parameter("stringBuilder", null, typeof(StringBuilder), null, ValidationType.None, null); + + private readonly Parameter IndentFirstLine = + new Parameter("indentFirstLine", null, typeof(bool), null, ValidationType.None, null); + + private readonly Parameter FormattedPropertyName = + new Parameter("formattedPropertyName", null, typeof(string), null, ValidationType.None, null); + + private readonly Parameter Spaces = + new Parameter("spaces", null, typeof(int), null, ValidationType.None, null); + + private readonly Parameter ChildObject = new Parameter("childObject", null, typeof(object), + null, ValidationType.None, null); + + private BicepSerializationTypeProvider() + : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + } + + protected override string DefaultName => "BicepSerializationHelpers"; + + protected override IEnumerable BuildMethods() + { + yield return new Method( + new MethodSignature( + AppendChildObjectMethodName, + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + null, + null, + new Parameter[] + { + new Parameter("stringBuilder", null, typeof(StringBuilder), null, ValidationType.None, + null), + ChildObject, + KnownParameters.Serializations.Options, + Spaces, + IndentFirstLine, + FormattedPropertyName + }), + WriteAppendChildObject()); + } + + private List WriteAppendChildObject() + { + var statements = new List(); + VariableReference indent = new VariableReference(typeof(string), "indent"); + statements.Add(Declare(indent, New.Instance(typeof(string), Literal(' '), Spaces))); + + VariableReference data = new VariableReference(typeof(BinaryData), "data"); + + var emptyObjectLength = new VariableReference(typeof(int), "emptyObjectLength"); + var length = new VariableReference(typeof(int), "length"); + var stringBuilder = new StringBuilderExpression(new ParameterReference(StringBuilderParameter)); + + statements.Add(Declare( + emptyObjectLength, + new BinaryOperatorExpression("+", + new BinaryOperatorExpression("+", + new BinaryOperatorExpression("+", + // 2 chars for open and close brace + new ConstantExpression(new Constant(2, typeof(int))), + Spaces), + // 2 new lines + EnvironmentExpression.NewLine().Property(nameof(string.Length))), + EnvironmentExpression.NewLine().Property(nameof(string.Length))))); + + statements.Add(Declare(length, stringBuilder.Property(nameof(StringBuilder.Length)))); + + var inMultilineString = new VariableReference(typeof(bool), "inMultilineString"); + statements.Add(Declare(inMultilineString, BoolExpression.False)); + statements.Add(EmptyLine); + + statements.Add(Declare( + data, + new InvokeStaticMethodExpression(typeof(ModelReaderWriter), nameof(ModelReaderWriter.Write), + new[] + { + new ParameterReference(ChildObject), + new ParameterReference(KnownParameters.Serializations.Options) + }))); + VariableReference lines = new VariableReference(typeof(string[]), "lines"); + statements.Add(Declare( + lines, + new InvokeInstanceMethodExpression( + new InvokeInstanceMethodExpression(data, nameof(ToString), Array.Empty(), null, + false), + nameof(string.Split), + new ValueExpression[] + { + new InvokeInstanceMethodExpression( + EnvironmentExpression.NewLine(), nameof(string.ToCharArray), + Array.Empty(), null, false), + new TypeReference(typeof(StringSplitOptions)).Property(nameof(StringSplitOptions.RemoveEmptyEntries)) + }, + null, + false) + )); + + var line = new VariableReference(typeof(string), "line"); + var i = new VariableReference(typeof(int), "i"); + statements.Add(new ForStatement(new AssignmentExpression(i, Int(0)), LessThan(i, lines.Property("Length")), new UnaryOperatorExpression("++", i, true)) + { + Declare(line, new IndexerExpression(lines, i)), + // if this is a multiline string, we do not apply the indentation, except for the first line containing only the ''' which is handled + // in the subsequent if statement + new IfStatement(new BoolExpression(inMultilineString)) + { + new IfStatement(new BoolExpression(line.Invoke(nameof(string.Contains), Literal("'''")))) + { + Assign(new BoolExpression(inMultilineString), BoolExpression.False) + }, + stringBuilder.AppendLine(line), + Continue + }, + new IfStatement(new BoolExpression(line.Invoke(nameof(string.Contains), Literal("'''")))) + { + Assign(new BoolExpression(inMultilineString), BoolExpression.True), + stringBuilder.AppendLine(new FormattableStringExpression("{0}{1}",indent, line)), + Continue + }, + new IfElseStatement( + And(Equal(i, Int(0)), + Not(new BoolExpression(IndentFirstLine))), + stringBuilder.AppendLine(new FormattableStringExpression("{0}", line)), + stringBuilder.AppendLine(new FormattableStringExpression("{0}{1}", indent, line))) + }); + + statements.Add(new IfStatement( + new BoolExpression( + Equal( + stringBuilder.Property(nameof(StringBuilder.Length)), + new BinaryOperatorExpression("+", length, emptyObjectLength)))) + { + Assign( + stringBuilder.Property(nameof(StringBuilder.Length)), + new BinaryOperatorExpression( + "-", + new BinaryOperatorExpression( + "-", + stringBuilder.Property(nameof(StringBuilder.Length)), + emptyObjectLength), + Literal(new StringExpression(FormattedPropertyName)).Property(nameof(string.Length)))) + }); + + return statements; + } + + internal InvokeStaticMethodStatement AppendChildObject( + ValueExpression stringBuilder, + ValueExpression expression, + ConstantExpression spaces, + BoolExpression isArrayElement, + StringExpression formattedPropertyName) + { + return new InvokeStaticMethodStatement( + Type, + "AppendChildObject", + new[] + { + stringBuilder, expression, KnownParameters.Serializations.Options, spaces, isArrayElement, + formattedPropertyName + }); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ChangeTrackingDictionaryProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ChangeTrackingDictionaryProvider.cs new file mode 100644 index 0000000..75e89f2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ChangeTrackingDictionaryProvider.cs @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ChangeTrackingDictionaryProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new ChangeTrackingDictionaryProvider(Configuration.HelperNamespace, null)); + public static ChangeTrackingDictionaryProvider Instance => _instance.Value; + + private class ChangeTrackingDictionaryTemplate { } + private readonly CSharpType _tKey = typeof(ChangeTrackingDictionaryTemplate<,>).GetGenericArguments()[0]; + private readonly CSharpType _tValue = typeof(ChangeTrackingDictionaryTemplate<,>).GetGenericArguments()[1]; + + private readonly Parameter _indexParam; + private readonly CSharpType _dictionary; + private readonly CSharpType _IDictionary; + private readonly CSharpType _IReadOnlyDictionary; + private readonly CSharpType _IEnumerator; + private readonly CSharpType _keyValuePair; + private readonly FieldDeclaration _innerDictionaryField; + private readonly DictionaryExpression _innerDictionary; + private readonly MethodSignature _ensureDictionarySignature; + + private InvokeInstanceMethodExpression EnsureDictionary { get; init; } + private BoolExpression IsUndefined { get; } = new BoolExpression(new MemberExpression(This, "IsUndefined")); + + private ChangeTrackingDictionaryProvider(string defaultNamespace, SourceInputModel? sourceInputModel) + : base(defaultNamespace, sourceInputModel) + { + DeclarationModifiers = TypeSignatureModifiers.Internal; + WhereClause = Where.NotNull(_tKey); + _indexParam = new Parameter("key", null, _tKey, null, ValidationType.None, null); + _IDictionary = new CSharpType(typeof(IDictionary<,>), _tKey, _tValue); + _dictionary = new CSharpType(typeof(Dictionary<,>), _tKey, _tValue); + _IReadOnlyDictionary = new CSharpType(typeof(IReadOnlyDictionary<,>), _tKey, _tValue); + _IEnumerator = new CSharpType(typeof(IEnumerator<>), new CSharpType(typeof(KeyValuePair<,>), _tKey, _tValue)); + _keyValuePair = new CSharpType(typeof(KeyValuePair<,>), _tKey, _tValue); + _innerDictionaryField = new FieldDeclaration(FieldModifiers.Private, new CSharpType(typeof(IDictionary<,>), _tKey, _tValue), "_innerDictionary"); + _innerDictionary = new DictionaryExpression(_tKey, _tValue, new VariableReference(_IDictionary, _innerDictionaryField.Declaration)); + _ensureDictionarySignature = new MethodSignature("EnsureDictionary", null, null, MethodSignatureModifiers.Public, _IDictionary, null, Array.Empty()); + EnsureDictionary = This.Invoke(_ensureDictionarySignature); + } + + protected override string DefaultName => "ChangeTrackingDictionary"; + + protected override IEnumerable BuildTypeArguments() + { + yield return _tKey; + yield return _tValue; + } + + protected override IEnumerable BuildFields() + { + yield return _innerDictionaryField; + } + + protected override IEnumerable BuildImplements() + { + yield return _IDictionary; + yield return _IReadOnlyDictionary; + } + + protected override IEnumerable BuildConstructors() + { + yield return DefaultConstructor(); + yield return ConstructorWithDictionary(); + yield return ConstructorWithReadOnlyDictionary(); + } + + private Method ConstructorWithReadOnlyDictionary() + { + var dicationaryParam = new Parameter("dictionary", null, _IReadOnlyDictionary, null, ValidationType.None, null); + var dictionary = new DictionaryExpression(_tKey, _tValue, dicationaryParam); + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new[] { dicationaryParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(Equal(dictionary, Null)) + { + Return() + }, + Assign(_innerDictionary, New.Instance(_dictionary)), + new ForeachStatement("pair", dictionary, out var pair) + { + _innerDictionary.Add(pair) + } + }); + } + + private Method ConstructorWithDictionary() + { + var dicationaryParam = new Parameter("dictionary", null, _IDictionary, null, ValidationType.None, null); + var dictionary = new ParameterReference(dicationaryParam); + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new[] { dicationaryParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(Equal(dictionary, Null)) + { + Return() + }, + Assign(_innerDictionary, New.Instance(_dictionary, dictionary)) + }); + } + + private Method DefaultConstructor() + { + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, Array.Empty()); + return new Method(signature, Array.Empty()); + } + + protected override IEnumerable BuildProperties() + { + yield return BuildIsUndefined(); + yield return BuildCount(); + yield return BuildIsReadOnly(); + yield return BuildKeys(); + yield return BuildValues(); + yield return BuildIndexer(); + yield return BuildEnumerableKeys(); + yield return BuildEnumerableValues(); + } + + private PropertyDeclaration BuildEnumerableValues() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.None, new CSharpType(typeof(IEnumerable<>), _tValue), "Values", new ExpressionPropertyBody( + new MemberExpression(This, "Values")), + null, + _IReadOnlyDictionary); + } + + private PropertyDeclaration BuildEnumerableKeys() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.None, new CSharpType(typeof(IEnumerable<>), _tKey), "Keys", new ExpressionPropertyBody( + new MemberExpression(This, "Keys")), + null, + _IReadOnlyDictionary); + } + + private PropertyDeclaration BuildIndexer() + { + var indexParam = new Parameter("key", null, _tKey, null, ValidationType.None, null); + return new IndexerDeclaration(null, MethodSignatureModifiers.Public, _tValue, "this", indexParam, new MethodPropertyBody( + new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(KeyNotFoundException), Nameof(new ParameterReference(_indexParam)))) + }, + Return(new ArrayElementExpression(EnsureDictionary, new ParameterReference(_indexParam))), + }, + new MethodBodyStatement[] + { + Assign( + new ArrayElementExpression(EnsureDictionary, new ParameterReference(_indexParam)), + new KeywordExpression("value", null)) + })); + } + + private PropertyDeclaration BuildValues() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, new CSharpType(typeof(ICollection<>), _tValue), "Values", + new ExpressionPropertyBody(new TernaryConditionalOperator( + IsUndefined, + new InvokeStaticMethodExpression(typeof(Array), "Empty", Array.Empty(), new[] { _tValue }), + new MemberExpression(EnsureDictionary, "Values")))); + } + + private PropertyDeclaration BuildKeys() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, new CSharpType(typeof(ICollection<>), _tKey), "Keys", + new ExpressionPropertyBody(new TernaryConditionalOperator( + IsUndefined, + new InvokeStaticMethodExpression(typeof(Array), "Empty", Array.Empty(), new[] { _tKey }), + new MemberExpression(EnsureDictionary, "Keys")))); + } + + private PropertyDeclaration BuildIsReadOnly() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, typeof(bool), "IsReadOnly", + new ExpressionPropertyBody(new TernaryConditionalOperator( + IsUndefined, + False, + new MemberExpression(EnsureDictionary, "IsReadOnly")))); + } + + private PropertyDeclaration BuildCount() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, typeof(int), "Count", + new ExpressionPropertyBody(new TernaryConditionalOperator( + IsUndefined, + Literal(0), + new MemberExpression(EnsureDictionary, "Count")))); + } + + private PropertyDeclaration BuildIsUndefined() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, typeof(bool), "IsUndefined", new ExpressionPropertyBody(Equal(_innerDictionary, Null))); + } + + private MethodSignature GetSignature( + string name, + CSharpType? returnType, + MethodSignatureModifiers modifiers = MethodSignatureModifiers.Public, + IReadOnlyList? parameters = null, + CSharpType? explicitImpl = null) + { + return new MethodSignature(name, null, null, modifiers, returnType, null, parameters ?? Array.Empty(), ExplicitInterface: explicitImpl); + } + + protected override IEnumerable BuildMethods() + { + yield return BuildGetEnumeratorGeneric(); + yield return BuildGetEnumerator(); + yield return BuildAddPair(); + yield return BuildClear(); + yield return BuildContains(); + yield return BuildCopyTo(); + yield return BuildRemovePair(); + yield return BuildAdd(); + yield return BuildContainsKey(); + yield return BuildRemoveKey(); + yield return BuildTryGetValue(); + yield return BuildEnsureDictionary(); + } + + private Method BuildTryGetValue() + { + var keyParam = new Parameter("key", null, _tKey, null, ValidationType.None, null); + var valueParam = new Parameter("value", null, _tValue, null, ValidationType.None, null, IsOut: true); + var value = new ParameterReference(valueParam); + var signature = GetSignature("TryGetValue", typeof(bool), parameters: new[] { keyParam, valueParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Assign(value, Default), + Return(False) + }, + Return(EnsureDictionary.Invoke("TryGetValue", new ParameterReference(keyParam), new KeywordExpression("out", value))) + }); + } + + private Method BuildRemoveKey() + { + var keyParam = new Parameter("key", null, _tKey, null, ValidationType.None, null); + var signature = GetSignature("Remove", typeof(bool), parameters: new[] { keyParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("Remove", new ParameterReference(keyParam))) + }); + } + + private Method BuildContainsKey() + { + var keyParam = new Parameter("key", null, _tKey, null, ValidationType.None, null); + var signature = GetSignature("ContainsKey", typeof(bool), parameters: new[] { keyParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("ContainsKey", new ParameterReference(keyParam))) + }); + } + + private Method BuildAdd() + { + var keyParam = new Parameter("key", null, _tKey, null, ValidationType.None, null); + var valueParam = new Parameter("value", null, _tValue, null, ValidationType.None, null); + var signature = GetSignature("Add", null, parameters: new[] { keyParam, valueParam }); + return new Method(signature, new MethodBodyStatement[] + { + EnsureDictionary.Invoke("Add", new ParameterReference(keyParam), new ParameterReference(valueParam)).ToStatement() + }); + } + + private Method BuildRemovePair() + { + var itemParam = new Parameter("item", null, _keyValuePair, null, ValidationType.None, null); + var item = new ParameterReference(itemParam); + var signature = GetSignature("Remove", typeof(bool), parameters: new[] { itemParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("Remove", item)) + }); + } + + private Method BuildCopyTo() + { + //TODO: This line will not honor the generic type of the array + var arrayParam = new Parameter("array", null, typeof(KeyValuePair<,>).MakeArrayType(), null, ValidationType.None, null); + var array = new ParameterReference(arrayParam); + var indexParam = new Parameter("index", null, typeof(int), null, ValidationType.None, null); + var index = new ParameterReference(indexParam); + var signature = GetSignature("CopyTo", null, parameters: new[] { arrayParam, indexParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return() + }, + EnsureDictionary.Invoke("CopyTo", array, index).ToStatement() + }); + } + + private Method BuildContains() + { + var itemParam = new Parameter("item", null, _keyValuePair, null, ValidationType.None, null); + var item = new ParameterReference(itemParam); + var signature = GetSignature("Contains", typeof(bool), parameters: new[] { itemParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("Contains", item)) + }); + } + + private Method BuildClear() + { + var signature = GetSignature("Clear", null); + return new Method(signature, new MethodBodyStatement[] + { + EnsureDictionary.Invoke("Clear").ToStatement() + }); + } + + private Method BuildAddPair() + { + var itemParam = new Parameter("item", null, _keyValuePair, null, ValidationType.None, null); + var item = new ParameterReference(itemParam); + var signature = GetSignature("Add", null, parameters: new[] { itemParam }); + return new Method(signature, new MethodBodyStatement[] + { + EnsureDictionary.Invoke("Add", item).ToStatement() + }); + } + + private Method BuildGetEnumerator() + { + var signature = GetSignature("GetEnumerator", typeof(IEnumerator), MethodSignatureModifiers.None, explicitImpl: typeof(IEnumerable)); + return new Method(signature, new MethodBodyStatement[] + { + Return(This.Invoke("GetEnumerator")) + }); + } + + private Method BuildGetEnumeratorGeneric() + { + var signature = GetSignature("GetEnumerator", _IEnumerator); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + new DeclareLocalFunctionStatement(new CodeWriterDeclaration("enumerateEmpty"), Array.Empty(), _IEnumerator, new KeywordStatement("yield", new KeywordExpression("break", null))), + Return(new InvokeStaticMethodExpression(null, "enumerateEmpty", Array.Empty())) + }, + Return(EnsureDictionary.Invoke("GetEnumerator")) + }); + } + + private Method BuildEnsureDictionary() + { + return new Method(_ensureDictionarySignature, new MethodBodyStatement[] + { + Return(new BinaryOperatorExpression("??=", _innerDictionary, New.Instance(_dictionary))) + }); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ChangeTrackingListProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ChangeTrackingListProvider.cs new file mode 100644 index 0000000..0529e94 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ChangeTrackingListProvider.cs @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ChangeTrackingListProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new ChangeTrackingListProvider()); + + private class ChangeTrackingListTemplate { } + + private readonly MethodSignature _ensureListSignature; + private readonly MethodSignature _getEnumeratorSignature; + private readonly CSharpType _t; + private readonly FieldDeclaration _innerListField; + private readonly CSharpType _tArray; + private readonly Parameter _tParam; + private readonly Parameter _indexParam = new Parameter("index", null, typeof(int), null, ValidationType.None, null); + private VariableReference _innerList; + private readonly CSharpType _iListOfT; + private readonly CSharpType _iReadOnlyListOfT; + + private BoolExpression IsUndefined { get; } = new BoolExpression(new MemberExpression(This, "IsUndefined")); + private InvokeInstanceMethodExpression EnsureList { get; init; } + + public static ChangeTrackingListProvider Instance => _instance.Value; + + private ChangeTrackingListProvider() : base(Configuration.HelperNamespace, null) + { + _t = typeof(ChangeTrackingListTemplate<>).GetGenericArguments()[0]; + _iListOfT = new CSharpType(typeof(IList<>), _t); + _iReadOnlyListOfT = new CSharpType(typeof(IReadOnlyList<>), _t); + + _ensureListSignature = new MethodSignature("EnsureList", null, null, MethodSignatureModifiers.Public, _iListOfT, null, Array.Empty()); + _getEnumeratorSignature = new MethodSignature("GetEnumerator", null, null, MethodSignatureModifiers.Public, new CSharpType(typeof(IEnumerator<>), _t), null, Array.Empty()); + _innerListField = new FieldDeclaration(FieldModifiers.Private, _iListOfT, "_innerList"); + _innerList = new VariableReference(_iListOfT, _innerListField.Declaration); + _tArray = typeof(ChangeTrackingListTemplate<>).GetGenericArguments()[0].MakeArrayType(); + _tParam = new Parameter("item", null, _t, null, ValidationType.None, null); + DeclarationModifiers = TypeSignatureModifiers.Internal; + EnsureList = This.Invoke(_ensureListSignature); + } + + protected override string DefaultName => "ChangeTrackingList"; + + protected override IEnumerable BuildConstructors() + { + yield return new Method(new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, Array.Empty()), EmptyStatement); + var iListParam = new Parameter("innerList", null, _iListOfT, null, ValidationType.None, null); + var iListSignature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new Parameter[] { iListParam }); + var iListVariable = new ParameterReference(iListParam); + var iListBody = new MethodBodyStatement[] + { + new IfStatement(NotEqual(iListVariable, Null)) + { + new AssignValueStatement(_innerList, iListVariable) + } + }; + + yield return new Method(iListSignature, iListBody); + var iReadOnlyListParam = new Parameter("innerList", null, _iReadOnlyListOfT, null, ValidationType.None, null); + var iReadOnlyListSignature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new Parameter[] { iReadOnlyListParam }); + var iReadOnlyListVariable = new ParameterReference(iReadOnlyListParam); + var iReadOnlyListBody = new MethodBodyStatement[] + { + new IfStatement(NotEqual(iReadOnlyListVariable, Null)) + { + new AssignValueStatement(_innerList, Linq.ToList(iReadOnlyListVariable)) + } + }; + + yield return new Method(iReadOnlyListSignature, iReadOnlyListBody); + } + + protected override IEnumerable BuildTypeArguments() + { + yield return _t; + } + + protected override IEnumerable BuildImplements() + { + yield return _iListOfT; + yield return _iReadOnlyListOfT; + } + + protected override IEnumerable BuildFields() + { + yield return _innerListField; + } + + protected override PropertyDeclaration[] BuildProperties() => + new[] + { + new PropertyDeclaration(null, MethodSignatureModifiers.Public, typeof(bool), "IsUndefined", new ExpressionPropertyBody(Equal(_innerList, Null))), + BuildCount(), + BuildIsReadOnly(), + BuildIndexer() + }; + + private PropertyDeclaration BuildIsReadOnly() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, typeof(bool), "IsReadOnly", + new ExpressionPropertyBody(new TernaryConditionalOperator( + IsUndefined, + False, + new MemberExpression(EnsureList, "IsReadOnly")))); + } + + private PropertyDeclaration BuildCount() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public, typeof(int), "Count", + new ExpressionPropertyBody(new TernaryConditionalOperator( + IsUndefined, + Literal(0), + new MemberExpression(EnsureList, "Count")))); + } + + private PropertyDeclaration BuildIndexer() + { + var indexParam = new Parameter("index", null, typeof(int), null, ValidationType.None, null); + return new IndexerDeclaration(null, MethodSignatureModifiers.Public, _t, "this", indexParam, new MethodPropertyBody( + new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(ArgumentOutOfRangeException), Nameof(new ParameterReference(_indexParam)))) + }, + Return(new ArrayElementExpression(EnsureList, new ParameterReference(_indexParam))), + }, + new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(ArgumentOutOfRangeException), Nameof(new ParameterReference(_indexParam)))) + }, + new AssignValueStatement( + new ArrayElementExpression(EnsureList, new ParameterReference(_indexParam)), + new KeywordExpression("value", null)) + })); + } + + protected override IEnumerable BuildMethods() + { + yield return BuildReset(); + yield return BuildGetEnumeratorOfT(); + yield return BuildGetEnumerator(); + yield return BuildAdd(); + yield return BuildClear(); + yield return BuildContains(); + yield return BuildCopyTo(); + yield return BuildRemove(); + yield return BuildIndexOf(); + yield return BuildInsert(); + yield return BuildRemoveAt(); + yield return BuildEnsureList(); + } + + private Method BuildRemoveAt() + { + var indexVariable = new ParameterReference(_indexParam); + return new Method(new MethodSignature("RemoveAt", null, null, MethodSignatureModifiers.Public, null, null, new Parameter[] { _indexParam }), new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(ArgumentOutOfRangeException), Nameof(indexVariable))) + }, + new InvokeInstanceMethodStatement(EnsureList, "RemoveAt", new ValueExpression[] { indexVariable }, false) + }); + } + + private Method BuildInsert() + { + return new Method(new MethodSignature("Insert", null, null, MethodSignatureModifiers.Public, null, null, new Parameter[] { _indexParam, _tParam }), new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(EnsureList, "Insert", new ValueExpression[] { new ParameterReference(_indexParam), new ParameterReference(_tParam) }, false) + }); + } + + private Method BuildIndexOf() + { + var signature = new MethodSignature("IndexOf", null, null, MethodSignatureModifiers.Public, typeof(int), null, new Parameter[] { _tParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(Literal(-1)) + }, + Return(EnsureList.Invoke(signature)) + }); + } + + private Method BuildRemove() + { + var signature = new MethodSignature("Remove", null, null, MethodSignatureModifiers.Public, typeof(bool), null, new Parameter[] { _tParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureList.Invoke(signature)) + }); + } + + private Method BuildCopyTo() + { + var arrayParam = new Parameter("array", null, _tArray, null, ValidationType.None, null); + var arrayIndexParam = new Parameter("arrayIndex", null, typeof(int), null, ValidationType.None, null); + return new Method(new MethodSignature("CopyTo", null, null, MethodSignatureModifiers.Public, null, null, new Parameter[] { arrayParam, arrayIndexParam }), new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return() + }, + new InvokeInstanceMethodStatement(EnsureList, "CopyTo", new ValueExpression[] { new ParameterReference(arrayParam), new ParameterReference(arrayIndexParam) }, false) + }); + } + + private Method BuildContains() + { + var signature = new MethodSignature("Contains", null, null, MethodSignatureModifiers.Public, typeof(bool), null, new Parameter[] { _tParam }); + return new Method(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureList.Invoke(signature)) + }); + } + + private Method BuildClear() + { + return new Method(new MethodSignature("Clear", null, null, MethodSignatureModifiers.Public, null, null, Array.Empty()), new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(EnsureList, "Clear") + }); + } + + private Method BuildAdd() + { + var genericParameter = new Parameter("item", null, _t, null, ValidationType.None, null); + return new Method(new MethodSignature("Add", null, null, MethodSignatureModifiers.Public, null, null, new Parameter[] { genericParameter }), new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(EnsureList, "Add", new ParameterReference(genericParameter)) + }); + } + + private Method BuildGetEnumerator() + { + return new Method(new MethodSignature("GetEnumerator", null, null, MethodSignatureModifiers.None, typeof(IEnumerator), null, Array.Empty(), ExplicitInterface: typeof(IEnumerable)), new MethodBodyStatement[] + { + Return(This.Invoke(_getEnumeratorSignature)) + }); + } + + private Method BuildGetEnumeratorOfT() + { + return new Method(_getEnumeratorSignature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + new DeclareLocalFunctionStatement(new CodeWriterDeclaration("enumerateEmpty"), Array.Empty(), new CSharpType(typeof(IEnumerator<>), _t), new KeywordStatement("yield", new KeywordExpression("break", null))), + Return(new InvokeStaticMethodExpression(null, "enumerateEmpty", Array.Empty())) + }, + Return(EnsureList.Invoke(_getEnumeratorSignature)) + }); + } + + private Method BuildReset() + { + return new Method(new MethodSignature("Reset", null, null, MethodSignatureModifiers.Public, null, null, Array.Empty()), new MethodBodyStatement[] + { + Assign(_innerList, Null) + }); + } + + private Method BuildEnsureList() + { + return new Method(_ensureListSignature, new MethodBodyStatement[] + { + Return(new BinaryOperatorExpression("??=", _innerList, New.Instance(new CSharpType(typeof(List<>), _t)))) + }); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ModelSerializationExtensionsProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ModelSerializationExtensionsProvider.cs new file mode 100644 index 0000000..0f8b0e6 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/ModelSerializationExtensionsProvider.cs @@ -0,0 +1,685 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ModelSerializationExtensionsProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new Lazy(() => new ModelSerializationExtensionsProvider()); + public static ModelSerializationExtensionsProvider Instance => _instance.Value; + private class WriteObjectValueTemplate { } + + private readonly CSharpType _t = typeof(WriteObjectValueTemplate<>).GetGenericArguments()[0]; + + private readonly MethodSignatureModifiers _methodModifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension; + private readonly TypeFormattersProvider _typeFormattersProvider; + + public ModelSerializationExtensionsProvider() : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + _typeFormattersProvider = new TypeFormattersProvider(this); + + _wireOptionsField = new FieldDeclaration( + modifiers: FieldModifiers.Internal | FieldModifiers.Static | FieldModifiers.ReadOnly, + type: typeof(ModelReaderWriterOptions), + name: _wireOptionsName) + { + InitializationValue = New.Instance(typeof(ModelReaderWriterOptions), Literal("W")) + }; + + if (Configuration.EnableInternalRawData) + { + // when we have this configuration, we have a sentinel value for raw data values to let us skip values + _sentinelBinaryDataField = new FieldDeclaration( + modifiers: FieldModifiers.Internal | FieldModifiers.Static | FieldModifiers.ReadOnly, + type: typeof(BinaryData), + name: _sentinelBinaryDataName) + { + InitializationValue = BinaryDataExpression.FromBytes( + // since this is a hard-coded value, we can just hard-code the bytes directly instead of + // using the BinaryData.FromObjectAsJson method + new InvokeInstanceMethodExpression( + LiteralU8("\"__EMPTY__\""), "ToArray", [], null, false)) + }; + } + } + + private const string _wireOptionsName = "WireOptions"; + private readonly FieldDeclaration _wireOptionsField; + private const string _sentinelBinaryDataName = "SentinelValue"; + private readonly FieldDeclaration? _sentinelBinaryDataField; + + private ModelReaderWriterOptionsExpression? _wireOptions; + public ModelReaderWriterOptionsExpression WireOptions => _wireOptions ??= new ModelReaderWriterOptionsExpression(new MemberExpression(Type, _wireOptionsName)); + + protected override string DefaultName => "ModelSerializationExtensions"; + + protected override IEnumerable BuildFields() + { + yield return _wireOptionsField; + + if (_sentinelBinaryDataField != null) + { + yield return _sentinelBinaryDataField; + } + } + + protected override IEnumerable BuildMethods() + { + #region JsonElementExtensions + yield return BuildGetObjectMethod(); + yield return BuildGetBytesFromBase64(); + yield return BuildGetDateTimeOffsetMethod(); + yield return BuildGetTimeSpanMethod(); + yield return BuildGetCharMethod(); + yield return BuildThrowNonNullablePropertyIsNullMethod(); + yield return BuildGetRequiredStringMethod(); + #endregion + + #region Utf8JsonWriterExtensions + foreach (var method in BuildWriteStringValueMethods()) + { + yield return method; + } + yield return BuildWriteBase64StringValueMethod(); + yield return BuildWriteNumberValueMethod(); + yield return BuildWriteObjectValueMethodGeneric(); + yield return BuildWriteObjectValueMethod(); + #endregion + + #region Sentinel Value related methods + if (_sentinelBinaryDataField != null) + { + var valueParameter = new Parameter("value", null, typeof(BinaryData), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _isSentinelValueMethodName, + Modifiers: MethodSignatureModifiers.Static | MethodSignatureModifiers.Internal, + ReturnType: typeof(bool), + Parameters: new[] { valueParameter }, + Summary: null, Description: null, ReturnDescription: null); + + var sentinelValue = new BinaryDataExpression(_sentinelBinaryDataField); + var value = new BinaryDataExpression(valueParameter); + var indexer = new VariableReference(typeof(int), "i"); + var body = new MethodBodyStatement[] + { + Declare("sentinelSpan", new TypedValueExpression(typeof(ReadOnlySpan), sentinelValue.ToMemory().Property(nameof(ReadOnlyMemory.Span))), out var sentinelSpan), + Declare("valueSpan", new TypedValueExpression(typeof(ReadOnlySpan), value.ToMemory().Property(nameof(ReadOnlyMemory.Span))), out var valueSpan), + Return(new InvokeStaticMethodExpression(typeof(MemoryExtensions), nameof(MemoryExtensions.SequenceEqual), new[] { sentinelSpan, valueSpan }, CallAsExtension: true)), + }; + + yield return new(signature, body); + } + #endregion + } + + private const string _isSentinelValueMethodName = "IsSentinelValue"; + + #region JsonElementExtensions method builders + private const string _getBytesFromBase64MethodName = "GetBytesFromBase64"; + private const string _getCharMethodName = "GetChar"; + private const string _getDateTimeOffsetMethodName = "GetDateTimeOffset"; + private const string _getObjectMethodName = "GetObject"; + private const string _getTimeSpanMethodName = "GetTimeSpan"; + private const string _throwNonNullablePropertyIsNullMethodName = "ThrowNonNullablePropertyIsNull"; + private const string _getRequiredStringMethodName = "GetRequiredString"; + + private readonly Parameter _formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + private readonly Parameter _propertyParameter = new Parameter("property", null, typeof(JsonProperty), null, ValidationType.None, null); + + private Method BuildGetObjectMethod() + { + var signature = new MethodSignature( + Name: _getObjectMethodName, + Summary: null, + Description: null, + Modifiers: _methodModifiers, + ReturnType: typeof(object), + ReturnDescription: null, + Parameters: new[] { KnownParameters.Serializations.JsonElement }); + var element = new JsonElementExpression(KnownParameters.Serializations.JsonElement); + var body = new SwitchStatement(element.ValueKind) + { + new(JsonValueKindExpression.String, Return(element.GetString())), + new(JsonValueKindExpression.Number, new MethodBodyStatement[] + { + new IfStatement(element.TryGetInt32(out var intValue)) + { + Return(intValue) + }, + new IfStatement(element.TryGetInt64(out var longValue)) + { + Return(longValue) + }, + Return(element.GetDouble()) + }), + new(JsonValueKindExpression.True, Return(True)), + new(JsonValueKindExpression.False, Return(False)), + new(new ValueExpression[] { JsonValueKindExpression.Undefined, JsonValueKindExpression.Null }, Return(Null)), + new(JsonValueKindExpression.Object, new MethodBodyStatement[] + { + Var("dictionary", New.Dictionary(typeof(string), typeof(object)), out var dictionary), + new ForeachStatement("jsonProperty", element.EnumerateObject(), out var jsonProperty) + { + dictionary.Add(jsonProperty.Property(nameof(JsonProperty.Name)), new JsonElementExpression(jsonProperty.Property(nameof(JsonProperty.Value))).GetObject()) + }, + Return(dictionary) + }), + new(JsonValueKindExpression.Array, new MethodBodyStatement[] + { + Var("list", New.List(typeof(object)), out var list), + new ForeachStatement("item", element.EnumerateArray(), out var item) + { + list.Add(new JsonElementExpression(item).GetObject()) + }, + Return(list.ToArray()) + }), + SwitchCase.Default(Throw(New.NotSupportedException(new FormattableStringExpression("Not supported value kind {0}", element.ValueKind)))) + }; + return new Method(signature, body); + } + + public InvokeStaticMethodExpression GetObject(JsonElementExpression element) + => new InvokeStaticMethodExpression(Type, _getObjectMethodName, new ValueExpression[] { element }, CallAsExtension: true); + + private Method BuildGetBytesFromBase64() + { + var signature = new MethodSignature( + Name: _getBytesFromBase64MethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.JsonElement, _formatParameter }, + ReturnType: typeof(byte[]), + Summary: null, Description: null, ReturnDescription: null); + var element = new JsonElementExpression(KnownParameters.Serializations.JsonElement); + var format = new StringExpression(_formatParameter); + var body = new MethodBodyStatement[] + { + new IfStatement(element.ValueKindEqualsNull()) + { + Return(Null) + }, + EmptyLine, + Return(new SwitchExpression(format, + new SwitchCaseExpression(Literal("U"), _typeFormattersProvider.FromBase64UrlString(GetRequiredString(element))), + new SwitchCaseExpression(Literal("D"), element.GetBytesFromBase64()), + SwitchCaseExpression.Default(ThrowExpression(New.ArgumentException(format, new FormattableStringExpression("Format is not supported: '{0}'", format)))) + )) + }; + + return new Method(signature, body); + } + + public InvokeStaticMethodExpression GetBytesFromBase64(JsonElementExpression element, string? format) + => new InvokeStaticMethodExpression(Type, _getBytesFromBase64MethodName, new ValueExpression[] { element, Literal(format) }, CallAsExtension: true); + + private Method BuildGetDateTimeOffsetMethod() + { + var signature = new MethodSignature( + Name: _getDateTimeOffsetMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.JsonElement, _formatParameter }, + ReturnType: typeof(DateTimeOffset), + Summary: null, Description: null, ReturnDescription: null); + var element = new JsonElementExpression(KnownParameters.Serializations.JsonElement); + var format = new StringExpression(_formatParameter); + var body = new SwitchExpression(format, + SwitchCaseExpression.When(Literal("U"), Equal(element.ValueKind, JsonValueKindExpression.Number), DateTimeOffsetExpression.FromUnixTimeSeconds(element.GetInt64())), + // relying on the param check of the inner call to throw ArgumentNullException if GetString() returns null + SwitchCaseExpression.Default(_typeFormattersProvider.ParseDateTimeOffset(element.GetString(), format)) + ); + + return new Method(signature, body); + } + + public InvokeStaticMethodExpression GetDateTimeOffset(JsonElementExpression element, string? format) + => new InvokeStaticMethodExpression(Type, _getDateTimeOffsetMethodName, new ValueExpression[] { element, Literal(format) }, CallAsExtension: true); + + private Method BuildGetTimeSpanMethod() + { + var signature = new MethodSignature( + Name: _getTimeSpanMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.JsonElement, _formatParameter }, + ReturnType: typeof(TimeSpan), + Summary: null, Description: null, ReturnDescription: null); + var element = new JsonElementExpression(KnownParameters.Serializations.JsonElement); + // relying on the param check of the inner call to throw ArgumentNullException if GetString() returns null + var body = _typeFormattersProvider.ParseTimeSpan(element.GetString(), _formatParameter); + + return new Method(signature, body); + } + + public InvokeStaticMethodExpression GetTimeSpan(JsonElementExpression element, string? format) + => new InvokeStaticMethodExpression(Type, _getTimeSpanMethodName, new ValueExpression[] { element, Literal(format) }, CallAsExtension: true); + + private Method BuildGetCharMethod() + { + var signature = new MethodSignature( + Name: _getCharMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.JsonElement }, + ReturnType: typeof(char), + Summary: null, Description: null, ReturnDescription: null); + var element = new JsonElementExpression(KnownParameters.Serializations.JsonElement); + var body = new IfElseStatement( + element.ValueKindEqualsString(), + new MethodBodyStatement[] + { + Var("text", element.GetString(), out var text), + new IfStatement(Equal(text, Null).Or(NotEqual(text.Length, Literal(1)))) + { + Throw(New.NotSupportedException(new FormattableStringExpression("Cannot convert \\\"{0}\\\" to a char", text))) + }, + Return(text.Index(0)) + }, + Throw(New.NotSupportedException(new FormattableStringExpression("Cannot convert {0} to a char", element.ValueKind))) + ); + + return new Method(signature, body); + } + + public InvokeStaticMethodExpression GetChar(JsonElementExpression element) + => new InvokeStaticMethodExpression(Type, _getCharMethodName, new ValueExpression[] { element }, CallAsExtension: true); + + private Method BuildThrowNonNullablePropertyIsNullMethod() + { + var signature = new MethodSignature( + Name: _throwNonNullablePropertyIsNullMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { _propertyParameter }, + ReturnType: null, + Attributes: new[] + { + new CSharpAttribute(typeof(ConditionalAttribute), Literal("DEBUG")) + }, + Summary: null, Description: null, ReturnDescription: null); + var property = new JsonPropertyExpression(_propertyParameter); + var body = Throw(New.JsonException(new FormattableStringExpression("A property '{0}' defined as non-nullable but received as null from the service. This exception only happens in DEBUG builds of the library and would be ignored in the release build", property.Name))); + + return new Method(signature, body); + } + + public MethodBodyStatement ThrowNonNullablePropertyIsNull(JsonPropertyExpression property) + => new InvokeStaticMethodStatement(Type, _throwNonNullablePropertyIsNullMethodName, new[] { property }, CallAsExtension: true); + + private Method BuildGetRequiredStringMethod() + { + var signature = new MethodSignature( + Name: _getRequiredStringMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.JsonElement }, + ReturnType: typeof(string), + Summary: null, Description: null, ReturnDescription: null); + var element = new JsonElementExpression(KnownParameters.Serializations.JsonElement); + var body = new MethodBodyStatement[] + { + Var("value", element.GetString(), out var value), + new IfStatement(Equal(value, Null)) + { + Throw(New.InvalidOperationException(new FormattableStringExpression("The requested operation requires an element of type 'String', but the target element has type '{0}'.", element.ValueKind))) + }, + Return(value) + }; + + return new Method(signature, body); + } + + public InvokeStaticMethodExpression GetRequiredString(JsonElementExpression element) + => new InvokeStaticMethodExpression(Type, _getRequiredStringMethodName, new[] { element }, CallAsExtension: true); + #endregion + + #region Utf8JsonWriterExtensions method builders + private const string _writeStringValueMethodName = "WriteStringValue"; + private const string _writeBase64StringValueMethodName = "WriteBase64StringValue"; + private const string _writeNumberValueMethodName = "WriteNumberValue"; + private const string _writeObjectValueMethodName = "WriteObjectValue"; + + private IEnumerable BuildWriteStringValueMethods() + { + var writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + var dateTimeOffsetValueParameter = new Parameter("value", null, typeof(DateTimeOffset), null, ValidationType.None, null); + yield return new Method( + new MethodSignature( + Name: _writeStringValueMethodName, + Modifiers: _methodModifiers, + ReturnType: null, + Parameters: new[] { KnownParameters.Serializations.Utf8JsonWriter, dateTimeOffsetValueParameter, _formatParameter }, + Summary: null, Description: null, ReturnDescription: null), + writer.WriteStringValue(_typeFormattersProvider.ToString(dateTimeOffsetValueParameter, _formatParameter)) + ); + + var dateTimeValueParameter = new Parameter("value", null, typeof(DateTime), null, ValidationType.None, null); + yield return new Method( + new MethodSignature( + Name: _writeStringValueMethodName, + Modifiers: _methodModifiers, + ReturnType: null, + Parameters: new[] { KnownParameters.Serializations.Utf8JsonWriter, dateTimeValueParameter, _formatParameter }, + Summary: null, Description: null, ReturnDescription: null), + writer.WriteStringValue(_typeFormattersProvider.ToString(dateTimeValueParameter, _formatParameter)) + ); + + var timeSpanValueParameter = new Parameter("value", null, typeof(TimeSpan), null, ValidationType.None, null); + yield return new Method( + new MethodSignature( + Name: _writeStringValueMethodName, + Modifiers: _methodModifiers, + ReturnType: null, + Parameters: new[] { KnownParameters.Serializations.Utf8JsonWriter, timeSpanValueParameter, _formatParameter }, + Summary: null, Description: null, ReturnDescription: null), + writer.WriteStringValue(_typeFormattersProvider.ToString(timeSpanValueParameter, _formatParameter)) + ); + + var charValueParameter = new Parameter("value", null, typeof(char), null, ValidationType.None, null); + var value = new CharExpression(charValueParameter); + yield return new Method( + new MethodSignature( + Name: _writeStringValueMethodName, + Modifiers: _methodModifiers, + ReturnType: null, + Parameters: new[] { KnownParameters.Serializations.Utf8JsonWriter, charValueParameter }, + Summary: null, Description: null, ReturnDescription: null), + writer.WriteStringValue(value.InvokeToString(new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture)))) + ); + } + + public MethodBodyStatement WriteStringValue(Utf8JsonWriterExpression writer, ValueExpression value, string? format) + => new InvokeStaticMethodStatement(Type, _writeStringValueMethodName, new[] { writer, value, Literal(format) }, CallAsExtension: true); + + private Method BuildWriteBase64StringValueMethod() + { + var valueParameter = new Parameter("value", null, typeof(byte[]), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _writeBase64StringValueMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.Utf8JsonWriter, valueParameter, _formatParameter }, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + var writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + var value = (ValueExpression)valueParameter; + var format = new StringExpression(_formatParameter); + var body = new MethodBodyStatement[] + { + new IfStatement(Equal(value, Null)) + { + writer.WriteNullValue(), + Return() + }, + new SwitchStatement(format) + { + new(Literal("U"), new MethodBodyStatement[] + { + writer.WriteStringValue(_typeFormattersProvider.ToBase64UrlString(value)), + Break + }), + new(Literal("D"), new MethodBodyStatement[] + { + writer.WriteBase64StringValue(value), + Break + }), + SwitchCase.Default(Throw(New.ArgumentException(format, new FormattableStringExpression("Format is not supported: '{0}'", format)))) + } + }; + + return new Method(signature, body); + } + + public MethodBodyStatement WriteBase64StringValue(Utf8JsonWriterExpression writer, ValueExpression value, string? format) + => new InvokeStaticMethodStatement(Type, _writeBase64StringValueMethodName, new[] { writer, value, Literal(format) }, CallAsExtension: true); + + private Method BuildWriteNumberValueMethod() + { + var valueParameter = new Parameter("value", null, typeof(DateTimeOffset), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _writeNumberValueMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { KnownParameters.Serializations.Utf8JsonWriter, valueParameter, _formatParameter }, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + var writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + var value = new DateTimeOffsetExpression(valueParameter); + var format = new StringExpression(_formatParameter); + var body = new MethodBodyStatement[] + { + new IfStatement(NotEqual(format, Literal("U"))) + { + Throw(New.ArgumentOutOfRangeException(format, "Only 'U' format is supported when writing a DateTimeOffset as a Number.")), + }, + writer.WriteNumberValue(value.ToUnixTimeSeconds()) + }; + + return new Method(signature, body); + } + + public MethodBodyStatement WriteNumberValue(Utf8JsonWriterExpression writer, ValueExpression value, string? format) + => new InvokeStaticMethodStatement(Type, _writeNumberValueMethodName, new[] { writer, value, Literal(format) }, CallAsExtension: true); + + private Method BuildWriteObjectValueMethod() + { + ValueExpression value; + Utf8JsonWriterExpression writer; + ParameterReference options; + MethodSignature signature = GetWriteObjectValueMethodSignature(null, out value, out writer, out options); + return new Method(signature, new MethodBodyStatement[] + { + writer.WriteObjectValue(new TypedValueExpression(typeof(object), value), options) + }); + } + + private Method BuildWriteObjectValueMethodGeneric() + { + ValueExpression value; + Utf8JsonWriterExpression writer; + ParameterReference options; + MethodSignature signature = GetWriteObjectValueMethodSignature(_t, out value, out writer, out options); + List cases = new List + { + new(Null, new MethodBodyStatement[] + { + writer.WriteNullValue(), + Break + }) + }; + if (Configuration.UseModelReaderWriter) + { + cases.Add( + BuildWriteObjectValueSwitchCase(new CSharpType(typeof(IJsonModel<>), _t), "jsonModel", jsonModel => new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(jsonModel, nameof(IJsonModel.Write), writer, NullCoalescing(options, ModelReaderWriterOptionsExpression.Wire)), + Break + })); + } + if (Configuration.IsBranded) + { + cases.Add( + BuildWriteObjectValueSwitchCase(typeof(IUtf8JsonSerializable), "serializable", serializable => new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(serializable, nameof(IUtf8JsonSerializable.Write), writer), + Break + })); + } + cases.AddRange(new[] + { + // byte[] case + BuildWriteObjectValueSwitchCase(typeof(byte[]), "bytes", bytes => new MethodBodyStatement[] + { + writer.WriteBase64StringValue(bytes), + Break + }), + // BinaryData case + BuildWriteObjectValueSwitchCase(typeof(BinaryData), "bytes", bytes => new MethodBodyStatement[] + { + writer.WriteBase64StringValue(bytes), + Break + }), + // JsonElement case + BuildWriteObjectValueSwitchCase(typeof(JsonElement), "json", json => new MethodBodyStatement[] + { + new JsonElementExpression(json).WriteTo(writer), + Break + }), + // int case + BuildWriteObjectValueSwitchCase(typeof(int), "i", i => new MethodBodyStatement[] + { + writer.WriteNumberValue(i), + Break + }), + // decimal case + BuildWriteObjectValueSwitchCase(typeof(decimal), "d", dec => new MethodBodyStatement[] + { + writer.WriteNumberValue(dec), + Break + }), + // double case + BuildWriteObjectValueSwitchCase(typeof(double), "d", d => new MethodBodyStatement[] + { + new IfElseStatement( + DoubleExpression.IsNan(d), + writer.WriteStringValue(Literal("NaN")), + writer.WriteNumberValue(d)), + Break + }), + // float case + BuildWriteObjectValueSwitchCase(typeof(float), "f", f => new MethodBodyStatement[] + { + writer.WriteNumberValue(f), + Break + }), + // long case + BuildWriteObjectValueSwitchCase(typeof(long), "l", l => new MethodBodyStatement[] + { + writer.WriteNumberValue(l), + Break + }), + // string case + BuildWriteObjectValueSwitchCase(typeof(string), "s", s => new MethodBodyStatement[] + { + writer.WriteStringValue(s), + Break + }), + // bool case + BuildWriteObjectValueSwitchCase(typeof(bool), "b", b => new MethodBodyStatement[] + { + writer.WriteBooleanValue(b), + Break + }), + // Guid case + BuildWriteObjectValueSwitchCase(typeof(Guid), "g", g => new MethodBodyStatement[] + { + writer.WriteStringValue(g), + Break + }), + // DateTimeOffset case + BuildWriteObjectValueSwitchCase(typeof(DateTimeOffset), "dateTimeOffset", dateTimeOffset => new MethodBodyStatement[] + { + writer.WriteStringValue(dateTimeOffset, "O"), + Break + }), + // DateTime case + BuildWriteObjectValueSwitchCase(typeof(DateTime), "dateTime", dateTime => new MethodBodyStatement[] + { + writer.WriteStringValue(dateTime, "O"), + Break + }), + // IEnumerable> case + BuildWriteObjectValueSwitchCase(typeof(IEnumerable>), "enumerable", enumerable => new MethodBodyStatement[] + { + writer.WriteStartObject(), + new ForeachStatement("pair", new EnumerableExpression(typeof(KeyValuePair), enumerable), out var pair) + { + writer.WritePropertyName(pair.Property(nameof(KeyValuePair.Key))), + writer.WriteObjectValue(new TypedValueExpression(typeof(object), pair.Property(nameof(KeyValuePair.Value))), options) + }, + writer.WriteEndObject(), + Break + }), + // IEnumerable case + BuildWriteObjectValueSwitchCase(typeof(IEnumerable), "objectEnumerable", objectEnumerable => new MethodBodyStatement[] + { + writer.WriteStartArray(), + new ForeachStatement("item", new EnumerableExpression(typeof(object), objectEnumerable), out var item) + { + writer.WriteObjectValue(new TypedValueExpression(typeof(object), item), options) + }, + writer.WriteEndArray(), + Break + }), + // TimeSpan case + BuildWriteObjectValueSwitchCase(typeof(TimeSpan), "timeSpan", timeSpan => new MethodBodyStatement[] + { + writer.WriteStringValue(timeSpan, "P"), + Break + }), + // default + SwitchCase.Default(Throw(New.NotSupportedException(new FormattableStringExpression("Not supported type {0}", value.InvokeGetType())))) + }); + + return new Method(signature, new SwitchStatement(value, cases)); + + static SwitchCase BuildWriteObjectValueSwitchCase(CSharpType type, string varName, Func bodyFunc) + { + var declaration = new DeclarationExpression(type, varName, out var variable); + var body = bodyFunc(variable); + + return new(declaration, body); + } + } + + private MethodSignature GetWriteObjectValueMethodSignature(CSharpType? genericArgument, out ValueExpression value, out Utf8JsonWriterExpression writer, out ParameterReference options) + { + var valueParameter = new Parameter("value", null, genericArgument ?? typeof(object), null, ValidationType.None, null); + var optionsParameter = new Parameter("options", null, typeof(ModelReaderWriterOptions), Constant.Default(new CSharpType(typeof(ModelReaderWriterOptions)).WithNullable(true)), ValidationType.None, null); + var parameters = Configuration.UseModelReaderWriter + ? new[] { KnownParameters.Serializations.Utf8JsonWriter, valueParameter, optionsParameter } + : new[] { KnownParameters.Serializations.Utf8JsonWriter, valueParameter }; + var signature = new MethodSignature( + Name: _writeObjectValueMethodName, + Summary: null, + Description: null, + Modifiers: _methodModifiers, + ReturnType: null, + ReturnDescription: null, + Parameters: parameters, + GenericArguments: genericArgument != null ? new[] { genericArgument } : null); + value = (ValueExpression)valueParameter; + writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + options = new ParameterReference(optionsParameter); + return signature; + } + + public MethodBodyStatement WriteObjectValue(Utf8JsonWriterExpression writer, TypedValueExpression value, ValueExpression? options = null) + { + var parameters = options is null + ? new ValueExpression[] { writer, value } + : new ValueExpression[] { writer, value, options }; + return new InvokeStaticMethodStatement(Type, _writeObjectValueMethodName, parameters, CallAsExtension: true, TypeArguments: new[] { value.Type }); + } + #endregion + + public BoolExpression IsSentinelValue(ValueExpression value) + { + return new(new InvokeStaticMethodExpression(Type, _isSentinelValueMethodName, new[] { value })); + } + + protected override IEnumerable BuildNestedTypes() + { + yield return _typeFormattersProvider; // TODO -- we should remove this and move `TypeFormattersProvider.Instance` to the list of helper providers + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/MultipartFormDataRequestContentProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/MultipartFormDataRequestContentProvider.cs new file mode 100644 index 0000000..4c62d82 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/MultipartFormDataRequestContentProvider.cs @@ -0,0 +1,495 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using MultipartFormDataContent = System.Net.Http.MultipartFormDataContent; + +namespace AutoRest.CSharp.Common.Output.Models.Types.HelperTypeProviders +{ + internal class MultipartFormDataRequestContentProvider : ExpressionTypeProvider + { + protected override string DefaultName { get; } = Configuration.ApiTypes.MultipartRequestContentTypeName; + private static readonly Lazy _instance = new(() => new MultipartFormDataRequestContentProvider()); + private class FormDataItemTemplate { } + public static MultipartFormDataRequestContentProvider Instance => _instance.Value; + private MultipartFormDataRequestContentProvider() : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal; + Inherits = Configuration.ApiTypes.RequestContentType; + _multipartContentField = new FieldDeclaration( + modifiers: FieldModifiers.Private | FieldModifiers.ReadOnly, + type: typeof(MultipartFormDataContent), + name: "_multipartContent"); + _multipartContentExpression = new MultipartFormDataContentExpression(_multipartContentField); + _randomField = new FieldDeclaration( + modifiers: FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, + type: typeof(Random), + name: "_random", + initializationValue: New.Instance(typeof(Random), Array.Empty()), + serializationFormat: SerializationFormat.Default); + _boundaryValuesFields = new FieldDeclaration( + modifiers: FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, + type: typeof(char[]), + name: "_boundaryValues", + initializationValue: Literal("0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz").ToCharArray(), + serializationFormat: SerializationFormat.Default); + _contentTypeProperty = new PropertyDeclaration( + description: null, + modifiers: MethodSignatureModifiers.Public, + propertyType: typeof(string), + name: _contentTypePropertyName, + propertyBody: new MethodPropertyBody(Return(_multipartContentExpression.Headers.ContentType.InvokeToString()))); + _httpContentProperty = new PropertyDeclaration( + description: null, + modifiers: MethodSignatureModifiers.Internal, + propertyType: typeof(HttpContent), + name: _httpContentPropertyName, + propertyBody: new ExpressionPropertyBody(_multipartContentField) + ); + } + private readonly FieldDeclaration _multipartContentField; + private readonly FieldDeclaration _randomField; + private readonly FieldDeclaration _boundaryValuesFields; + + private readonly PropertyDeclaration _contentTypeProperty; + private const string _contentTypePropertyName = "ContentType"; + private readonly PropertyDeclaration _httpContentProperty; + private const string _httpContentPropertyName = "HttpContent"; + private const string _createBoundaryMethodName = "CreateBoundary"; + private const string _addMethodName = "Add"; + private const string _addFilenameHeaderMethodName = "AddFilenameHeader"; + private const string _addContentTypeHeaderMethodName = "AddContentTypeHeader"; + private const string _writeToMethodName = "WriteTo"; + private const string _writeToAsyncMethodName = "WriteToAsync"; + + private readonly MultipartFormDataContentExpression _multipartContentExpression; + protected override IEnumerable BuildFields() + { + yield return _multipartContentField; + yield return _randomField; + yield return _boundaryValuesFields; + } + protected override IEnumerable BuildProperties() + { + yield return _contentTypeProperty; + yield return _httpContentProperty; + } + protected override IEnumerable BuildConstructors() + { + var signature = new ConstructorSignature( + Type: Type, + Modifiers: MethodSignatureModifiers.Public, + Parameters: Array.Empty(), + Summary: null, Description: null); + + yield return new Method(signature, Assign(_multipartContentField, New.Instance(typeof(MultipartFormDataContent), new[] { CreateBoundary() }))); + } + protected override IEnumerable BuildMethods() + { + yield return BuildCreateBoundaryMethod(); + foreach (var method in BuildAddMethods()) + { + yield return method; + } + yield return BuildAddFilenameHeaderMethod(); + yield return BuildAddContentTypeHeaderMethod(); + yield return BuildTryComputeLengthMethod(); + yield return BuildWriteToMethod(); + yield return BuildWriteToAsyncMethod(); + yield return BuildDisposeMethod(); + } + private Method BuildCreateBoundaryMethod() + { + var signature = new MethodSignature( + Name: _createBoundaryMethodName, + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Private | MethodSignatureModifiers.Static, + ReturnType: typeof(string), + ReturnDescription: null, + Parameters: Array.Empty()); + var i = new VariableReference(typeof(int), "i"); + var body = new MethodBodyStatement[] + { + Declare(typeof(Span), "chars", New.Array(typeof(char), Literal(70)), out var chars), + Declare(typeof(byte[]), "random", New.Array(typeof(byte), Literal(70)), out var random), + new InvokeInstanceMethodStatement(_randomField, nameof(Random.NextBytes), new[]{random }, false), + Declare(typeof(int), "mask", new BinaryOperatorExpression(">>", Literal(255), Literal(2)), out var mask), + new ForStatement( + new AssignmentExpression(i, Int(0)), + LessThan(i, Literal(70)), + new UnaryOperatorExpression("++", i,true)) + { + Assign(new ArrayElementExpression(chars, i), new ArrayElementExpression(_boundaryValuesFields, new BinaryOperatorExpression("&", new ArrayElementExpression(random, i), mask))), + }, + Return(chars.Untyped.InvokeToString()) + }; + return new Method(signature, body); + } + private IEnumerable BuildAddMethods() + { + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddMethod(); + yield return BuildAddHttpContentMethod(); + } + private Method BuildAddMethod() + { + var contentParam = new Parameter("content", null, typeof(T), null, ValidationType.None, null); + var nameParam = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + var filenameParam = new Parameter("filename", null, new CSharpType(typeof(string), true), + Constant.Default(new CSharpType(typeof(string), true)), ValidationType.None, null); + var contentTypeParam = new Parameter("contentType", null, new CSharpType(typeof(string), true), + Constant.Default(new CSharpType(typeof(string), true)), ValidationType.None, null); + var signature = new MethodSignature( + Name: _addMethodName, + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public, + ReturnType: null, + ReturnDescription: null, + Parameters: new[] + { + contentParam, + nameParam, + filenameParam, + contentTypeParam + }); + MethodBodyStatement? valueDelareStatement = null; + ValueExpression contentExpression = New.Instance(typeof(ByteArrayContent), contentParam); + Type contentParamType = typeof(T); + if (contentParamType == typeof(Stream)) + { + contentExpression = New.Instance(typeof(StreamContent), contentParam); + } + else if (contentParamType == typeof(string)) + { + contentExpression = New.Instance(typeof(StringContent), contentParam); + } + else if (contentParamType == typeof(bool)) + { + var boolToStringValue = new TernaryConditionalOperator(contentParam, Literal("true"), Literal("false")); + valueDelareStatement = Declare(typeof(string), "value", boolToStringValue, out TypedValueExpression boolVariable); + contentExpression = New.Instance(typeof(StringContent), boolVariable); + } + else if (contentParamType == typeof(int) + || contentParamType == typeof(float) + || contentParamType == typeof(long) + || contentParamType == typeof(double) + || contentParamType == typeof(decimal)) + { + ValueExpression invariantCulture = new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture)); + var invariantCultureValue = new InvokeInstanceMethodExpression(contentParam, nameof(Int32.ToString), new[] { Literal("G"), invariantCulture }, null, false); + valueDelareStatement = Declare(typeof(string), "value", invariantCultureValue, out TypedValueExpression variable); + contentExpression = New.Instance(typeof(StringContent), variable); + } + else if (contentParamType == typeof(BinaryData)) + { + contentExpression = New.Instance(typeof(ByteArrayContent), new BinaryDataExpression(contentParam).ToArray()); + } + else if (contentParamType == typeof(byte[])) + { + contentExpression = New.Instance(typeof(ByteArrayContent), contentParam); + } + else + { + throw new NotSupportedException($"{contentParamType} is not supported"); + } + List addContentStatements = new List(); + if (valueDelareStatement != null) + { + addContentStatements.Add(valueDelareStatement); + } + addContentStatements.Add(new InvokeInstanceMethodStatement(null, _addMethodName, new[] { contentExpression, nameParam, filenameParam, contentTypeParam }, false)); + var body = new MethodBodyStatement[] + { + Argument.AssertNotNull(contentParam), + Argument.AssertNotNullOrEmpty(nameParam), + EmptyLine, + addContentStatements.ToArray(), + }; + return new Method(signature, body); + } + private Method BuildAddHttpContentMethod() + { + var contentParam = new Parameter("content", null, typeof(HttpContent), null, ValidationType.None, null); + var nameParam = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + var filenameParam = new Parameter("filename", null, new CSharpType(typeof(string), true), null, ValidationType.None, null); + var contentTypeParam = new Parameter("contentType", null, new CSharpType(typeof(string), true), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: "Add", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Private, + ReturnType: null, + ReturnDescription: null, + Parameters: new[] + { + contentParam, + nameParam, + filenameParam, + contentTypeParam, + }); + var body = new MethodBodyStatement[] + { + new IfStatement(NotEqual(filenameParam, Null)) + { + Argument.AssertNotNullOrEmpty(filenameParam), + AddFilenameHeader(contentParam, nameParam, filenameParam), + }, + new IfStatement(NotEqual(contentTypeParam, Null)) + { + Argument.AssertNotNullOrEmpty(contentTypeParam), + AddContentTypeHeader(contentParam, contentTypeParam), + }, + _multipartContentExpression.Add(contentParam, nameParam) + }; + return new Method(signature, body); + } + private Method BuildAddFilenameHeaderMethod() + { + var contentParam = new Parameter("content", null, typeof(HttpContent), null, ValidationType.None, null); + var nameParam = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + var filenameParam = new Parameter("filename", null, typeof(string), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: "AddFilenameHeader", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + ReturnType: null, + ReturnDescription: null, + Parameters: new[] + { + contentParam, + nameParam, + filenameParam, + }); + var initialProperties = new Dictionary + { + { "Name", nameParam }, + { "FileName", filenameParam }, + }; + ValueExpression ContentDispositionHeaderExpression = new HttpContentExpression(contentParam).Headers.ContentDisposition; + var body = new MethodBodyStatement[] + { + Declare(typeof(ContentDispositionHeaderValue), "header", New.Instance(typeof(ContentDispositionHeaderValue), new[]{ Literal("form-data") }, initialProperties), out var header), + Assign(ContentDispositionHeaderExpression, header), + }; + return new Method(signature, body); + } + + private Method BuildAddContentTypeHeaderMethod() + { + var contentParam = new Parameter("content", null, typeof(HttpContent), null, ValidationType.None, null); + var contentTypeParam = new Parameter("contentType", null, typeof(string), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _addContentTypeHeaderMethodName, + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + ReturnType: null, + ReturnDescription: null, + Parameters: new[] + { + contentParam, + contentTypeParam, + }); + var contentTypeExpression = (ValueExpression)contentTypeParam; + ValueExpression ContentTypeHeaderExpression = new HttpContentExpression(contentParam).Headers.ContentType; + var body = new MethodBodyStatement[] + { + Declare(typeof(MediaTypeHeaderValue), "header", New.Instance(typeof(MediaTypeHeaderValue), new[]{ contentTypeExpression }), out var header), + Assign(ContentTypeHeaderExpression, header), + }; + return new Method(signature, body); + } + + private Method BuildTryComputeLengthMethod() + { + var lengthParameter = new Parameter("length", null, typeof(long), null, ValidationType.None, null, IsOut: true); + var signature = new MethodSignature( + Name: "TryComputeLength", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: typeof(bool), + ReturnDescription: null, + Parameters: new[] + { + lengthParameter, + }); + ValueExpression contentLengthHeaderExpression = _multipartContentExpression.Headers.ContentLength; + var body = new MethodBodyStatement[] + { + new IfStatement(BoolExpression.Is(contentLengthHeaderExpression, new DeclarationExpression(typeof(long), "contentLength", out var contentLengthVariable))) + { + Assign(lengthParameter, contentLengthVariable), + Return(Literal(true)), + }, + Assign(lengthParameter, Literal(0)), + Return(Literal(false)), + }; + return new Method(signature, body); + } + private Method BuildWriteToMethod() + { + var streamParam = new Parameter("stream", null, typeof(Stream), null, ValidationType.None, null); + var streamExpression = (ValueExpression)streamParam; + var cancellationTokenParam = KnownParameters.CancellationTokenParameter; + var cancellatinTokenExpression = (ValueExpression)cancellationTokenParam; + var signature = new MethodSignature( + Name: "WriteTo", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: null, + ReturnDescription: null, + Parameters: new[] + { + streamParam, + cancellationTokenParam + }); + var taskWaitCompletedStatementsList = new List(); + if (Configuration.IsBranded) + { + taskWaitCompletedStatementsList.Add(new PragmaWarningPreprocessorDirective("disable", "AZC0107")); + /*_multipartContent.CopyToAsync(stream).EnsureCompleted();*/ + taskWaitCompletedStatementsList.Add(new InvokeStaticMethodStatement(typeof(Azure.Core.Pipeline.TaskExtensions), "EnsureCompleted", new[] { _multipartContentExpression.CopyToAsyncExpression(streamParam) }, CallAsExtension: false)); + taskWaitCompletedStatementsList.Add(new PragmaWarningPreprocessorDirective("restore", "AZC0107")); + } + else + { + ValueExpression getTaskWaiterExpression = new InvokeInstanceMethodExpression(_multipartContentExpression.CopyToAsyncExpression(streamParam), nameof(Task.GetAwaiter), Array.Empty(), null, false); + /*_multipartContent.CopyToAsync(stream).GetAwaiter().GetResult();*/ + taskWaitCompletedStatementsList.Add(new InvokeInstanceMethodStatement(getTaskWaiterExpression, nameof(TaskAwaiter.GetResult), Array.Empty(), false)); + } + var body = new MethodBodyStatement[] + { + new IfElsePreprocessorDirective( + "NET6_0_OR_GREATER", + /*_multipartContent.CopyTo(stream, default, cancellationToken );*/ + new InvokeInstanceMethodStatement(_multipartContentField, nameof(MultipartFormDataContent.CopyTo), new[] { streamExpression, Snippets.Default ,cancellatinTokenExpression }, false), + taskWaitCompletedStatementsList.ToArray() + ), + }; + return new Method(signature, body); + } + + private Method BuildWriteToAsyncMethod() + { + var streamParam = new Parameter("stream", null, typeof(Stream), null, ValidationType.None, null); + var streamExpression = (ValueExpression)streamParam; + var cancellationTokenParam = KnownParameters.CancellationTokenParameter; + var cancellatinTokenExpression = (ValueExpression)cancellationTokenParam; + + var signature = new MethodSignature( + Name: "WriteToAsync", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override | MethodSignatureModifiers.Async, + ReturnType: typeof(Task), + ReturnDescription: null, + Parameters: new[] + { + streamParam, + cancellationTokenParam + }); + var body = new MethodBodyStatement[] + { + new IfElsePreprocessorDirective( + "NET6_0_OR_GREATER", + /*await _multipartContent.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);,*/ + _multipartContentExpression.CopyToAsync(streamExpression, cancellatinTokenExpression, true), + /*await _multipartContent.CopyToAsync(stream).ConfigureAwait(false);*/ + _multipartContentExpression.CopyToAsync(streamExpression, true) + ) + }; + return new Method(signature, body); + } + + private Method BuildDisposeMethod() + { + var signature = new MethodSignature( + Name: "Dispose", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: null, + ReturnDescription: null, + Parameters: Array.Empty()); + var body = new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(_multipartContentField, nameof(MultipartFormDataContent.Dispose), new ValueExpression[] { }, false) + }; + return new Method(signature, body); + } + public ValueExpression CreateBoundary() + => new InvokeStaticMethodExpression(Type, _createBoundaryMethodName, new ValueExpression[] { }); + public MethodBodyStatement AddFilenameHeader(ValueExpression httpContent, ValueExpression name, ValueExpression filename) + => new InvokeInstanceMethodStatement(null, _addFilenameHeaderMethodName, new[] { httpContent, name, filename }, false); + public MethodBodyStatement AddContentTypeHeader(ValueExpression httpContent, ValueExpression contentType) + => new InvokeInstanceMethodStatement(null, _addContentTypeHeaderMethodName, new[] { httpContent, contentType }, false); + public MethodBodyStatement Add(ValueExpression multipartContent, ValueExpression content, ValueExpression name, ValueExpression? filename, ValueExpression? contentType) + { + var arguments = new List() { content, name }; + if (filename != null) + { + arguments.Add(filename); + } + if (contentType != null) + { + if (filename == null) + arguments.Add(Null); + arguments.Add(contentType); + } + return new InvokeInstanceMethodStatement(multipartContent, _addMethodName, arguments.ToArray(), false); + } + public MethodBodyStatement WriteTo(ValueExpression multipartContent, ValueExpression stream, ValueExpression? cancellationToken) + { + if (cancellationToken == null) + { + return new InvokeInstanceMethodStatement(multipartContent, _writeToMethodName, new[] { stream }, false); + } + else + { + return new InvokeInstanceMethodStatement(multipartContent, _writeToMethodName, new[] { stream, cancellationToken }, false); + } + } + public MethodBodyStatement WriteToAsync(ValueExpression multipartContent, ValueExpression stream, ValueExpression? cancellationToken) + { + if (cancellationToken == null) + { + return new InvokeInstanceMethodStatement(multipartContent, _writeToAsyncMethodName, new[] { stream }, false); + } + else + { + return new InvokeInstanceMethodStatement(multipartContent, _writeToAsyncMethodName, new[] { stream, cancellationToken }, false); + } + } + public ValueExpression ContentTypeProperty(ValueExpression instance) => instance.Property(_contentTypePropertyName); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/OptionalTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/OptionalTypeProvider.cs new file mode 100644 index 0000000..5526b1c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/OptionalTypeProvider.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class OptionalTypeProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new OptionalTypeProvider()); + public static OptionalTypeProvider Instance => _instance.Value; + + private class ListTemplate { } + + private readonly CSharpType _t = typeof(ListTemplate<>).GetGenericArguments()[0]; + private readonly CSharpType _tKey = ChangeTrackingDictionaryProvider.Instance.Type.Arguments[0]; + private readonly CSharpType _tValue = ChangeTrackingDictionaryProvider.Instance.Type.Arguments[1]; + private readonly CSharpType _genericChangeTrackingList; + private readonly CSharpType _genericChangeTrackingDictionary; + + private OptionalTypeProvider() : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + _genericChangeTrackingList = ChangeTrackingListProvider.Instance.Type; + _genericChangeTrackingDictionary = ChangeTrackingDictionaryProvider.Instance.Type; + } + + protected override string DefaultName => "Optional"; + + protected override IEnumerable BuildMethods() + { + yield return BuildIsListDefined(); + yield return BuildIsDictionaryDefined(); + yield return BuildIsReadOnlyDictionaryDefined(); + yield return IsStructDefined(); + yield return IsObjectDefined(); + yield return IsJsonElementDefined(); + yield return IsStringDefined(); + } + + private MethodSignature GetIsDefinedSignature(Parameter valueParam, IReadOnlyList? genericArguments = null, IReadOnlyList? genericParameterConstraints = null) => new( + "IsDefined", + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + typeof(bool), + null, + new[] { valueParam }, + GenericArguments: genericArguments, + GenericParameterConstraints: genericParameterConstraints); + + private MethodSignature GetIsCollectionDefinedSignature(Parameter collectionParam, params CSharpType[] cSharpTypes) => new( + "IsCollectionDefined", + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + typeof(bool), + null, + new[] { collectionParam }, + GenericArguments: cSharpTypes); + + private Method IsStringDefined() + { + var valueParam = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var signature = GetIsDefinedSignature(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + Return(NotEqual(new ParameterReference(valueParam), Null)) + }); + } + + private Method IsJsonElementDefined() + { + var valueParam = new Parameter("value", null, typeof(JsonElement), null, ValidationType.None, null); + var signature = GetIsDefinedSignature(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + Return(new JsonElementExpression(new ParameterReference(valueParam)).ValueKindNotEqualsUndefined()) + }); + } + + private Method IsObjectDefined() + { + var valueParam = new Parameter("value", null, typeof(object), null, ValidationType.None, null); + var signature = GetIsDefinedSignature(valueParam); + return new Method(signature, new MethodBodyStatement[] + { + Return(NotEqual(new ParameterReference(valueParam), Null)) + }); + } + + private Method IsStructDefined() + { + var valueParam = new Parameter("value", null, _t.WithNullable(true), null, ValidationType.None, null); + var signature = GetIsDefinedSignature(valueParam, new[] { _t }, new[] { Where.Struct(_t) }); + return new Method(signature, new MethodBodyStatement[] + { + Return(new MemberExpression(new ParameterReference(valueParam), "HasValue")) + }); + } + + private Method BuildIsReadOnlyDictionaryDefined() + { + var collectionParam = new Parameter("collection", null, new CSharpType(typeof(IReadOnlyDictionary<,>), _tKey, _tValue), null, ValidationType.None, null); + var signature = GetIsCollectionDefinedSignature(collectionParam, _tKey, _tValue); + VariableReference changeTrackingReference = new VariableReference(_genericChangeTrackingDictionary, new CodeWriterDeclaration("changeTrackingDictionary")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference, false); + + return new Method(signature, new MethodBodyStatement[] + { + Return(Not(BoolExpression.Is(new ParameterReference(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + private Method BuildIsDictionaryDefined() + { + var collectionParam = new Parameter("collection", null, new CSharpType(typeof(IDictionary<,>), _tKey, _tValue), null, ValidationType.None, null); + var signature = GetIsCollectionDefinedSignature(collectionParam, _tKey, _tValue); + VariableReference changeTrackingReference = new VariableReference(_genericChangeTrackingDictionary, new CodeWriterDeclaration("changeTrackingDictionary")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference, false); + + return new Method(signature, new MethodBodyStatement[] + { + Return(Not(BoolExpression.Is(new ParameterReference(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + private Method BuildIsListDefined() + { + var collectionParam = new Parameter("collection", null, new CSharpType(typeof(IEnumerable<>), _t), null, ValidationType.None, null); + var signature = GetIsCollectionDefinedSignature(collectionParam, _t); + VariableReference changeTrackingReference = new VariableReference(_genericChangeTrackingList, new CodeWriterDeclaration("changeTrackingList")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference, false); + + return new Method(signature, new MethodBodyStatement[] + { + Return(Not(BoolExpression.Is(new ParameterReference(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + internal BoolExpression IsDefined(TypedValueExpression value) + { + return new BoolExpression(new InvokeStaticMethodExpression(Type, "IsDefined", new[] { value })); + } + + internal BoolExpression IsCollectionDefined(TypedValueExpression collection) + { + return new BoolExpression(new InvokeStaticMethodExpression(Type, "IsCollectionDefined", new[] { collection })); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/RequestContentHelperProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/RequestContentHelperProvider.cs new file mode 100644 index 0000000..f58e1d8 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/RequestContentHelperProvider.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class RequestContentHelperProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new Lazy(() => new RequestContentHelperProvider()); + public static RequestContentHelperProvider Instance => _instance.Value; + + private readonly CSharpType _requestBodyType; + private readonly CSharpType _utf8JsonRequestBodyType; + + private readonly MethodSignatureModifiers _methodModifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Static; + private RequestContentHelperProvider() : base(Configuration.HelperNamespace, null) + { + DefaultName = $"{Configuration.ApiTypes.RequestContentType.Name}Helper"; + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + _requestBodyType = Configuration.ApiTypes.RequestContentType; + _utf8JsonRequestBodyType = Utf8JsonRequestContentProvider.Instance.Type; + } + + protected override string DefaultName { get; } + + protected override IEnumerable BuildMethods() + { + yield return BuildFromEnumerableTMethod(); + yield return BuildFromEnumerableBinaryDataMethod(); + yield return BuildFromReadOnlySpanMethod(); + + yield return BuildFromDictionaryTMethod(); + yield return BuildFromDictionaryBinaryDataMethod(); + + yield return BuildFromObjectMethod(); + yield return BuildFromBinaryDataMethod(); + } + + private const string _fromEnumerableName = "FromEnumerable"; + + public ValueExpression FromEnumerable(ValueExpression enumerable) + => new InvokeStaticMethodExpression(Type, _fromEnumerableName, new[] { enumerable }); + + private Method BuildFromEnumerableTMethod() + { + var enumerableTType = typeof(IEnumerable<>); + CSharpType tType = enumerableTType.GetGenericArguments()[0]; + var enumerableParameter = new Parameter("enumerable", null, enumerableTType, null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromEnumerableName, + Modifiers: _methodModifiers, + Parameters: new[] { enumerableParameter }, + ReturnType: _requestBodyType, + GenericArguments: new[] { tType }, + GenericParameterConstraints: new[] + { + Where.NotNull(tType) + }, + Summary: null, Description: null, ReturnDescription: null); + + var enumerable = new EnumerableExpression(tType, enumerableParameter); + var body = new List + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content) + }; + var writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + body.Add(new MethodBodyStatement[] + { + writer.WriteStartArray(), + new ForeachStatement("item", enumerable, out var item) + { + writer.WriteObjectValue(item, ModelReaderWriterOptionsExpression.Wire) + }, + writer.WriteEndArray(), + EmptyLine, + Return(content) + }); + + return new Method(signature, body); + } + + private Method BuildFromEnumerableBinaryDataMethod() + { + var enumerableParameter = new Parameter("enumerable", null, typeof(IEnumerable), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromEnumerableName, + Modifiers: _methodModifiers, + Parameters: new[] { enumerableParameter }, + ReturnType: _requestBodyType, + Summary: null, Description: null, ReturnDescription: null); + + var enumerable = new EnumerableExpression(typeof(BinaryData), enumerableParameter); + var body = new List + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content) + }; + var writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + body.Add(new MethodBodyStatement[] + { + writer.WriteStartArray(), + new ForeachStatement("item", enumerable, out var item) + { + new IfElseStatement( + Equal(item, Null), + writer.WriteNullValue(), + writer.WriteBinaryData(item)) + }, + writer.WriteEndArray(), + EmptyLine, + Return(content) + }); + + return new Method(signature, body); + } + + private Method BuildFromReadOnlySpanMethod() + { + var spanType = typeof(ReadOnlySpan<>); + CSharpType tType = spanType.GetGenericArguments()[0]; + var spanParameter = new Parameter("span", null, spanType, null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromEnumerableName, + Modifiers: _methodModifiers, + Parameters: new[] { spanParameter }, + ReturnType: _requestBodyType, + GenericArguments: new[] { tType }, + GenericParameterConstraints: new[] + { + Where.NotNull(tType) + }, + Summary: null, Description: null, ReturnDescription: null); + + var span = (ValueExpression)spanParameter; + var body = new List + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content) + }; + var writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + var i = new VariableReference(typeof(int), "i"); + body.Add(new MethodBodyStatement[] + { + writer.WriteStartArray(), + new ForStatement(new AssignmentExpression(i, Int(0)), LessThan(i, span.Property(nameof(ReadOnlySpan.Length))), new UnaryOperatorExpression("++", i, true)) + { + writer.WriteObjectValue(new TypedValueExpression(tType, new IndexerExpression(span, i)), ModelReaderWriterOptionsExpression.Wire) + }, + writer.WriteEndArray(), + EmptyLine, + Return(content) + }); + + return new Method(signature, body); + } + + private const string _fromDictionaryName = "FromDictionary"; + + public ValueExpression FromDictionary(ValueExpression dictionary) + => new InvokeStaticMethodExpression(Type, _fromDictionaryName, new[] { dictionary }); + + private Method BuildFromDictionaryTMethod() + { + var dictionaryTType = typeof(IDictionary<,>); + CSharpType valueType = dictionaryTType.GetGenericArguments()[1]; + var dictionaryParameter = new Parameter("dictionary", null, new CSharpType(dictionaryTType, typeof(string), valueType), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromDictionaryName, + Modifiers: _methodModifiers, + Parameters: new[] { dictionaryParameter }, + ReturnType: _requestBodyType, + GenericArguments: new[] { valueType }, + GenericParameterConstraints: new[] + { + Where.NotNull(valueType) + }, + Summary: null, Description: null, ReturnDescription: null); + + var dictionary = new DictionaryExpression(typeof(string), valueType, dictionaryParameter); + var body = new List + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content) + }; + var writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + body.Add(new MethodBodyStatement[] + { + writer.WriteStartObject(), + new ForeachStatement("item", dictionary, out var item) + { + writer.WritePropertyName(item.Key), + writer.WriteObjectValue(item.Value, ModelReaderWriterOptionsExpression.Wire) + }, + writer.WriteEndObject(), + EmptyLine, + Return(content) + }); + + return new Method(signature, body); + } + + private Method BuildFromDictionaryBinaryDataMethod() + { + var dictionaryParameter = new Parameter("dictionary", null, typeof(IDictionary), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromDictionaryName, + Modifiers: _methodModifiers, + Parameters: new[] { dictionaryParameter }, + ReturnType: _requestBodyType, + Summary: null, Description: null, ReturnDescription: null); + + var dictionary = new DictionaryExpression(typeof(string), typeof(BinaryData), dictionaryParameter); + var body = new List + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content) + }; + var writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + body.Add(new MethodBodyStatement[] + { + writer.WriteStartObject(), + new ForeachStatement("item", dictionary, out var item) + { + writer.WritePropertyName(item.Key), + new IfElseStatement( + Equal(item.Value, Null), + writer.WriteNullValue(), + writer.WriteBinaryData(item.Value)) + }, + writer.WriteEndObject(), + EmptyLine, + Return(content) + }); + + return new Method(signature, body); + } + + private const string _fromObjectName = "FromObject"; + + public ValueExpression FromObject(ValueExpression value) + => new InvokeStaticMethodExpression(Type, _fromObjectName, new[] { value }); + + private Method BuildFromObjectMethod() + { + var valueParameter = new Parameter("value", null, typeof(object), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromObjectName, + Modifiers: _methodModifiers, + Parameters: new[] { valueParameter }, + ReturnType: _requestBodyType, + Summary: null, Description: null, ReturnDescription: null); + + var body = new MethodBodyStatement[] + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content), + Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content).WriteObjectValue(valueParameter, ModelReaderWriterOptionsExpression.Wire), + Return(content) + }; + + return new Method(signature, body); + } + + private Method BuildFromBinaryDataMethod() + { + var valueParameter = new Parameter("value", null, typeof(BinaryData), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromObjectName, + Modifiers: _methodModifiers, + Parameters: new[] { valueParameter }, + ReturnType: _requestBodyType, + Summary: null, Description: null, ReturnDescription: null); + + var body = new List + { + Declare(_utf8JsonRequestBodyType, "content", New.Instance(_utf8JsonRequestBodyType), out var content) + }; + var writer = Utf8JsonRequestContentProvider.Instance.JsonWriterProperty(content); + var value = new BinaryDataExpression(valueParameter); + body.Add(new MethodBodyStatement[] + { + writer.WriteBinaryData(value), + Return(content) + }); + + return new Method(signature, body); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ClientPipelineExtensionsProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ClientPipelineExtensionsProvider.cs new file mode 100644 index 0000000..f9498aa --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ClientPipelineExtensionsProvider.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types.System +{ + internal class ClientPipelineExtensionsProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new ClientPipelineExtensionsProvider()); + + public static ClientPipelineExtensionsProvider Instance => _instance.Value; + + private const string _processMessageAsync = "ProcessMessageAsync"; + private const string _processMessage = "ProcessMessage"; + private const string _processHeadAsBoolMessageAsync = "ProcessHeadAsBoolMessageAsync"; + private const string _processHeadAsBoolMessage = "ProcessHeadAsBoolMessage"; + + private Parameter _pipelineParam; + private Parameter _messageParam; + private Parameter _requestOptionsParam; + private ClientPipelineExpression _pipeline; + private PipelineMessageExpression _message; + private RequestOptionsExpression _options; + + private ClientPipelineExtensionsProvider() + : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + _pipelineParam = new Parameter("pipeline", null, typeof(ClientPipeline), null, ValidationType.None, null); + _messageParam = new Parameter("message", null, typeof(PipelineMessage), null, ValidationType.None, null); + _requestOptionsParam = new Parameter("options", null, typeof(RequestOptions), null, ValidationType.None, null); + _pipeline = new ClientPipelineExpression(_pipelineParam); + _message = new PipelineMessageExpression(_messageParam); + _options = new RequestOptionsExpression(_requestOptionsParam); + } + + protected override string DefaultName => "ClientPipelineExtensions"; + + protected override IEnumerable BuildMethods() + { + yield return BuildProcessMessageAsync(); + yield return BuildProcessMessage(); + yield return ProcessHeadAsBoolMessageAsync(); + yield return ProcessHeadAsBoolMessage(); + } + + private Method ProcessHeadAsBoolMessage() + { + MethodSignature signature = GetProcessHeadAsBoolMessageSignature(false); + var responseVariable = new VariableReference(typeof(PipelineResponse), "response"); + var response = new PipelineResponseExpression(responseVariable); + return new Method(signature, new MethodBodyStatement[] + { + Assign(new DeclarationExpression(responseVariable, false), _pipeline.ProcessMessage(_message, _options, false)), + GetProcessHeadAsBoolMessageBody(response) + }); + } + + private Method ProcessHeadAsBoolMessageAsync() + { + MethodSignature signature = GetProcessHeadAsBoolMessageSignature(true); + var responseVariable = new VariableReference(typeof(PipelineResponse), "response"); + var response = new PipelineResponseExpression(responseVariable); + return new Method(signature, new MethodBodyStatement[] + { + Assign(new DeclarationExpression(responseVariable, false), _pipeline.ProcessMessage(_message, _options, true)), + GetProcessHeadAsBoolMessageBody(response) + }); + } + + private MethodBodyStatement GetProcessHeadAsBoolMessageBody(PipelineResponseExpression response) + { + return new MethodBodyStatement[] + { + new SwitchStatement(new MemberExpression(response, "Status"), new SwitchCase[] + { + new SwitchCase(new BinaryOperatorExpression("<", new UnaryOperatorExpression("and", new UnaryOperatorExpression(">=", Literal(200), false), true), Literal(300)), new MethodBodyStatement[] + { + Return(ClientResultExpression.FromValue(typeof(bool), True, response)) + }), + new SwitchCase(new BinaryOperatorExpression("<", new UnaryOperatorExpression("and", new UnaryOperatorExpression(">=", Literal(400), false), true), Literal(500)), new MethodBodyStatement[] + { + Return(ClientResultExpression.FromValue(typeof(bool), False, response)) + }), + new SwitchCase(Array.Empty(), new MethodBodyStatement[] + { + Return(new NewInstanceExpression(ErrorResultProvider.Instance.Type.MakeGenericType(new CSharpType[]{ typeof(bool) }), new ValueExpression[]{ response, new NewInstanceExpression(typeof(ClientResultException), new[] { response })})) + }) + }), + }; + } + + private MethodSignature GetProcessHeadAsBoolMessageSignature(bool isAsync) + { + var modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension; + if (isAsync) + { + modifiers |= MethodSignatureModifiers.Async; + } + return new MethodSignature( + isAsync ? "ProcessHeadAsBoolMessageAsync" : "ProcessHeadAsBoolMessage", + null, + null, + modifiers, + isAsync ? typeof(ValueTask>) : typeof(ClientResult), + null, + new[] { _pipelineParam, _messageParam, _requestOptionsParam }); + } + + private Method BuildProcessMessage() + { + MethodSignature signature = GetProcessMessageSignature(false); + + var clientErrorNoThrow = FrameworkEnumValue(ClientErrorBehaviors.NoThrow); + return new Method(signature, new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(_pipeline, nameof(ClientPipeline.Send), new[] { _message }, false), + EmptyLine, + new IfStatement(And(_message.Response.IsError, NotEqual(new BinaryOperatorExpression("&", _options.Property("ErrorOptions", true), clientErrorNoThrow), clientErrorNoThrow))) + { + Throw(New.Instance(typeof(ClientResultException), _message.Response)) + }, + EmptyLine, + Declare("response", new TypedValueExpression(typeof(PipelineResponse), new TernaryConditionalOperator(_message.BufferResponse, _message.Response, _message.ExtractResponse())), out var response), + Return(response) + }); + } + + private MethodSignature GetProcessMessageSignature(bool isAsync) + { + var modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension; + if (isAsync) + { + modifiers |= MethodSignatureModifiers.Async; + } + return new MethodSignature( + isAsync ? "ProcessMessageAsync" : "ProcessMessage", + null, + null, + modifiers, + isAsync ? typeof(ValueTask) : typeof(PipelineResponse), + null, + new[] { _pipelineParam, _messageParam, _requestOptionsParam }); + } + + private Method BuildProcessMessageAsync() + { + MethodSignature signature = GetProcessMessageSignature(true); + + var clientErrorNoThrow = FrameworkEnumValue(ClientErrorBehaviors.NoThrow); + return new Method(signature, new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(_pipeline, nameof(ClientPipeline.SendAsync), new[]{ _message }, true), + EmptyLine, + new IfStatement(And(_message.Response.IsError, NotEqual(new BinaryOperatorExpression("&", _options.Property("ErrorOptions", true), clientErrorNoThrow), clientErrorNoThrow))) + { + Throw(new InvokeStaticMethodExpression(typeof(ClientResultException), nameof(ClientResultException.CreateAsync), new[] { _message.Response }, CallAsAsync: true)) + }, + EmptyLine, + Declare("response", new TypedValueExpression(typeof(PipelineResponse), new TernaryConditionalOperator(_message.BufferResponse, _message.Response, _message.ExtractResponse())), out var response), + Return(response) + }); + } + + internal PipelineResponseExpression ProcessMessage(IReadOnlyList arguments, bool isAsync) + { + return new(new InvokeStaticMethodExpression(Type, isAsync ? _processMessageAsync : _processMessage, arguments, CallAsExtension: true, CallAsAsync: isAsync)); + } + + internal ClientResultExpression ProcessHeadAsBoolMessage(IReadOnlyList arguments, bool isAsync) + { + return new(new InvokeStaticMethodExpression(Type, isAsync ? _processHeadAsBoolMessageAsync : _processHeadAsBoolMessage, arguments, CallAsExtension: true, CallAsAsync: isAsync)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ClientUriBuilderProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ClientUriBuilderProvider.cs new file mode 100644 index 0000000..13a6563 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ClientUriBuilderProvider.cs @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types.System +{ + internal class ClientUriBuilderProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new ClientUriBuilderProvider()); + + public static ClientUriBuilderProvider Instance => _instance.Value; + + private ClientUriBuilderProvider() : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal; + } + + protected override string DefaultName => "ClientUriBuilder"; + + private readonly FieldDeclaration _uriBuilderField = new(FieldModifiers.Private, typeof(UriBuilder), "_uriBuilder"); + private readonly FieldDeclaration _pathBuilderField = new(FieldModifiers.Private, typeof(StringBuilder), "_pathBuilder"); + private readonly FieldDeclaration _queryBuilderField = new(FieldModifiers.Private, typeof(StringBuilder), "_queryBuilder"); + + protected override IEnumerable BuildFields() + { + yield return _uriBuilderField; + yield return _pathBuilderField; + yield return _queryBuilderField; + } + + private PropertyDeclaration? _uriBuilderProperty; + private PropertyDeclaration UriBuilderProperty => _uriBuilderProperty ??= new( + modifiers: MethodSignatureModifiers.Private, + name: "UriBuilder", + propertyType: typeof(UriBuilder), + propertyBody: new ExpressionPropertyBody(new BinaryOperatorExpression(" ??= ", _uriBuilderField, New.Instance(typeof(UriBuilder)))), + description: null); + + internal ValueExpression UriBuilderPath => new MemberExpression(UriBuilderProperty, "Path"); + internal ValueExpression UriBuilderQuery => new MemberExpression(UriBuilderProperty, "Query"); + + private PropertyDeclaration? _pathBuilderProperty; + private PropertyDeclaration PathBuilderProperty => _pathBuilderProperty ??= new( + modifiers: MethodSignatureModifiers.Private, + name: "PathBuilder", + propertyType: typeof(StringBuilder), + propertyBody: new ExpressionPropertyBody(new BinaryOperatorExpression(" ??= ", _pathBuilderField, New.Instance(typeof(StringBuilder), UriBuilderPath))), + description: null); + + private PropertyDeclaration? _queryBuilderProperty; + private PropertyDeclaration QueryBuilderProperty => _queryBuilderProperty ??= new( + modifiers: MethodSignatureModifiers.Private, + name: "QueryBuilder", + propertyType: typeof(StringBuilder), + propertyBody: new ExpressionPropertyBody(new BinaryOperatorExpression(" ??= ", _queryBuilderField, New.Instance(typeof(StringBuilder), UriBuilderQuery))), + description: null); + + protected override IEnumerable BuildProperties() + { + yield return UriBuilderProperty; + yield return PathBuilderProperty; + yield return QueryBuilderProperty; + } + + protected override IEnumerable BuildConstructors() + { + var signature = new ConstructorSignature( + Type: Type, + Modifiers: MethodSignatureModifiers.Public, + Parameters: Array.Empty(), + Summary: null, Description: null); + yield return new(signature, EmptyStatement); + } + + protected override IEnumerable BuildMethods() + { + yield return BuildResetMethod(); + + foreach (var method in BuildAppendPathMethods()) + { + yield return method; + } + + foreach (var method in BuildAppendQueryMethods()) + { + yield return method; + } + + foreach (var method in BuildAppendQueryDelimitedMethods()) + { + yield return method; + } + + yield return BuildToUriMethod(); + } + + private const string _resetMethodName = "Reset"; + private Method BuildResetMethod() + { + var uriParameter = new Parameter("uri", null, typeof(Uri), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _resetMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] + { + uriParameter + }, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + + var body = new MethodBodyStatement[] + { + Assign(_uriBuilderField, New.Instance(_uriBuilderField.Type, uriParameter)), + Assign(_pathBuilderField, New.Instance(_pathBuilderField.Type, UriBuilderPath)), + Assign(_queryBuilderField, New.Instance(_queryBuilderField.Type, UriBuilderQuery)) + }; + + return new(signature, body); + } + + private const string _appendPathMethodName = "AppendPath"; + private IEnumerable BuildAppendPathMethods() + { + var valueParameter = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var escapeParameter = new Parameter("escape", null, typeof(bool), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _appendPathMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] { valueParameter, escapeParameter }, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + + var value = new StringExpression(valueParameter); + var escape = new BoolExpression(escapeParameter); + var pathBuilder = new StringBuilderExpression(PathBuilderProperty); + MethodBodyStatement body = new MethodBodyStatement[] + { + Argument.AssertNotNullOrWhiteSpace(value), + EmptyLine, + new IfStatement(escape) + { + Assign(value, new InvokeStaticMethodExpression(typeof(Uri), nameof(Uri.EscapeDataString), new[]{ value })) + }, + EmptyLine, + new IfStatement(And(And(GreaterThan(pathBuilder.Length, Int(0)), Equal(new IndexerExpression(pathBuilder, pathBuilder.Length - Int(1)), Literal('/'))), Equal(new IndexerExpression(value, Int(0)), Literal('/')))) + { + pathBuilder.Remove(pathBuilder.Length - Int(1), Int(1)) + }, + EmptyLine, + pathBuilder.Append(value), + Assign(UriBuilderPath, pathBuilder.InvokeToString()) + }; + + yield return new(signature, body); + + yield return BuildAppendPathMethod(typeof(bool), false, false); + yield return BuildAppendPathMethod(typeof(float), true, false); + yield return BuildAppendPathMethod(typeof(double), true, false); + yield return BuildAppendPathMethod(typeof(int), true, false); + yield return BuildAppendPathMethod(typeof(byte[]), true, true); + yield return BuildAppendPathMethod(typeof(IEnumerable), true, false); + yield return BuildAppendPathMethod(typeof(DateTimeOffset), true, true); + yield return BuildAppendPathMethod(typeof(TimeSpan), true, true); + yield return BuildAppendPathMethod(typeof(Guid), true, false); + yield return BuildAppendPathMethod(typeof(long), true, false); + } + + private Method BuildAppendPathMethod(CSharpType valueType, bool escapeDefaultValue, bool hasFormat) + { + var valueParameter = new Parameter("value", null, valueType, null, ValidationType.None, null); + var escapeParameter = new Parameter("escape", null, typeof(bool), new Constant(escapeDefaultValue, typeof(bool)), ValidationType.None, null); + var formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + var parameters = hasFormat + ? new[] { valueParameter, formatParameter, escapeParameter } + : new[] { valueParameter, escapeParameter }; + + var signature = new MethodSignature( + Name: _appendPathMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: parameters, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + // TODO -- this is temporary, in the future, TypeFormatters will be a standalone static class, but now it is a nested type inside ModelSerializationExtensions + var typeFormattersProvider = (TypeFormattersProvider)ModelSerializationExtensionsProvider.Instance.NestedTypes[0]; + var convertToStringExpression = typeFormattersProvider.ConvertToString(valueParameter, hasFormat ? (ValueExpression)formatParameter : null); + var body = new InvokeInstanceMethodStatement(null, _appendPathMethodName, convertToStringExpression, escapeParameter); + + return new(signature, body); + } + + private const string _appendQueryMethodName = "AppendQuery"; + private IEnumerable BuildAppendQueryMethods() + { + var nameParameter = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + var valueParameter = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var escapeParameter = new Parameter("escape", null, typeof(bool), null, ValidationType.None, null); + + var signature = new MethodSignature( + Name: _appendQueryMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] { nameParameter, valueParameter, escapeParameter }, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + + var name = new StringExpression(nameParameter); + var value = new StringExpression(valueParameter); + var escape = new BoolExpression(escapeParameter); + var queryBuilder = new StringBuilderExpression(QueryBuilderProperty); + var body = new MethodBodyStatement[] + { + Argument.AssertNotNullOrWhiteSpace(name), + Argument.AssertNotNullOrWhiteSpace(value), + EmptyLine, + new IfStatement(GreaterThan(queryBuilder.Length, Int(0))) + { + queryBuilder.Append(Literal('&')) + }, + EmptyLine, + new IfStatement(escape) + { + Assign(value, new InvokeStaticMethodExpression(typeof(Uri), nameof(Uri.EscapeDataString), new[] { value })) + }, + EmptyLine, + queryBuilder.Append(name), + queryBuilder.Append(Literal('=')), + queryBuilder.Append(value) + }; + + yield return new(signature, body); + + yield return BuildAppendQueryMethod(typeof(bool), false, false); + yield return BuildAppendQueryMethod(typeof(float), true, false); + yield return BuildAppendQueryMethod(typeof(DateTimeOffset), true, true); + yield return BuildAppendQueryMethod(typeof(TimeSpan), true, true); + yield return BuildAppendQueryMethod(typeof(double), true, false); + yield return BuildAppendQueryMethod(typeof(decimal), true, false); + yield return BuildAppendQueryMethod(typeof(int), true, false); + yield return BuildAppendQueryMethod(typeof(long), true, false); + yield return BuildAppendQueryMethod(typeof(TimeSpan), true, false); + yield return BuildAppendQueryMethod(typeof(byte[]), true, true); + yield return BuildAppendQueryMethod(typeof(Guid), true, false); + } + + private Method BuildAppendQueryMethod(CSharpType valueType, bool escapeDefaultValue, bool hasFormat) + { + var nameParameter = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + var valueParameter = new Parameter("value", null, valueType, null, ValidationType.None, null); + var escapeParameter = new Parameter("escape", null, typeof(bool), new Constant(escapeDefaultValue, typeof(bool)), ValidationType.None, null); + var formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + var parameters = hasFormat + ? new[] { nameParameter, valueParameter, formatParameter, escapeParameter } + : new[] { nameParameter, valueParameter, escapeParameter }; + + var signature = new MethodSignature( + Name: _appendQueryMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: parameters, + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + // TODO -- this is temporary, in the future, TypeFormatters will be a standalone static class, but now it is a nested type inside ModelSerializationExtensions + var typeFormattersProvider = (TypeFormattersProvider)ModelSerializationExtensionsProvider.Instance.NestedTypes[0]; + var convertToStringExpression = typeFormattersProvider.ConvertToString(valueParameter, hasFormat ? (ValueExpression)formatParameter : null); + var body = new InvokeInstanceMethodStatement(null, _appendQueryMethodName, nameParameter, convertToStringExpression, escapeParameter); + + return new(signature, body); + } + + private IEnumerable BuildAppendQueryDelimitedMethods() + { + yield return BuildAppendQueryDelimitedMethod(false); + yield return BuildAppendQueryDelimitedMethod(true); + } + + private readonly CSharpType _t = typeof(IEnumerable<>).GetGenericArguments()[0]; + + private const string _appendQueryDelimitedMethodName = "AppendQueryDelimited"; + private Method BuildAppendQueryDelimitedMethod(bool hasFormat) + { + var nameParameter = new Parameter("name", null, typeof(string), null, ValidationType.None, null); + var valueParameter = new Parameter("value", null, new CSharpType(typeof(IEnumerable<>), _t), null, ValidationType.None, null); + var delimiterParameter = new Parameter("delimiter", null, typeof(string), null, ValidationType.None, null); + var formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + var escapeParameter = new Parameter("escape", null, typeof(bool), new Constant(true, typeof(bool)), ValidationType.None, null); + + var parameters = hasFormat + ? new[] { nameParameter, valueParameter, delimiterParameter, formatParameter, escapeParameter } + : new[] { nameParameter, valueParameter, delimiterParameter, escapeParameter }; + var signature = new MethodSignature( + Name: _appendQueryDelimitedMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: parameters, + ReturnType: null, + GenericArguments: new[] { _t }, + Summary: null, Description: null, ReturnDescription: null); + + var name = new StringExpression(nameParameter); + var value = new EnumerableExpression(_t, valueParameter); + var delimiter = new StringExpression(delimiterParameter); + var format = hasFormat ? new StringExpression(formatParameter) : null; + var escape = new BoolExpression(escapeParameter); + + var typeFormattersProvider = (TypeFormattersProvider)ModelSerializationExtensionsProvider.Instance.NestedTypes[0]; + var v = new VariableReference(_t, "v"); + var body = new[] + { + Var("stringValues", value.Select(new TypedFuncExpression(new[] {v.Declaration}, typeFormattersProvider.ConvertToString(v, format))), out var stringValues), + new InvokeInstanceMethodStatement(null, _appendQueryMethodName, name, StringExpression.Join(delimiter, stringValues), escape) + }; + + return new(signature, body); + } + + private const string _toUriMethodName = "ToUri"; + private Method BuildToUriMethod() + { + var signature = new MethodSignature( + Name: _toUriMethodName, + Modifiers: MethodSignatureModifiers.Public, + Parameters: Array.Empty(), + ReturnType: typeof(Uri), + Summary: null, Description: null, ReturnDescription: null); + + var pathBuilder = (ValueExpression)_pathBuilderField; + var queryBuilder = (ValueExpression)_queryBuilderField; + var body = new MethodBodyStatement[] + { + new IfStatement(NotEqual(pathBuilder, Null)) + { + Assign(UriBuilderPath, pathBuilder.InvokeToString()) + }, + EmptyLine, + new IfStatement(NotEqual(queryBuilder, Null)) + { + Assign(UriBuilderQuery, queryBuilder.InvokeToString()) + }, + EmptyLine, + Return(new MemberExpression(UriBuilderProperty, nameof(UriBuilder.Uri))) + }; + + return new(signature, body); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ErrorResultProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ErrorResultProvider.cs new file mode 100644 index 0000000..a29784f --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/System/ErrorResultProvider.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types.System +{ + internal class ErrorResultProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new(() => new ErrorResultProvider()); + + public static ErrorResultProvider Instance => _instance.Value; + + private class ErrorResultTemplate { } + + private CSharpType _t = typeof(ErrorResultTemplate<>).GetGenericArguments()[0]; + private FieldDeclaration _responseField; + private FieldDeclaration _exceptionField; + private VariableReference _response; + private VariableReference _exception; + + private ErrorResultProvider() + : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal; + _responseField = new FieldDeclaration(FieldModifiers.Private | FieldModifiers.ReadOnly, typeof(PipelineResponse), "_response"); + _exceptionField = new FieldDeclaration(FieldModifiers.Private | FieldModifiers.ReadOnly, typeof(ClientResultException), "_exception"); + _response = new VariableReference(_responseField.Type, _responseField.Declaration); + _exception = new VariableReference(_exceptionField.Type, _exceptionField.Declaration); + } + + protected override string DefaultName => "ErrorResult"; + + protected override IEnumerable BuildTypeArguments() + { + yield return _t; + } + + protected override IEnumerable BuildImplements() + { + yield return new CSharpType(typeof(ClientResult<>), _t); + } + + protected override IEnumerable BuildFields() + { + yield return _responseField; + yield return _exceptionField; + } + + protected override IEnumerable BuildConstructors() + { + yield return BuildCtor(); + } + + private Method BuildCtor() + { + var responseParam = new Parameter("response", null, typeof(PipelineResponse), null, ValidationType.None, null); + var exceptionParam = new Parameter("exception", null, typeof(ClientResultException), null, ValidationType.None, null); + var response = new ParameterReference(responseParam); + var exception = new ParameterReference(exceptionParam); + var baseInitializer = new ConstructorInitializer(true, new List { Default, response }); + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new[] { responseParam, exceptionParam }, Initializer: baseInitializer); + return new Method(signature, new MethodBodyStatement[] + { + Assign(_response, response), + Assign(_exception, exception), + }); + } + + protected override IEnumerable BuildProperties() + { + yield return BuildValue(); + } + + private PropertyDeclaration BuildValue() + { + return new PropertyDeclaration(null, MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, _t, "Value", new ExpressionPropertyBody( + ThrowExpression(_exception) + )); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/TypeFormattersProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/TypeFormattersProvider.cs new file mode 100644 index 0000000..70d9a3b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/TypeFormattersProvider.cs @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using ValidationType = AutoRest.CSharp.Output.Models.Shared.ValidationType; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class TypeFormattersProvider : ExpressionTypeProvider + { + //private static readonly Lazy _instance = new(() => new TypeFormattersProvider()); + //public static TypeFormattersProvider Instance => _instance.Value; + + // TODO -- this type provider is temprorarily an inner class therefore it is not singleton yet. We need to change this to singleton when we decide to emit the entire TypeFormatters class. + internal TypeFormattersProvider(ExpressionTypeProvider declaringTypeProvider) : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + DeclaringTypeProvider = declaringTypeProvider; + } + + protected override string DefaultName => "TypeFormatters"; + + private readonly FieldDeclaration _roundtripZFormatField = new(FieldModifiers.Private | FieldModifiers.Const, typeof(string), "RoundtripZFormat", Literal("yyyy-MM-ddTHH:mm:ss.fffffffZ"), SerializationFormat.Default); + private readonly FieldDeclaration _defaultNumberFormatField = new(FieldModifiers.Public | FieldModifiers.Const, typeof(string), "DefaultNumberFormat", Literal("G"), SerializationFormat.Default); + + protected override IEnumerable BuildFields() + { + yield return _roundtripZFormatField; + yield return _defaultNumberFormatField; + } + + private readonly MethodSignatureModifiers _methodModifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Static; + + protected override IEnumerable BuildMethods() + { + foreach (var method in BuildToStringMethods()) + { + yield return method; + } + + yield return BuildToBase64UrlStringMethod(); + yield return BuildFromBase64UrlString(); + yield return BuildParseDateTimeOffsetMethod(); + yield return BuildParseTimeSpanMethod(); + yield return BuildConvertToStringMethod(); + } + + private readonly ValueExpression _invariantCultureExpression = new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture)); + private const string _toStringMethodName = "ToString"; + + private IEnumerable BuildToStringMethods() + { + var boolValueParameter = new Parameter("value", null, typeof(bool), null, ValidationType.None, null); + var boolSignature = new MethodSignature( + Name: _toStringMethodName, + Parameters: new[] { boolValueParameter }, + Modifiers: _methodModifiers, + ReturnType: typeof(string), + Summary: null, Description: null, ReturnDescription: null); + var boolValue = new BoolExpression(boolValueParameter); + yield return new Method(boolSignature, + new TernaryConditionalOperator(boolValue, Literal("true"), Literal("false"))); + + var dateTimeParameter = boolValueParameter with { Type = typeof(DateTime) }; + var formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + var dateTimeSignature = boolSignature with + { + Parameters = new[] { dateTimeParameter, formatParameter } + }; + var dateTimeValue = (ValueExpression)dateTimeParameter; + var dateTimeValueKind = dateTimeValue.Property(nameof(DateTime.Kind)); + var format = new StringExpression(formatParameter); + var sdkName = Configuration.IsBranded ? "Azure SDK requires" : "Generated clients require"; + yield return new Method(dateTimeSignature, + new SwitchExpression(dateTimeValueKind, new SwitchCaseExpression[] + { + new(new MemberExpression(typeof(DateTimeKind), nameof(DateTimeKind.Utc)), ToString(dateTimeValue.CastTo(typeof(DateTimeOffset)), format)), + SwitchCaseExpression.Default(ThrowExpression(New.NotSupportedException(new FormattableStringExpression($"DateTime {{0}} has a Kind of {{1}}. {sdkName} it to be UTC. You can call DateTime.SpecifyKind to change Kind property value to DateTimeKind.Utc.", dateTimeValue, dateTimeValueKind)))) + })); + + var dateTimeOffsetParameter = boolValueParameter with { Type = typeof(DateTimeOffset) }; + var dateTimeOffsetSignature = boolSignature with + { + Parameters = new[] { dateTimeOffsetParameter, formatParameter } + }; + var dateTimeOffsetValue = new DateTimeOffsetExpression(dateTimeOffsetParameter); + var roundtripZFormat = new StringExpression(_roundtripZFormatField); + yield return new Method(dateTimeOffsetSignature, + new SwitchExpression(format, new SwitchCaseExpression[] + { + new(Literal("D"), dateTimeOffsetValue.InvokeToString(Literal("yyyy-MM-dd"), _invariantCultureExpression)), + new(Literal("U"), dateTimeOffsetValue.ToUnixTimeSeconds().InvokeToString(_invariantCultureExpression)), + new(Literal("O"), dateTimeOffsetValue.ToUniversalTime().InvokeToString(roundtripZFormat, _invariantCultureExpression)), + new(Literal("o"), dateTimeOffsetValue.ToUniversalTime().InvokeToString(roundtripZFormat, _invariantCultureExpression)), + new(Literal("R"), dateTimeOffsetValue.InvokeToString(Literal("r"), _invariantCultureExpression)), + SwitchCaseExpression.Default(dateTimeOffsetValue.InvokeToString(format, _invariantCultureExpression)) + })); + + var timeSpanParameter = boolValueParameter with { Type = typeof(TimeSpan) }; + var timeSpanSignature = boolSignature with + { + Parameters = new[] { timeSpanParameter, formatParameter } + }; + var timeSpanValue = new TimeSpanExpression(timeSpanParameter); + yield return new Method(timeSpanSignature, + new SwitchExpression(format, new SwitchCaseExpression[] + { + new(Literal("P"), new InvokeStaticMethodExpression(typeof(XmlConvert), nameof(XmlConvert.ToString), new[] {timeSpanValue})), + SwitchCaseExpression.Default(timeSpanValue.InvokeToString(format, _invariantCultureExpression)) + })); + + var byteArrayParameter = boolValueParameter with { Type = typeof(byte[]) }; + var byteArraySignature = boolSignature with + { + Parameters = new[] { byteArrayParameter, formatParameter } + }; + var byteArrayValue = (ValueExpression)byteArrayParameter; + yield return new Method(byteArraySignature, + new SwitchExpression(format, new SwitchCaseExpression[] + { + new(Literal("U"), ToBase64UrlString(byteArrayValue)), + new(Literal("D"), new InvokeStaticMethodExpression(typeof(Convert), nameof(Convert.ToBase64String), new[] {byteArrayValue})), + SwitchCaseExpression.Default(ThrowExpression(New.ArgumentException(format, new FormattableStringExpression("Format is not supported: '{0}'", format)))) + })); + } + + public StringExpression ToString(ValueExpression value) + => new(new InvokeStaticMethodExpression(Type, _toStringMethodName, new[] { value })); + + public StringExpression ToString(ValueExpression value, ValueExpression format) + => new(new InvokeStaticMethodExpression(Type, _toStringMethodName, new[] { value, format })); + + private const string _toBase64UrlStringMethodName = "ToBase64UrlString"; + + private Method BuildToBase64UrlStringMethod() + { + var valueParameter = new Parameter("value", null, typeof(byte[]), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _toBase64UrlStringMethodName, + Parameters: new[] { valueParameter }, + ReturnType: typeof(string), + Modifiers: _methodModifiers, + Summary: null, Description: null, ReturnDescription: null); + + var value = (ValueExpression)valueParameter; + var valueLength = new IntExpression(value.Property("Length")); + var body = new List + { + Declare("numWholeOrPartialInputBlocks", new IntExpression(new BinaryOperatorExpression("/", new KeywordExpression("checked", new BinaryOperatorExpression("+", valueLength, Int(2))), Int(3))), out var numWholeOrPartialInputBlocks), + Declare("size", new IntExpression(new KeywordExpression("checked", new BinaryOperatorExpression("*", numWholeOrPartialInputBlocks, Int(4)))), out var size), + }; + var output = new VariableReference(typeof(char[]), "output"); + body.Add(new MethodBodyStatement[] + { + Declare(output, New.Array(typeof(char), size)), + EmptyLine, + Declare("numBase64Chars", new IntExpression(new InvokeStaticMethodExpression(typeof(Convert), nameof(Convert.ToBase64CharArray), new[] { value, Int(0), valueLength, output, Int(0) })), out var numBase64Chars), + EmptyLine, + Declare("i", Int(0), out var i), + new ForStatement(null, LessThan(i, numBase64Chars), new UnaryOperatorExpression("++", i, true)) + { + Declare("ch", new CharExpression(new IndexerExpression(output, i)), out var ch), + new IfElseStatement(new IfStatement(Equal(ch, Literal('+'))) + { + Assign(new IndexerExpression(output, i), Literal('-')) + }, new IfElseStatement(new IfStatement(Equal(ch, Literal('/'))) + { + Assign(new IndexerExpression(output, i), Literal('_')) + }, new IfStatement(Equal(ch, Literal('='))) + { + Break + })) + }, + EmptyLine, + Return(New.Instance(typeof(string), output, Int(0), i)) + }); + + return new Method(signature, body); + } + + public StringExpression ToBase64UrlString(ValueExpression value) + => new(new InvokeStaticMethodExpression(Type, _toBase64UrlStringMethodName, new[] { value })); + + private const string _fromBase64UrlStringMethodName = "FromBase64UrlString"; + + private Method BuildFromBase64UrlString() + { + var valueParameter = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _fromBase64UrlStringMethodName, + Parameters: new[] { valueParameter }, + Modifiers: _methodModifiers, + ReturnType: typeof(byte[]), + Summary: null, Description: null, ReturnDescription: null); + var value = new StringExpression(valueParameter); + + var body = new List + { + Declare("paddingCharsToAdd", new IntExpression(new SwitchExpression(new BinaryOperatorExpression("%", value.Length, Literal(4)), new SwitchCaseExpression[] + { + new SwitchCaseExpression(Int(0), Int(0)), + new SwitchCaseExpression(Int(2), Int(2)), + new SwitchCaseExpression(Int(3), Int(1)), + SwitchCaseExpression.Default(ThrowExpression(New.InvalidOperationException(Literal("Malformed input")))) + })), out var paddingCharsToAdd) + }; + var output = new VariableReference(typeof(char[]), "output"); + var outputLength = output.Property("Length"); + body.Add(new MethodBodyStatement[] + { + Declare(output, New.Array(typeof(char), new BinaryOperatorExpression("+", value.Length, paddingCharsToAdd))), + Declare("i", Int(0), out var i), + new ForStatement(null, LessThan(i, value.Length), new UnaryOperatorExpression("++", i, true)) + { + Declare("ch", value.Index(i), out var ch), + new IfElseStatement(new IfStatement(Equal(ch, Literal('-'))) + { + Assign(new IndexerExpression(output, i), Literal('+')) + }, new IfElseStatement(new IfStatement(Equal(ch, Literal('_'))) + { + Assign(new IndexerExpression(output, i), Literal('/')) + }, Assign(new IndexerExpression(output, i), ch))) + }, + EmptyLine, + new ForStatement(null, LessThan(i, outputLength), new UnaryOperatorExpression("++", i, true)) + { + Assign(new IndexerExpression(output, i), Literal('=')) + }, + EmptyLine, + Return(new InvokeStaticMethodExpression(typeof(Convert), nameof(Convert.FromBase64CharArray), new[] { output, Int(0), outputLength })) + }); + + return new Method(signature, body); + } + + public ValueExpression FromBase64UrlString(ValueExpression value) + => new InvokeStaticMethodExpression(Type, _fromBase64UrlStringMethodName, new[] { value }); + + private const string _parseDateTimeOffsetMethodName = "ParseDateTimeOffset"; + private Method BuildParseDateTimeOffsetMethod() + { + var valueParameter = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _parseDateTimeOffsetMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { valueParameter, formatParameter }, + ReturnType: typeof(DateTimeOffset), + Summary: null, Description: null, ReturnDescription: null); + + var value = new StringExpression(valueParameter); + var format = new StringExpression(formatParameter); + var invariantCulture = new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture)); + return new Method(signature, + new SwitchExpression(format, new SwitchCaseExpression[] + { + new(Literal("U"), DateTimeOffsetExpression.FromUnixTimeSeconds(LongExpression.Parse(value, invariantCulture))), + SwitchCaseExpression.Default(DateTimeOffsetExpression.Parse(value, invariantCulture, new MemberExpression(typeof(DateTimeStyles), nameof(DateTimeStyles.AssumeUniversal)))) + })); + } + + public DateTimeOffsetExpression ParseDateTimeOffset(ValueExpression value, ValueExpression format) + => new(new InvokeStaticMethodExpression(Type, _parseDateTimeOffsetMethodName, new[] { value, format })); + + private const string _parseTimeSpanMethodName = "ParseTimeSpan"; + private Method BuildParseTimeSpanMethod() + { + var valueParameter = new Parameter("value", null, typeof(string), null, ValidationType.None, null); + var formatParameter = new Parameter("format", null, typeof(string), null, ValidationType.None, null); + var signature = new MethodSignature( + Name: _parseTimeSpanMethodName, + Modifiers: _methodModifiers, + Parameters: new[] { valueParameter, formatParameter }, + ReturnType: typeof(TimeSpan), + Summary: null, Description: null, ReturnDescription: null); + + var value = new StringExpression(valueParameter); + var format = new StringExpression(formatParameter); + return new Method(signature, + new SwitchExpression(format, new SwitchCaseExpression[] + { + new(Literal("P"), new InvokeStaticMethodExpression(typeof(XmlConvert), nameof(XmlConvert.ToTimeSpan), new[]{ value })), + SwitchCaseExpression.Default(TimeSpanExpression.ParseExact(value, format, new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture)))) + })); + } + + public TimeSpanExpression ParseTimeSpan(ValueExpression value, ValueExpression format) + => new(new InvokeStaticMethodExpression(Type, _parseTimeSpanMethodName, new[] { value, format })); + + private const string _convertToStringMethodName = "ConvertToString"; + private Method BuildConvertToStringMethod() + { + var valueParameter = new Parameter("value", null, typeof(object), null, ValidationType.None, null); + var nullableStringType = new CSharpType(typeof(string), true); + var formatParameter = new Parameter("format", null, nullableStringType, Constant.Default(nullableStringType), ValidationType.None, null); + var signature = new MethodSignature( + Name: _convertToStringMethodName, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + Parameters: new[] { valueParameter, formatParameter }, + ReturnType: typeof(string), + Summary: null, Description: null, ReturnDescription: null); + + var value = (ValueExpression)valueParameter; + var format = new StringExpression(formatParameter); + var body = new SwitchExpression(value, new SwitchCaseExpression[] + { + new SwitchCaseExpression(Null, Literal("null")), + new SwitchCaseExpression(new DeclarationExpression(typeof(string), "s", out var s), s), + new SwitchCaseExpression(new DeclarationExpression(typeof(bool), "b", out var b), ToString(b)), + new SwitchCaseExpression(GetTypePattern(new CSharpType[] {typeof(int),typeof(float), typeof(double), typeof(long), typeof(decimal)}), value.CastTo(typeof(IFormattable)).Invoke(nameof(IFormattable.ToString), _defaultNumberFormatField, _invariantCultureExpression)), + // TODO -- figure out how to write this line + SwitchCaseExpression.When(new DeclarationExpression(typeof(byte[]), "b", out var bytes), NotEqual(format, Null), ToString(bytes, format)), + new SwitchCaseExpression(new DeclarationExpression(typeof(IEnumerable), "s", out var enumerable), StringExpression.Join(Literal(","), enumerable)), + SwitchCaseExpression.When(new DeclarationExpression(typeof(DateTimeOffset), "dateTime", out var dateTime), NotEqual(format, Null), ToString(dateTime, format)), + SwitchCaseExpression.When(new DeclarationExpression(typeof(TimeSpan), "timeSpan", out var timeSpan), NotEqual(format, Null), ToString(timeSpan, format)), + new SwitchCaseExpression(new DeclarationExpression(typeof(TimeSpan), "timeSpan", out var timeSpanNoFormat), new InvokeStaticMethodExpression(typeof(XmlConvert), nameof(XmlConvert.ToString), new[] { timeSpanNoFormat })), + new SwitchCaseExpression(new DeclarationExpression(typeof(Guid), "guid", out var guid), guid.InvokeToString()), + new SwitchCaseExpression(new DeclarationExpression(typeof(BinaryData), "binaryData", out var binaryData), ConvertToString(new BinaryDataExpression(binaryData).ToArray(), format)), + SwitchCaseExpression.Default(value.InvokeToString()) + }); + + return new(signature, body); + } + + private static ValueExpression GetTypePattern(IReadOnlyList types) + { + ValueExpression result = types[^1]; + + for (int i = types.Count - 2; i >= 0; i--) + { + result = new BinaryOperatorExpression(" or ", types[i], result); // chain them together + } + + return result; + } + + public StringExpression ConvertToString(ValueExpression value, ValueExpression? format = null) + { + var arguments = format != null + ? new[] { value, format } + : new[] { value }; + return new(new InvokeStaticMethodExpression(Type, _convertToStringMethodName, arguments)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/Utf8JsonRequestContentProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/Utf8JsonRequestContentProvider.cs new file mode 100644 index 0000000..3267086 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/HelperTypeProviders/Utf8JsonRequestContentProvider.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.System; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class Utf8JsonRequestContentProvider : ExpressionTypeProvider + { + private static readonly Lazy _instance = new Lazy(() => new Utf8JsonRequestContentProvider()); + public static Utf8JsonRequestContentProvider Instance => _instance.Value; + + private Utf8JsonRequestContentProvider() : base(Configuration.HelperNamespace, null) + { + DeclarationModifiers = TypeSignatureModifiers.Internal; + DefaultName = $"Utf8Json{Configuration.ApiTypes.RequestContentType.Name}"; + Inherits = Configuration.ApiTypes.RequestContentType; + + _streamField = new FieldDeclaration( + modifiers: FieldModifiers.Private | FieldModifiers.ReadOnly, + type: typeof(MemoryStream), + name: "_stream"); + _contentField = new FieldDeclaration( + modifiers: FieldModifiers.Private | FieldModifiers.ReadOnly, + type: Inherits, + name: "_content"); + _writerProperty = new PropertyDeclaration( + description: null, + modifiers: MethodSignatureModifiers.Public, + propertyType: typeof(Utf8JsonWriter), + name: _jsonWriterName, + propertyBody: new AutoPropertyBody(false)); + } + + private readonly FieldDeclaration _streamField; + private readonly FieldDeclaration _contentField; + private readonly PropertyDeclaration _writerProperty; + + protected override string DefaultName { get; } + + protected override IEnumerable BuildFields() + { + yield return _streamField; + yield return _contentField; + } + + protected override IEnumerable BuildProperties() + { + yield return _writerProperty; + } + + protected override IEnumerable BuildConstructors() + { + var signature = new ConstructorSignature( + Type: Type, + Modifiers: MethodSignatureModifiers.Public, + Parameters: Array.Empty(), + Summary: null, Description: null); + + var stream = (ValueExpression)_streamField; + var content = (ValueExpression)_contentField; + var writer = (ValueExpression)_writerProperty; + var body = new MethodBodyStatement[] + { + Assign(stream, New.Instance(typeof(MemoryStream))), + Assign(content, new InvokeStaticMethodExpression(null, Configuration.ApiTypes.RequestContentCreateName, new[] { stream })), + Assign(writer, New.Instance(typeof(Utf8JsonWriter), stream)) + }; + yield return new Method(signature, body); + } + + protected override IEnumerable BuildMethods() + { + yield return BuildWriteToAsyncMethod(); + yield return BuildWriteToMethod(); + yield return BuildTryComputeLengthMethod(); + yield return BuildDisposeMethod(); + } + + public Utf8JsonWriterExpression JsonWriterProperty(ValueExpression instance) + => new(instance.Property(_jsonWriterName)); + + private const string _jsonWriterName = "JsonWriter"; + + private static readonly string WriteToAsync = Configuration.IsBranded ? nameof(RequestContent.WriteToAsync) : nameof(BinaryContent.WriteToAsync); + private static readonly string WriteTo = Configuration.IsBranded ? nameof(RequestContent.WriteTo) : nameof(BinaryContent.WriteTo); + private static readonly string TryComputeLength = Configuration.IsBranded ? nameof(RequestContent.TryComputeLength) : nameof(BinaryContent.TryComputeLength); + private static readonly string Dispose = nameof(IDisposable.Dispose); + + private Method BuildWriteToAsyncMethod() + { + var signature = new MethodSignature( + Name: WriteToAsync, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override | MethodSignatureModifiers.Async, + ReturnType: typeof(Task), + Parameters: new[] { _streamParameter, KnownParameters.CancellationTokenParameter }, + Summary: null, Description: null, ReturnDescription: null); + + var writer = new Utf8JsonWriterExpression(_writerProperty); + var stream = new StreamExpression(_streamParameter); + var content = (ValueExpression)_contentField; + var cancellationToken = (ValueExpression)KnownParameters.CancellationTokenParameter; + var body = new MethodBodyStatement[] + { + writer.FlushAsync(), + new InvokeInstanceMethodStatement(content, WriteToAsync, new[] { stream, cancellationToken }, true) + }; + + return new Method(signature, body); + } + + private Method BuildWriteToMethod() + { + var signature = new MethodSignature( + Name: WriteTo, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: null, + Parameters: new[] { _streamParameter, KnownParameters.CancellationTokenParameter }, + Summary: null, Description: null, ReturnDescription: null); + + var writer = new Utf8JsonWriterExpression(_writerProperty); + var stream = new StreamExpression(_streamParameter); + var content = (ValueExpression)_contentField; + var cancellationToken = (ValueExpression)KnownParameters.CancellationTokenParameter; + var body = new MethodBodyStatement[] + { + writer.Flush(), + new InvokeInstanceMethodStatement(content, WriteTo, new[] { stream, cancellationToken }, false) + }; + + return new Method(signature, body); + } + + private Method BuildTryComputeLengthMethod() + { + var lengthParameter = new Parameter("length", null, typeof(long), null, ValidationType.None, null, IsOut: true); + var signature = new MethodSignature( + Name: TryComputeLength, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + Parameters: new[] { lengthParameter }, + ReturnType: typeof(bool), + Summary: null, Description: null, ReturnDescription: null); + + var length = (ValueExpression)lengthParameter; + var writer = new Utf8JsonWriterExpression(_writerProperty); + var body = new MethodBodyStatement[] + { + Assign(length, new BinaryOperatorExpression("+", writer.BytesCommitted, writer.BytesPending)), + Return(True) + }; + + return new Method(signature, body); + } + + private Method BuildDisposeMethod() + { + var signature = new MethodSignature( + Name: Dispose, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + Parameters: Array.Empty(), + ReturnType: null, + Summary: null, Description: null, ReturnDescription: null); + + var stream = new StreamExpression(_streamField); + TypedValueExpression content = Configuration.IsBranded + ? new RequestContentExpression(_contentField) + : new BinaryContentExpression(_contentField); + var writer = new Utf8JsonWriterExpression(_writerProperty); + var body = new MethodBodyStatement[] + { + InvokeDispose(writer), + InvokeDispose(content), + InvokeDispose(stream) + }; + return new Method(signature, body); + + static MethodBodyStatement InvokeDispose(TypedValueExpression instance) + => new InvokeInstanceMethodStatement(instance, Dispose); + } + + private readonly Parameter _streamParameter = new Parameter("stream", null, typeof(Stream), null, ValidationType.None, null); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/JsonConverterProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/JsonConverterProvider.cs new file mode 100644 index 0000000..2d7b9ed --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/JsonConverterProvider.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class JsonConverterProvider : ExpressionTypeProvider + { + private readonly bool _includeSerializer; + private readonly bool _includeDeserializer; + private readonly SerializableObjectType _model; + + public JsonConverterProvider(SerializableObjectType model, SourceInputModel? sourceInputModel) : base(model.Declaration.Namespace, sourceInputModel) + { + _model = model; + _includeSerializer = model.IncludeSerializer; + _includeDeserializer = model.IncludeDeserializer; + DefaultName = $"{model.Declaration.Name}Converter"; + Inherits = new CSharpType(typeof(JsonConverter<>), model.Type); + } + + protected override string DefaultName { get; } + + protected override string DefaultAccessibility => "internal"; + + protected override IEnumerable BuildMethods() + { + var modelParameter = new Parameter("model", null, _model.Type, null, ValidationType.None, null); + var writeMethodSignature = new MethodSignature( + Name: nameof(JsonConverter.Write), + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: null, + ReturnDescription: null, + Parameters: new[] + { + KnownParameters.Serializations.Utf8JsonWriter, + modelParameter, + KnownParameters.Serializations.JsonOptions + }); + var writer = new Utf8JsonWriterExpression(KnownParameters.Serializations.Utf8JsonWriter); + MethodBodyStatement writeMethodBody; + if (_includeSerializer) + { + writeMethodBody = writer.WriteObjectValue(modelParameter, ModelReaderWriterOptionsExpression.Wire); + } + else + { + writeMethodBody = Throw(New.Instance(typeof(NotImplementedException))); + } + yield return new Method(writeMethodSignature, writeMethodBody); + + var readMethodSignature = new MethodSignature( + Name: nameof(JsonConverter.Read), + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: _model.Type, + ReturnDescription: null, + Parameters: new[] + { + KnownParameters.Serializations.Utf8JsonReader, + new Parameter("typeToConvert", null, typeof(Type), null, ValidationType.None, null), + KnownParameters.Serializations.JsonOptions + }); + MethodBodyStatement readMethodBody; + if (_includeDeserializer) + { + readMethodBody = new MethodBodyStatement[] + { + UsingVar("document", JsonDocumentExpression.ParseValue(KnownParameters.Serializations.Utf8JsonReader), out var doc), + Return(SerializableObjectTypeExpression.Deserialize(_model, doc.RootElement)) + }; + } + else + { + readMethodBody = Throw(New.Instance(typeof(NotImplementedException))); + } + yield return new Method(readMethodSignature, readMethodBody); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/MemberDeclarationOptions.cs b/logger/autorest.csharp/common/Output/Models/Types/MemberDeclarationOptions.cs new file mode 100644 index 0000000..a68681c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/MemberDeclarationOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class MemberDeclarationOptions + { + public MemberDeclarationOptions(string accessibility, string name, CSharpType type) + { + Accessibility = accessibility; + Name = name; + Type = type; + } + + public string Accessibility { get; } + public string Name { get; } + public CSharpType Type { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ModelFactoryTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/ModelFactoryTypeProvider.cs new file mode 100644 index 0000000..f7ce868 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ModelFactoryTypeProvider.cs @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.KnownCodeBlocks; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.Core.Expressions.DataFactory; +using Azure.ResourceManager.Models; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using Microsoft.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal sealed class ModelFactoryTypeProvider : TypeProvider + { + protected override string DefaultName { get; } + protected override string DefaultAccessibility { get; } + + // TODO: remove this intermediate state once we generate it before output types + private IReadOnlyList? _methods; + + // This method should only be called from OutputMethods as intermediate state. + private IReadOnlyList ShouldNotBeUsedForOutput([CallerMemberName] string caller = "") + { + Debug.Assert(caller == nameof(Methods) || caller == nameof(SignatureType), $"This method should not be used for output. Caller: {caller}"); + return _methods ??= _models.Select(CreateMethod).ToList(); + } + + // TODO: remove this intermediate state once we generate it before output types + private IReadOnlyList? _outputMethods; + public IReadOnlyList Methods + { + get + { + if (SignatureType is null) + { + // The overloading feature is not enabled, we jsut return the original methods + return ShouldNotBeUsedForOutput(); + } + // filter out duplicate methods in custom code and combine overload methods + return _outputMethods ??= ShouldNotBeUsedForOutput().Where(x => !SignatureType.MethodsToSkip.Contains(x.Signature)).Concat(SignatureType.OverloadMethods).ToList(); + } + } + + public FormattableString Description => $"Model factory for models."; + + internal string FullName => $"{Type.Namespace}.{Type.Name}"; + + private readonly IEnumerable _models; + private readonly TypeFactory _typeFactory; + + private ModelFactoryTypeProvider(IEnumerable objectTypes, string defaultClientName, string defaultNamespace, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + : base(defaultNamespace, sourceInputModel) + { + _typeFactory = typeFactory; + _models = objectTypes; + DefaultName = $"{defaultClientName}ModelFactory".ToCleanName(); + DefaultAccessibility = "public"; + ExistingModelFactoryMethods = typeof(ResourceManagerModelFactory).GetMethods(BindingFlags.Static | BindingFlags.Public).ToHashSet(); + } + + public static ModelFactoryTypeProvider? TryCreate(IEnumerable models, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + { + if (!Configuration.GenerateModelFactory) + return null; + + var objectTypes = models.OfType() + .Where(RequiresModelFactory) + .ToArray(); + + if (!objectTypes.Any()) + { + return null; + } + + var defaultNamespace = GetDefaultNamespace(); + var defaultRPName = GetRPName(defaultNamespace); + + defaultNamespace = GetDefaultModelNamespace(null, defaultNamespace); + + return new ModelFactoryTypeProvider(objectTypes, defaultRPName, defaultNamespace, typeFactory, sourceInputModel); + } + + private static string GetRPName(string defaultNamespace) + { + // for mgmt plane packages, we always have the prefix `Arm` on the name of model factories, except for Azure.ResourceManager + var prefix = Configuration.AzureArm && !Configuration.MgmtConfiguration.IsArmCore ? "Arm" : string.Empty; + return $"{prefix}{ClientBuilder.GetRPName(defaultNamespace)}"; + } + + private static string GetDefaultNamespace() + { + // we have this because the Azure.ResourceManager package is generated using batch, which generates multiple times, and each time Configuration.Namespace has a different value. + if (Configuration.AzureArm && Configuration.MgmtConfiguration.IsArmCore) + return "Azure.ResourceManager"; + + return Configuration.Namespace; + } + + public HashSet ExistingModelFactoryMethods { get; } + + private SignatureType? _signatureType; + public override SignatureType? SignatureType + { + get + { + // This can only be used for Mgmt now, because there are custom/hand-written code in HLC can't be loaded into CsharpType such as generic methods + // TODO: enable this for DPG, and check Configuration.Generate1ConvenientClient to disable it for HLC + if (!Configuration.AzureArm) + { + return null; + } + return _signatureType ??= new SignatureType(_typeFactory, ShouldNotBeUsedForOutput().Select(x => (MethodSignature)x.Signature).ToList(), _sourceInputModel, Declaration.Namespace, Declaration.Name); + } + } + + private ValueExpression BuildPropertyAssignmentExpression(Parameter parameter, ObjectTypeProperty property) + { + ValueExpression p = parameter; + var propertyStack = property.BuildHierarchyStack(); + + if (propertyStack.Count == 1) + return p; + + var assignmentProperty = propertyStack.Last(); + Debug.Assert(assignmentProperty.FlattenedProperty != null); + + // determine whether this is a value type that changed to nullable because of other enclosing properties are nullable + var isOverriddenValueType = assignmentProperty.FlattenedProperty.IsOverriddenValueType; + + // iterate over the property stack to build a nested expression of variable assignment + ObjectTypeProperty immediateParentProperty; + property = propertyStack.Pop(); + // or .Value + ValueExpression result = isOverriddenValueType + ? p.NullableStructValue(parameter.Type) // when it is changed to nullable, we call .Value because its constructor will only take the non-nullable value + : p; + + CSharpType from = parameter.Type; + while (propertyStack.Count > 0) + { + immediateParentProperty = propertyStack.Pop(); + var parentPropertyType = immediateParentProperty.Declaration.Type; + switch (parentPropertyType) + { + case { IsFrameworkType: false, Implementation: SerializableObjectType serializableObjectType }: + // when a property is flattened, it should only have one property. But the serialization ctor might takes two parameters because it may have the raw data field as an extra parameter + var parameters = serializableObjectType.SerializationConstructor.Signature.Parameters; + var arguments = new List(); + // get the type of the first parameter of its ctor + var to = parameters[0].Type; + arguments.Add(result.GetConversion(from, to)); + // check if we need extra parameters for the raw data field + if (parameters.Count > 1) + { + // this parameter should be the raw data field, otherwise this property should not have been flattened in the first place + arguments.Add(new PositionalParameterReference(parameters[1].Name, Null)); + } + result = New.Instance(parentPropertyType, arguments.ToArray()); + break; + case { IsFrameworkType: false, Implementation: SystemObjectType systemObjectType }: + // for the case of SystemObjectType, the serialization constructor is internal and the definition of this class might be outside of this assembly, we need to use its corresponding model factory to construct it + // find the method in the list + var method = ExistingModelFactoryMethods.First(m => m.Name == systemObjectType.Type.Name); + result = new InvokeStaticMethodExpression(method.DeclaringType!, method.Name, new[] { result }); + break; + default: + throw new InvalidOperationException($"The propertyType {parentPropertyType} (implementation type: {parentPropertyType.Implementation.GetType()}) is unhandled here, this should never happen"); + } + + // change the from type to the current type + property = immediateParentProperty; + from = parentPropertyType; // since this is the property type of the immediate parent property, we should never get another valid conversion + } + + if (assignmentProperty.FlattenedProperty != null) + { + if (isOverriddenValueType) + result = new TernaryConditionalOperator( + p.Property(nameof(Nullable.HasValue)), + result, + Null); + else if (parameter.Type.IsNullable) + result = new TernaryConditionalOperator( + NotEqual(p, Null), + result, + Null); + } + + return result; + } + + private Method CreateMethod(SerializableObjectType model) + { + var ctor = model.SerializationConstructor; + var ctorToCall = ctor; + var discriminator = model.Discriminator; + if (model.Declaration.IsAbstract && discriminator != null) + { + // the model factory entry method `RequiresModelFactory` makes sure this: if this model is abstract, the discriminator must not be null + ctorToCall = discriminator.DefaultObjectType.SerializationConstructor; + } + var methodParameters = new List(ctor.Signature.Parameters.Count); + var methodArguments = new List(ctor.Signature.Parameters.Count); + + foreach (var ctorParameter in ctorToCall.Signature.Parameters) + { + var property = ctorToCall.FindPropertyInitializedByParameter(ctorParameter); + if (property == null) + { + // if the property is not found, in order not to introduce compilation errors, we need to add a `default` into the argument list + methodArguments.Add(new PositionalParameterReference(ctorParameter.Name, Default)); + continue; + } + + if (ctorParameter.IsRawData) + { + // we do not want to include the raw data as a parameter of the model factory entry method, therefore here we skip the parameter, and use empty dictionary as argument + methodArguments.Add(new PositionalParameterReference(ctorParameter.Name, Null)); + continue; + } + + if (property.FlattenedProperty != null) + property = property.FlattenedProperty; + + var parameterName = property.Declaration.Name.ToVariableName(); + var inputType = property.Declaration.Type; + // check if the property is the discriminator, but skip the check if the configuration is on for HLC only + if (discriminator != null && discriminator.Property == property && !Configuration.ModelFactoryForHlc.Contains(model.Declaration.Name)) + { + if (discriminator.Value is { } value) + { + // this is a derived class, we do not add this parameter to the method, but we need an argument for the invocation + methodArguments.Add(new ConstantExpression(value)); + continue; + } + // this class is the base in a discriminated set + switch (inputType) + { + case { IsFrameworkType: false, Implementation: EnumType { IsExtensible: true } extensibleEnum }: + inputType = extensibleEnum.ValueType; + break; + case { IsFrameworkType: false, Implementation: EnumType { IsExtensible: false } }: + // we skip the parameter if the discriminator is a sealed choice because we can never pass in a "Unknown" value. + // but we still need to add it to the method argument list as a `default` + methodArguments.Add(Default); + continue; + default: + break; + } + } + + inputType = inputType.InputType; + if (!inputType.IsValueType) + { + inputType = inputType.WithNullable(true); + } + + var parameter = ctorParameter with + { + Name = parameterName, + Type = inputType, + DefaultValue = Constant.Default(inputType), + Initializer = inputType.GetParameterInitializer(ctorParameter.DefaultValue) + }; + + methodParameters.Add(parameter); + + var expression = BuildPropertyAssignmentExpression(parameter, property).GetConversion(parameter.Type, ctorParameter.Type); + methodArguments.Add(expression); + } + + FormattableString returnDescription = $"A new {model.Type:C} instance for mocking."; + + var signature = new MethodSignature( + ctor.Signature.Name, + ctor.Signature.Summary, + ctor.Signature.Description, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + model.Type, + returnDescription, + methodParameters); + + var methodBody = new MethodBodyStatement[] + { + // write the initializers and validations + new ParameterValidationBlock(methodParameters, true), + Return(New.Instance(ctorToCall.Signature, methodArguments)) + }; + + return new(signature, methodBody); + } + + private static bool RequiresModelFactory(SerializableObjectType model) + { + if (model.Declaration.Accessibility != "public" || !model.IncludeDeserializer) + { + return false; + } + + if (model.Declaration.IsAbstract && model.Discriminator == null) + { + return false; + } + + var properties = model.EnumerateHierarchy().SelectMany(obj => obj.Properties.Where(p => p != (obj as SerializableObjectType)?.RawDataField)).ToArray(); + // we skip models with internal properties when the internal property is neither a discriminator or safe flattened + if (properties.Any(p => p.Declaration.Accessibility != "public" && model.Discriminator?.Property != p && p.FlattenedProperty == null)) + { + return false; + } + + // We skip models that don't have read-only properties other than discriminator or collections + // While discriminator property is generated as read-write, it can be made read-only via customization + if (!properties.Any(p => p.IsReadOnly && model.Discriminator?.Property != p && !p.ValueType.IsReadWriteDictionary && !p.ValueType.IsReadWriteList)) + { + return false; + } + + if (model.SerializationConstructor.Signature.Parameters.Any(p => !p.Type.IsPublic)) + { + return false; + } + + return model.Constructors + .Where(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + .All(c => properties.Any(property => c.FindParameterByInitializedProperty(property) == default)); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ModelTypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/ModelTypeProvider.cs new file mode 100644 index 0000000..ff19600 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ModelTypeProvider.cs @@ -0,0 +1,814 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Serialization.Multipart; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal sealed class ModelTypeProvider : SerializableObjectType + { + private ModelTypeProviderFields? _fields; + private ConstructorSignature? _publicConstructor; + private ConstructorSignature? _serializationConstructor; + private ObjectTypeProperty? _additionalPropertiesProperty; + private readonly InputModelTypeUsage _inputModelUsage; + private readonly InputTypeSerialization _inputModelSerialization; + private readonly InputModelType _inputModel; + private readonly TypeFactory _typeFactory; + private readonly IReadOnlyList _derivedModels; + private readonly SerializableObjectType? _defaultDerivedType; + + protected override string DefaultName { get; } + protected override string DefaultAccessibility { get; } + public bool IsAccessibilityOverridden { get; } + + protected override bool IsAbstract => !Configuration.SuppressAbstractBaseClasses.Contains(DefaultName) && _inputModel.DiscriminatorProperty is not null && _inputModel.DiscriminatorValue is null; + + public ModelTypeProviderFields Fields => _fields ??= new ModelTypeProviderFields(UpdateInputModelProperties(), Declaration.Name, _inputModelUsage, _typeFactory, ModelTypeMapping, _inputModel.AdditionalProperties, IsStruct, _inputModel.IsPropertyBag); + private ConstructorSignature InitializationConstructorSignature => _publicConstructor ??= EnsurePublicConstructorSignature(); + private ConstructorSignature SerializationConstructorSignature => _serializationConstructor ??= EnsureSerializationConstructorSignature(); + + public override ObjectTypeProperty? AdditionalPropertiesProperty => _additionalPropertiesProperty ??= EnsureAdditionalPropertiesProperty(); + + private ObjectTypeProperty? EnsureAdditionalPropertiesProperty() + => Fields.AdditionalProperties is { } additionalPropertiesField + ? new ObjectTypeProperty(additionalPropertiesField, null) + : null; + + private ObjectTypeProperty? _rawDataField; + protected internal override InputModelTypeUsage GetUsage() => _inputModelUsage; + + public override ObjectTypeProperty? RawDataField + { + get + { + if (_rawDataField != null) + return _rawDataField; + + if (!ShouldHaveRawData) + return null; + + // if we have an additional properties property, and its value type is also BinaryData, we should not have a raw data field + if (AdditionalPropertiesProperty != null) + { + var valueType = AdditionalPropertiesProperty.Declaration.Type.ElementType; + if (valueType.EqualsIgnoreNullable(_typeFactory.UnknownType)) + { + return null; + } + } + + // when the configuration is enabled, generate this as internal + // when this model has derived types, the accessibility should change from private to `protected internal` + string accessibility = Configuration.EnableInternalRawData + ? "internal" + : HasDerivedTypes() ? "private protected" : "private"; + + _rawDataField = new ObjectTypeProperty( + BuilderHelpers.CreateMemberDeclaration( + Configuration.EnableInternalRawData ? InternalAdditionalPropertiesPropertyName : PrivateAdditionalPropertiesPropertyName, + _privateAdditionalPropertiesPropertyType, accessibility, null, _typeFactory), + PrivateAdditionalPropertiesPropertyDescription, + true, + null); + + return _rawDataField; + } + } + + private bool HasDerivedTypes() + { + if (_derivedModels.Any()) + return true; + + if (_inputModel.DiscriminatorProperty is not null) + return true; + + return false; + } + + public ModelTypeProvider(InputModelType inputModel, string defaultNamespace, SourceInputModel? sourceInputModel, TypeFactory typeFactory, SerializableObjectType? defaultDerivedType = null) + : base(defaultNamespace, sourceInputModel) + { + DefaultName = inputModel.Name.ToCleanName(); + DefaultAccessibility = inputModel.Access ?? "public"; + IsAccessibilityOverridden = inputModel.Access != null; + + _typeFactory = typeFactory; + _inputModel = inputModel; + _deprecation = inputModel.Deprecation; + _derivedModels = inputModel.DerivedModels; + _defaultDerivedType = inputModel.DerivedModels.Any() && inputModel.BaseModel is { DiscriminatorProperty: not null } + ? this //if I have children and parents then I am my own defaultDerivedType + : defaultDerivedType ?? (inputModel.IsUnknownDiscriminatorModel ? this : null); + + _inputModelUsage = UpdateInputModelUsage(inputModel, ModelTypeMapping); + _inputModelSerialization = UpdateInputSerialization(inputModel, ModelTypeMapping); + + IsPropertyBag = inputModel.IsPropertyBag; + IsUnknownDerivedType = inputModel.IsUnknownDiscriminatorModel; + SkipInitializerConstructor = IsUnknownDerivedType; + + JsonConverter = + _inputModel.UseSystemTextJsonConverter || (ModelTypeMapping?.UseSystemTextJsonConverter == true) + ? new JsonConverterProvider(this, _sourceInputModel) + : null; + } + + private static InputModelTypeUsage UpdateInputModelUsage(InputModelType inputModel, ModelTypeMapping? modelTypeMapping) + { + var usage = inputModel.Usage; + if (modelTypeMapping?.Usage is { } usageDefinedInSource) + { + foreach (var schemaTypeUsage in usageDefinedInSource.Select(u => Enum.Parse(u, true))) + { + usage |= schemaTypeUsage switch + { + SchemaTypeUsage.Input => InputModelTypeUsage.Input, + SchemaTypeUsage.Output => InputModelTypeUsage.Output, + SchemaTypeUsage.RoundTrip => InputModelTypeUsage.Input | InputModelTypeUsage.Output, + _ => InputModelTypeUsage.None + }; + } + } + + return usage; + } + + private static InputTypeSerialization UpdateInputSerialization(InputModelType inputModel, ModelTypeMapping? modelTypeMapping) + { + var serialization = inputModel.Serialization; + + if (modelTypeMapping?.Formats is { } formatsDefinedInSource) + { + foreach (var format in formatsDefinedInSource) + { + var mediaType = Enum.Parse(format, true); + if (mediaType == KnownMediaType.Json) + { + serialization = serialization with { Json = true }; + } + else if (mediaType == KnownMediaType.Xml) + { + serialization = serialization with { Xml = new InputTypeXmlSerialization(inputModel.Name, false, false, false) }; + } + } + } + + return serialization; + } + + private IReadOnlyList UpdateInputModelProperties() + { + if (_inputModel.BaseModel is not { } baseModel) + { + return _inputModel.Properties; + } + + var existingBaseType = GetSourceBaseType(); + // If base type in custom code is different from the current base type, we need to replace the base type and handle the properties accordingly + if (existingBaseType is not null && existingBaseType.Name != baseModel.Name && !SymbolEqualityComparer.Default.Equals(_sourceInputModel?.FindForType(Declaration.Namespace, baseModel.Name.ToCleanName()), existingBaseType)) + { + var properties = _inputModel.Properties.ToList(); + + // Add all properties in the hierarchy of current base type + var currentBaseModelProperties = baseModel.GetSelfAndBaseModels().SelectMany(m => m.Properties); + properties.AddRange(currentBaseModelProperties); + + return properties; + } + + return _inputModel.Properties; + } + + private MethodSignatureModifiers GetFromResponseModifiers() + { + var signatures = MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static; + var parent = GetBaseObjectType(); + if (parent is ModelTypeProvider parentModelType) + { + if (parentModelType.Methods.Any(m => m.Signature.Name == Configuration.ApiTypes.FromResponseName)) + signatures |= MethodSignatureModifiers.New; + } + return signatures; + } + + private MethodSignatureModifiers GetToRequestContentModifiers() + { + //TODO if no one inherits from this we can omit the virtual + var signatures = MethodSignatureModifiers.Internal; + // structs cannot have virtual members + if (IsStruct) + { + return signatures; + } + + var parent = GetBaseObjectType(); + if (parent is null) + { + signatures |= MethodSignatureModifiers.Virtual; + } + else if (parent is ModelTypeProvider parentModelType) + { + signatures |= (parentModelType.Methods.Any(m => m.Signature.Name == Configuration.ApiTypes.ToRequestContentName)) + ? MethodSignatureModifiers.Override + : MethodSignatureModifiers.Virtual; + } + return signatures; + } + + protected override FormattableString CreateDescription() + { + return string.IsNullOrEmpty(_inputModel.Description) ? $"The {_inputModel.Name}." : FormattableStringHelpers.FromString(BuilderHelpers.EscapeXmlDocDescription(_inputModel.Description)); + } + + private ConstructorSignature EnsurePublicConstructorSignature() + { + //get base public ctor params + GetConstructorParameters(Fields.PublicConstructorParameters, out var fullParameterList, out var baseInitializers, true); + + var accessibility = IsAbstract + ? MethodSignatureModifiers.Protected + : _inputModelUsage.HasFlag(InputModelTypeUsage.Input) + ? MethodSignatureModifiers.Public + : MethodSignatureModifiers.Internal; + + return new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + null, + accessibility, + fullParameterList, + Initializer: new(true, baseInitializers)); + } + + private ConstructorSignature EnsureSerializationConstructorSignature() + { + // if there is additional properties property, we need to append it to the parameter list + var parameters = Fields.SerializationParameters; + if (RawDataField != null) + { + var deserializationParameter = new Parameter( + RawDataField.Declaration.Name.ToVariableName(), + RawDataField.ParameterDescription, + RawDataField.Declaration.Type, + null, + ValidationType.None, + null + ) + { + IsRawData = true + }; + parameters = parameters.Append(deserializationParameter).ToList(); + } + + //get base public ctor params + GetConstructorParameters(parameters, out var fullParameterList, out var baseInitializers, false); + + return new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + null, + MethodSignatureModifiers.Internal, + fullParameterList, + Initializer: new(true, baseInitializers)); + } + + private void GetConstructorParameters(IEnumerable parameters, out IReadOnlyList fullParameterList, out IReadOnlyList baseInitializers, bool isInitializer) + { + var parameterList = new List(); + var parent = GetBaseObjectType(); + baseInitializers = Array.Empty(); + if (parent is not null) + { + var discriminatorParameterName = Discriminator?.Property.Declaration.Name.ToVariableName(); + var ctor = isInitializer ? parent.InitializationConstructor : parent.SerializationConstructor; + var baseParameters = new List(); + var baseParameterInitializers = new List(); + foreach (var p in ctor.Signature.Parameters) + { + // we check if we should have the discriminator to our ctor only when we are building the initialization ctor + if (isInitializer && IsDiscriminatorInheritedOnBase && p.Name == discriminatorParameterName) + { + // if this is base + if (Discriminator is { Value: null }) + { + baseParameterInitializers.Add(p); // pass it through + baseParameters.Add(p); + } + // if this is derived or unknown + else if (Discriminator is { Value: { } value }) + { + baseParameterInitializers.Add(new ConstantExpression(value)); + // do not add it into the list + } + else + { + throw new InvalidOperationException($"We have a inherited discriminator, but the discriminator is null, this will never happen"); + } + } + else if (p.IsRawData && AdditionalPropertiesProperty != null) + { + baseParameterInitializers.Add(Snippets.Null); + // do not add it into the list + } + else + { + baseParameterInitializers.Add(p); + baseParameters.Add(p); + } + } + parameterList.AddRange(baseParameters); + baseInitializers = baseParameterInitializers; + } + + parameterList.AddRange(parameters.Select(p => p with { Description = $"{p.Description}{BuilderHelpers.CreateDerivedTypesDescription(p.Type)}" })); + fullParameterList = parameterList.DistinctBy(p => p.Name).ToArray(); // we filter out the parameters with the same name since we might have the same property both in myself and my base. + } + + private bool? _isDiscriminatorInheritedOnBase; + internal bool IsDiscriminatorInheritedOnBase => _isDiscriminatorInheritedOnBase ??= EnsureIsDiscriminatorInheritedOnBase(); + + private bool EnsureIsDiscriminatorInheritedOnBase() + { + if (Discriminator is null) + { + return false; + } + + if (Discriminator is { Value: not null }) + { + var parent = GetBaseObjectType() as ModelTypeProvider; + return parent?.IsDiscriminatorInheritedOnBase ?? false; + } + + var property = Discriminator.Property; + // if the property corresponding to the discriminator could not be found on this type, it means we are inheriting the discriminator + return !Properties.Contains(property); + } + + protected override ObjectTypeConstructor BuildInitializationConstructor() + { + ObjectTypeConstructor? baseCtor = GetBaseObjectType()?.InitializationConstructor; + + return new ObjectTypeConstructor(InitializationConstructorSignature, GetPropertyInitializers(InitializationConstructorSignature.Parameters, true), baseCtor); + } + + protected override ObjectTypeConstructor BuildSerializationConstructor() + { + // the property bag never needs deserialization, therefore we return the initialization constructor here so that we do not write it in the generated code + if (IsPropertyBag) + return InitializationConstructor; + + // verifies the serialization ctor has the same parameter list as the public one, we return the initialization ctor + if (!SerializationConstructorSignature.Parameters.Any(p => p.Type.IsList) && InitializationConstructorSignature.Parameters.SequenceEqual(SerializationConstructorSignature.Parameters, Parameter.EqualityComparerByType)) + return InitializationConstructor; + + ObjectTypeConstructor? baseCtor = GetBaseObjectType()?.SerializationConstructor; + + return new ObjectTypeConstructor(SerializationConstructorSignature, GetPropertyInitializers(SerializationConstructorSignature.Parameters, false), baseCtor); + } + + private IReadOnlyList GetPropertyInitializers(IReadOnlyList parameters, bool isInitializer) + { + List defaultCtorInitializers = new(); + + // only initialization ctor initializes the discriminator + // and we should not initialize the discriminator again when the discriminator is inherited (it should show up in the ctor) + // [TODO]: Consolidate property initializer generation between HLC and DPG + if (!Configuration.Generation1ConvenienceClient && isInitializer && !IsDiscriminatorInheritedOnBase && Discriminator is { Value: { } discriminatorValue } && !IsUnknownDerivedType) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(Discriminator.Property, discriminatorValue)); + } + + Dictionary parameterMap = parameters.ToDictionary( + parameter => parameter.Name, + parameter => parameter); + + foreach (var property in Properties) + { + // we do not need to add initialization for raw data field + if (isInitializer && property == RawDataField) + { + continue; + } + + ReferenceOrConstant? initializationValue = null; + Constant? defaultInitializationValue = null; + + var propertyType = property.Declaration.Type; + if (parameterMap.TryGetValue(property.Declaration.Name.ToVariableName(), out var parameter) || IsStruct) + { + // For structs all properties become required + Constant? defaultParameterValue = null; + if (property.InputModelProperty?.DefaultValue is { } defaultValueObject) + { + defaultParameterValue = BuilderHelpers.ParseConstant(defaultValueObject, propertyType); + defaultInitializationValue = defaultParameterValue; + } + + var inputType = parameter?.Type ?? propertyType.InputType; + if (defaultParameterValue != null && !property.ValueType.CanBeInitializedInline(defaultParameterValue)) + { + inputType = inputType.WithNullable(true); + defaultParameterValue = Constant.Default(inputType); + } + + var validate = property.InputModelProperty?.Type is not InputNullableType && !inputType.IsValueType ? ValidationType.AssertNotNull : ValidationType.None; + var defaultCtorParameter = new Parameter( + property.Declaration.Name.ToVariableName(), + property.ParameterDescription, + inputType, + defaultParameterValue, + validate, + null + ); + + initializationValue = defaultCtorParameter; + } + else if (initializationValue == null && propertyType.IsCollection) + { + if (propertyType.IsReadOnlyMemory) + { + initializationValue = propertyType.IsNullable ? null : Constant.FromExpression($"{propertyType}.{nameof(ReadOnlyMemory.Empty)}", propertyType); + } + else + { + initializationValue = Constant.NewInstanceOf(propertyType.PropertyInitializationType); + } + } + // [TODO]: Consolidate property initializer generation between HLC and DPG + else if (property.InputModelProperty?.ConstantValue is { } constant && !propertyType.IsNullable && Configuration.Generation1ConvenienceClient) + { + initializationValue = BuilderHelpers.ParseConstant(constant.Value, propertyType); + } + + if (initializationValue != null) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(property, initializationValue.Value, defaultInitializationValue)); + } + } + + // [TODO]: Consolidate property initializer generation between HLC and DPG + if (Configuration.Generation1ConvenienceClient && Discriminator is { } discriminator) + { + if (defaultCtorInitializers.All(i => i.Property != discriminator.Property) && parameterMap.TryGetValue(discriminator.Property.Declaration.Name.ToVariableName(), out var discriminatorParameter)) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(discriminator.Property, discriminatorParameter, discriminator.Value)); + } + else if (!_inputModel.IsUnknownDiscriminatorModel && discriminator.Value is { } value) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(discriminator.Property, value)); + } + } + + return defaultCtorInitializers; + } + + protected override CSharpType? CreateInheritedType() + { + if (GetSourceBaseType() is { } sourceBaseType && _typeFactory.TryCreateType(sourceBaseType, out CSharpType? baseType)) + { + return baseType; + } + + return _inputModel.BaseModel is { } baseModel + ? _typeFactory.CreateType(baseModel) + : null; + } + + private HashSet GetParentPropertyNames() + { + return EnumerateHierarchy() + .Skip(1) + .SelectMany(type => type.Properties) + .Select(p => p.Declaration.Name) + .ToHashSet(); + } + + private INamedTypeSymbol? GetSourceBaseType() + => ExistingType?.BaseType is { } sourceBaseType && sourceBaseType.SpecialType != SpecialType.System_ValueType && sourceBaseType.SpecialType != SpecialType.System_Object + ? sourceBaseType + : null; + + protected override IEnumerable BuildProperties() + { + var existingPropertyNames = GetParentPropertyNames(); + foreach (var field in Fields) + { + var property = new ObjectTypeProperty(field, Fields.GetInputByField(field)); + if (existingPropertyNames.Contains(property.Declaration.Name)) + continue; + yield return property; + } + + if (AdditionalPropertiesProperty is { } additionalPropertiesProperty) + yield return additionalPropertiesProperty; + + if (RawDataField is { } rawData) + yield return rawData; + } + + protected override IEnumerable BuildConstructors() + { + if (!SkipInitializerConstructor) + yield return InitializationConstructor; + + if (SerializationConstructor != InitializationConstructor) + yield return SerializationConstructor; + + if (EmptyConstructor != null) + yield return EmptyConstructor; + } + + protected override JsonObjectSerialization? BuildJsonSerialization() + { + if (IsPropertyBag || !_inputModelSerialization.Json) + return null; + // Serialization uses field and property names that first need to verified for uniqueness + // For that, FieldDeclaration instances must be written in the main partial class before JsonObjectSerialization is created for the serialization partial class + var properties = SerializationBuilder.GetPropertySerializations(this, _typeFactory); + var selfProperties = SerializationBuilder.GetPropertySerializations(this, _typeFactory, true); + var (additionalProperties, rawDataField) = CreateAdditionalPropertiesSerialization(); + return new(this, SerializationConstructor.Signature.Parameters, properties, selfProperties, additionalProperties, rawDataField, Discriminator, JsonConverter); + } + + protected override BicepObjectSerialization? BuildBicepSerialization(JsonObjectSerialization? json) => null; + + protected override MultipartObjectSerialization? BuildMultipartSerialization() + { + if (IncludeSerializer && _inputModel.Usage.HasFlag(InputModelTypeUsage.MultipartFormData)) + { + var additionalProperties = CreateMultipartAdditionalPropertiesSerialization(); + var properties = SerializationBuilder.CreateMultipartPropertySerializations(this).ToArray(); + return new MultipartObjectSerialization(this, SerializationConstructorSignature.Parameters, properties, additionalProperties, Discriminator, false); + } + return null; + } + + /* handle additionalProperty serialization */ + private MultipartAdditionalPropertiesSerialization? CreateMultipartAdditionalPropertiesSerialization() + { + bool shouldExcludeInWireSerialization = false; + ObjectTypeProperty? additionalPropertiesProperty = null; + InputType? additionalPropertiesValueType = null; + foreach (var model in EnumerateHierarchy()) + { + additionalPropertiesProperty = model.AdditionalPropertiesProperty ?? (model as SerializableObjectType)?.RawDataField; + if (additionalPropertiesProperty != null) + { + // if this is a real "AdditionalProperties", we should NOT exclude it in wire + shouldExcludeInWireSerialization = additionalPropertiesProperty != model.AdditionalPropertiesProperty; + if (model is ModelTypeProvider { AdditionalPropertiesProperty: { }, _inputModel.AdditionalProperties: { } additionalProperties }) + { + additionalPropertiesValueType = additionalProperties; + } + break; + } + } + + if (additionalPropertiesProperty == null) + { + return null; + } + + var dictionaryValueType = additionalPropertiesProperty.Declaration.Type.Arguments[1]; + Debug.Assert(!dictionaryValueType.IsNullable, $"{typeof(JsonCodeWriterExtensions)} implicitly relies on {additionalPropertiesProperty.Declaration.Name} dictionary value being non-nullable"); + MultipartSerialization valueSerialization; + var declaredName = additionalPropertiesProperty.Declaration.Name; + var memberValueExpression = new TypedMemberExpression(null, declaredName, additionalPropertiesProperty.Declaration.Type); + if (additionalPropertiesValueType is not null) + { + // build the serialization when there is an input type corresponding to it + valueSerialization = SerializationBuilder.BuildMultipartSerialization(additionalPropertiesValueType, dictionaryValueType, false, SerializationFormat.Default, memberValueExpression); + } + else + { + // build a simple one from its type when there is not an input type corresponding to it (indicating it is a raw data field) + valueSerialization = new MultipartValueSerialization(dictionaryValueType, SerializationFormat.Default, true);//TODO support dictionary type + } + + return new MultipartAdditionalPropertiesSerialization( + additionalPropertiesProperty, + new CSharpType(typeof(Dictionary<,>), additionalPropertiesProperty.Declaration.Type.Arguments), + valueSerialization, + shouldExcludeInWireSerialization); + } + + private (JsonAdditionalPropertiesSerialization? AdditionalPropertiesSerialization, JsonAdditionalPropertiesSerialization? RawDataFieldSerialization) CreateAdditionalPropertiesSerialization() + { + // collect additional properties and raw data field + ObjectTypeProperty? additionalPropertiesProperty = null; + ObjectTypeProperty? rawDataField = null; + InputType? additionalPropertiesValueInputType = null; + foreach (var model in EnumerateHierarchy()) + { + additionalPropertiesProperty ??= model.AdditionalPropertiesProperty; + if (additionalPropertiesProperty != null && additionalPropertiesValueInputType == null) + { + if (model is ModelTypeProvider { AdditionalPropertiesProperty: { }, _inputModel.AdditionalProperties: { } additionalProperties }) + { + additionalPropertiesValueInputType = additionalProperties; + } + } + rawDataField ??= (model as SerializableObjectType)?.RawDataField; + } + + if (additionalPropertiesProperty == null && rawDataField == null) + { + return (null, null); + } + + // build serialization for additional properties property (if any) + var additionalPropertiesSerialization = BuildSerializationForAdditionalProperties(additionalPropertiesProperty, additionalPropertiesValueInputType, false, false); + // build serialization for raw data field (if any) + // the raw data is excluded when the configuration is turned off (default), when turned on, we should include it + var rawDataFieldSerialization = BuildSerializationForAdditionalProperties(rawDataField, null, Configuration.EnableInternalRawData ? false : true, true); + + return (additionalPropertiesSerialization, rawDataFieldSerialization); + + static JsonAdditionalPropertiesSerialization? BuildSerializationForAdditionalProperties(ObjectTypeProperty? additionalPropertiesProperty, InputType? additionalPropertiesValueType, bool shouldExcludeInWireSerialization, bool shouldExcludeInWireDeserialization) + { + if (additionalPropertiesProperty is null) + return null; + + var additionalPropertyValueType = additionalPropertiesProperty.Declaration.Type.Arguments[1]; + JsonSerialization valueSerialization; + if (additionalPropertiesValueType is not null) + { + valueSerialization = SerializationBuilder.BuildJsonSerialization(additionalPropertiesValueType, additionalPropertyValueType, false, SerializationFormat.Default); + } + else + { + valueSerialization = new JsonValueSerialization(additionalPropertyValueType, SerializationFormat.Default, true); + } + + return new JsonAdditionalPropertiesSerialization( + additionalPropertiesProperty, + valueSerialization, + new CSharpType(typeof(Dictionary<,>), additionalPropertiesProperty.Declaration.Type.Arguments), + shouldExcludeInWireSerialization, + shouldExcludeInWireDeserialization); + } + } + + protected override XmlObjectSerialization? BuildXmlSerialization() + { + return _inputModelSerialization.Xml is { } xml ? SerializationBuilder.BuildXmlObjectSerialization(xml.Name ?? _inputModel.Name, this, _typeFactory) : null; + } + + protected override bool EnsureIncludeSerializer() + { + // TODO -- this should always return true when use model reader writer is enabled. + return Configuration.UseModelReaderWriter || _inputModelUsage.HasFlag(InputModelTypeUsage.Input); + } + + protected override bool EnsureIncludeDeserializer() + { + // TODO -- this should always return true when use model reader writer is enabled. + return Configuration.UseModelReaderWriter || _inputModelUsage.HasFlag(InputModelTypeUsage.Output); + } + + protected override IEnumerable BuildMethods() + { + foreach (var method in SerializationMethodsBuilder.BuildSerializationMethods(this)) + { + yield return method; + } + + if (Serialization.Json is null) + { + yield break; + } + + if (IncludeDeserializer) + { + yield return Snippets.Extensible.Model.BuildFromOperationResponseMethod(this, GetFromResponseModifiers()); + } + + if (IncludeSerializer) + { + var type = IsUnknownDerivedType ? Inherits! : Type; + yield return Snippets.Extensible.Model.BuildConversionToRequestBodyMethod(GetToRequestContentModifiers(), type); + } + } + + public ObjectTypeProperty GetPropertyBySerializedName(string serializedName, bool includeParents = false) + { + if (!TryGetPropertyForInputModelProperty(p => p.InputModelProperty?.SerializedName == serializedName, out ObjectTypeProperty? objectProperty, includeParents)) + { + throw new InvalidOperationException($"Unable to find object property with serialized name '{serializedName}' in schema {DefaultName}"); + } + + return objectProperty; + } + + private bool TryGetPropertyForInputModelProperty(Func propertySelector, [NotNullWhen(true)] out ObjectTypeProperty? objectProperty, bool includeParents = false) + { + objectProperty = null; + + foreach (var type in EnumerateHierarchy()) + { + objectProperty = type.Properties.SingleOrDefault(propertySelector); + if (objectProperty != null || !includeParents) + { + break; + } + } + + return objectProperty != null; + } + + protected override ObjectTypeDiscriminator? BuildDiscriminator() + { + string? discriminatorPropertyName = _inputModel.DiscriminatorProperty?.SerializedName; + ObjectTypeDiscriminatorImplementation[] implementations = Array.Empty(); + Constant? value = null; + ObjectTypeProperty property; + + if (discriminatorPropertyName == null) + { + var parent = GetBaseObjectType(); + if (parent is null || parent.Discriminator is null) + { + //neither me nor my parent are discriminators so I can bail + return null; + } + + discriminatorPropertyName = parent.Discriminator.SerializedName; + property = parent.Discriminator.Property; + } + else + { + //only load implementations for the base type + implementations = GetDerivedTypes(_derivedModels).OrderBy(i => i.Key).ToArray(); + + // find the discriminator corresponding property in this type or its base type or more + property = GetPropertyForDiscriminator(discriminatorPropertyName); + } + + if (_inputModel.DiscriminatorValue != null) + { + value = BuilderHelpers.ParseConstant(_inputModel.DiscriminatorValue, property.Declaration.Type); + } + + return new ObjectTypeDiscriminator( + property, + discriminatorPropertyName, + implementations, + value, + _defaultDerivedType! + ); + } + + private ObjectTypeProperty GetPropertyForDiscriminator(string inputPropertyName) + { + foreach (var obj in EnumerateHierarchy()) + { + var property = obj.Properties.FirstOrDefault(p => p.InputModelProperty is not null && (p.InputModelProperty.IsDiscriminator || p.InputModelProperty.Name == inputPropertyName)); + if (property is not null) + return property; + } + + throw new InvalidOperationException($"Expecting discriminator property {inputPropertyName} on model {Declaration.Name}, but found none"); + } + + private IEnumerable GetDerivedTypes(IReadOnlyList derivedInputTypes) + { + foreach (var derivedInputType in derivedInputTypes) + { + var derivedModel = (ModelTypeProvider)_typeFactory.CreateType(derivedInputType).Implementation; + foreach (var discriminatorImplementation in GetDerivedTypes(derivedModel._derivedModels)) + { + yield return discriminatorImplementation; + } + + yield return new ObjectTypeDiscriminatorImplementation(derivedInputType.DiscriminatorValue!, derivedModel.Type); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ModelTypeProviderFields.cs b/logger/autorest.csharp/common/Output/Models/Types/ModelTypeProviderFields.cs new file mode 100644 index 0000000..a85d114 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ModelTypeProviderFields.cs @@ -0,0 +1,372 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Output.Models.FieldModifiers; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal sealed class ModelTypeProviderFields : IReadOnlyCollection + { + private readonly IReadOnlyList _fields; + private readonly IReadOnlyDictionary _fieldsToInputs; + // parameter name should be unique since it's bound to field property + private readonly IReadOnlyDictionary _parameterNamesToFields; + + public IReadOnlyList PublicConstructorParameters { get; } + public IReadOnlyList SerializationParameters { get; } + public int Count => _fields.Count; + public FieldDeclaration? AdditionalProperties { get; } + + public ModelTypeProviderFields(IReadOnlyList properties, string modelName, InputModelTypeUsage inputModelUsage, TypeFactory typeFactory, ModelTypeMapping? modelTypeMapping, InputType? additionalPropertiesValueType, bool isStruct, bool isPropertyBag) + { + var fields = new List(); + var fieldsToInputs = new Dictionary(); + var publicParameters = new List(); + var serializationParameters = new List(); + var parametersToFields = new Dictionary(); + + var visitedMembers = new HashSet(SymbolEqualityComparer.Default); + foreach (var inputModelProperty in properties) + { + var originalFieldName = BuilderHelpers.DisambiguateName(modelName, inputModelProperty.Name.ToCleanName(), "Property"); + var propertyType = GetPropertyDefaultType(inputModelUsage, inputModelProperty, typeFactory); + + // We represent property being optional by making it nullable (when it is a value type) + // Except in the case of collection where there is a special handling + var optionalViaNullability = inputModelProperty is { IsRequired: false} && inputModelProperty.Type is not InputNullableType && + !propertyType.IsCollection; + + var existingMember = modelTypeMapping?.GetMemberByOriginalName(originalFieldName); + var serializationFormat = SerializationBuilder.GetSerializationFormat(inputModelProperty.Type); + var field = existingMember is not null + ? CreateFieldFromExisting(existingMember, propertyType, GetPropertyInitializationValue(propertyType, inputModelProperty), serializationFormat, typeFactory, inputModelProperty.IsRequired, optionalViaNullability) + : CreateField(originalFieldName, propertyType, inputModelUsage, inputModelProperty, isStruct, isPropertyBag, optionalViaNullability); + + if (existingMember is not null) + { + visitedMembers.Add(existingMember); + } + + fields.Add(field); + fieldsToInputs[field] = inputModelProperty; + + var parameterName = field.Name.ToVariableName(); + var parameterValidation = GetParameterValidation(field, inputModelProperty); + var parameter = new Parameter( + Name: parameterName, + Description: FormattableStringHelpers.FromString(BuilderHelpers.EscapeXmlDocDescription(inputModelProperty.Description)), + Type: field.Type, + DefaultValue: null, + Validation: parameterValidation, + Initializer: null); + parametersToFields[parameter.Name] = field; + // all properties should be included in the serialization ctor + serializationParameters.Add(parameter with { Validation = ValidationType.None }); + + // for classes, only required + not readonly + not constant + not discriminator could get into the public ctor + // for structs, all properties must be set in the public ctor + if (isStruct || inputModelProperty is { IsRequired: true, IsDiscriminator: false, ConstantValue: null }) + { + // [TODO]: Provide a flag to add read/write properties to the public model constructor + if (Configuration.Generation1ConvenienceClient || !inputModelProperty.IsReadOnly) + { + publicParameters.Add(parameter with { Type = parameter.Type.InputType }); + } + } + } + + if (additionalPropertiesValueType is not null) + { + // We use a $ prefix here as AdditionalProperties comes from a swagger concept + // and not a swagger model/operation name to disambiguate from a possible property with + // the same name. + var existingMember = modelTypeMapping?.GetMemberByOriginalName("$AdditionalProperties"); + + var type = CreateAdditionalPropertiesPropertyType(typeFactory, additionalPropertiesValueType); + if (!inputModelUsage.HasFlag(InputModelTypeUsage.Input)) + { + type = type.OutputType; + } + + var name = existingMember is null ? "AdditionalProperties" : existingMember.Name; + var declaration = new CodeWriterDeclaration(name); + declaration.SetActualName(name); + + var accessModifiers = existingMember is null ? Public : GetAccessModifiers(existingMember); + + var additionalPropertiesField = new FieldDeclaration($"Additional Properties", accessModifiers | ReadOnly, type, type, declaration, null, false, SerializationFormat.Default, true); + var additionalPropertiesParameter = new Parameter(name.ToVariableName(), $"Additional Properties", type, null, ValidationType.None, null); + + // we intentionally do not add this field into the field list to avoid cyclic references + serializationParameters.Add(additionalPropertiesParameter); + if (isStruct) + { + publicParameters.Add(additionalPropertiesParameter with { Validation = ValidationType.AssertNotNull }); + } + + parametersToFields[additionalPropertiesParameter.Name] = additionalPropertiesField; + + AdditionalProperties = additionalPropertiesField; + } + + // adding the leftover members from the source type + if (modelTypeMapping is not null) + { + foreach (var existingMember in modelTypeMapping.GetPropertiesWithSerialization()) + { + if (visitedMembers.Contains(existingMember)) + { + continue; + } + var existingCSharpType = BuilderHelpers.GetTypeFromExisting(existingMember, typeof(object), typeFactory); + + // since the property doesn't exist in the input type, we use type of existing member both as original and field type + // the serialization will be generated for this type and it might has issues if the type is not recognized properly. + // but customer could always use the `CodeGenMemberSerializationHooks` attribute to override those incorrect serialization/deserialization code. + var field = CreateFieldFromExisting(existingMember, existingCSharpType, null, SerializationFormat.Default, typeFactory, false, false); + var parameter = new Parameter(field.Name.ToVariableName(), $"", field.Type, null, ValidationType.None, null); + + fields.Add(field); + serializationParameters.Add(parameter); + } + } + + _fields = fields; + _fieldsToInputs = fieldsToInputs; + _parameterNamesToFields = parametersToFields; + + PublicConstructorParameters = publicParameters; + SerializationParameters = serializationParameters; + } + + // TODO -- when we consolidate the schemas into input types, we should remove this method and move it into BuilderHelpers + private static CSharpType CreateAdditionalPropertiesPropertyType(TypeFactory typeFactory, InputType additionalPropertiesValueType) + { + var valueType = typeFactory.CreateType(additionalPropertiesValueType); + var originalType = new CSharpType(typeof(IDictionary<,>), typeof(string), valueType); + + return BuilderHelpers.CreateAdditionalPropertiesPropertyType(originalType, typeFactory.UnknownType); + } + + private static ValidationType GetParameterValidation(FieldDeclaration field, InputModelProperty inputModelProperty) + { + // we do not validate a parameter when it is a value type (struct or int, etc) + if (field.Type.IsValueType) + { + return ValidationType.None; + } + + // or it is readonly + if (inputModelProperty.IsReadOnly) + { + return ValidationType.None; + } + + // or it is optional + if (!field.IsRequired) + { + return ValidationType.None; + } + + // or it is nullable + if (field.Type.IsNullable) + { + return ValidationType.None; + } + + return ValidationType.AssertNotNull; + } + + public FieldDeclaration GetFieldByParameterName(string parameterName) => _parameterNamesToFields[parameterName]; + public bool TryGetFieldByParameter(Parameter parameter, [MaybeNullWhen(false)] out FieldDeclaration fieldDeclaration) => _parameterNamesToFields.TryGetValue(parameter.Name, out fieldDeclaration); + public InputModelProperty? GetInputByField(FieldDeclaration field) => _fieldsToInputs.TryGetValue(field, out var property) ? property : null; + + public IEnumerator GetEnumerator() => _fields.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private static bool PropertyIsReadOnly(InputModelProperty property, InputModelTypeUsage usage, CSharpType type, bool isStruct, bool isPropertyBag) + { + if (property.IsDiscriminator) + { + // discriminator properties should be writable because we need to set values to the discriminators in the public ctor of derived classes. + return false; + } + + // a property will not have setter when it is readonly + if (property.IsReadOnly) + { + return true; + } + + // structs must have all their properties set in constructor + if (isStruct) + { + return true; + } + + // structs must have all their properties set in constructor + if (!usage.HasFlag(InputModelTypeUsage.Input)) + { + return true; + } + + if (property.ConstantValue is not null && property.IsRequired) // a property will not have setter when it is required literal type + { + return true; + } + + if (type.IsCollection && !type.IsReadOnlyMemory) + { + // nullable collection should be settable + // one exception is in the property bag, we never let them to be settable. + return property.Type is not InputNullableType || isPropertyBag; + } + + // In mixed models required properties are not readonly + return property.IsRequired && usage.HasFlag(InputModelTypeUsage.Input) && !usage.HasFlag(InputModelTypeUsage.Output); + } + + private static FieldDeclaration CreateField(string fieldName, CSharpType originalType, InputModelTypeUsage usage, InputModelProperty inputModelProperty, bool isStruct, bool isPropertyBag, bool optionalViaNullability) + { + var valueType = originalType; + if (optionalViaNullability) + { + originalType = originalType.WithNullable(true); + } + + FieldModifiers fieldModifiers; + FieldModifiers? setterModifiers = null; + if (inputModelProperty.IsDiscriminator) + { + fieldModifiers = Configuration.PublicDiscriminatorProperty ? Public : Internal; + setterModifiers = Configuration.PublicDiscriminatorProperty ? Internal | Protected : null; + } + else + { + fieldModifiers = Public; + } + + if (PropertyIsReadOnly(inputModelProperty, usage, originalType, isStruct, isPropertyBag)) + { + fieldModifiers |= ReadOnly; + } + + CodeWriterDeclaration declaration = new CodeWriterDeclaration(fieldName); + declaration.SetActualName(fieldName); + return new FieldDeclaration( + FormattableStringHelpers.FromString(BuilderHelpers.EscapeXmlDocDescription(inputModelProperty.Description)), + fieldModifiers, + originalType, + valueType, + declaration, + GetPropertyInitializationValue(originalType, inputModelProperty), + inputModelProperty.IsRequired, + SerializationBuilder.GetSerializationFormat(inputModelProperty.Type, valueType), + OptionalViaNullability: optionalViaNullability, + IsField: false, + WriteAsProperty: true, + SetterModifiers: setterModifiers); + } + + private static FieldDeclaration CreateFieldFromExisting(ISymbol existingMember, CSharpType originalType, ValueExpression? initializationValue, SerializationFormat serializationFormat, TypeFactory typeFactory, bool isRequired, bool optionalViaNullability) + { + if (optionalViaNullability) + { + originalType = originalType.WithNullable(true); + } + var fieldType = BuilderHelpers.GetTypeFromExisting(existingMember, originalType, typeFactory); + var valueType = fieldType; + if (optionalViaNullability) + { + valueType = valueType.WithNullable(false); + } + + var fieldModifiers = GetAccessModifiers(existingMember); + if (BuilderHelpers.IsReadOnly(existingMember, fieldType)) + { + fieldModifiers |= ReadOnly; + } + + var writeAsProperty = existingMember is IPropertySymbol; + CodeWriterDeclaration declaration = new CodeWriterDeclaration(existingMember.Name); + declaration.SetActualName(existingMember.Name); + + return new FieldDeclaration( + Description: $"Must be removed by post-generation processing,", + Modifiers: fieldModifiers, + Type: fieldType, + ValueType: valueType, + Declaration: declaration, + InitializationValue: initializationValue, + IsRequired: isRequired, + serializationFormat != SerializationFormat.Default ? serializationFormat : SerializationBuilder.GetDefaultSerializationFormat(valueType), + IsField: existingMember is IFieldSymbol, + WriteAsProperty: writeAsProperty, + OptionalViaNullability: optionalViaNullability); + } + + private static FieldModifiers GetAccessModifiers(ISymbol symbol) => symbol.DeclaredAccessibility switch + { + Accessibility.Public => Public, + Accessibility.Protected => Protected, + Accessibility.Internal => Internal, + Accessibility.Private => Private, + _ => throw new ArgumentOutOfRangeException() + }; + + private static CSharpType GetPropertyDefaultType(in InputModelTypeUsage usage, in InputModelProperty property, TypeFactory typeFactory) + { + var propertyType = typeFactory.CreateType(property.Type); + + if (!usage.HasFlag(InputModelTypeUsage.Input) || property.IsReadOnly) + { + propertyType = propertyType.OutputType; + } + + return propertyType; + } + + private static ValueExpression? GetPropertyInitializationValue(CSharpType propertyType, InputModelProperty inputModelProperty) + { + // if the default value is set somewhere else, we just return it. + if (inputModelProperty.DefaultValue != null) + { + return new FormattableStringToExpression(inputModelProperty.DefaultValue); + } + + // if it is not set, we check if this property is a literal type, and use the literal type as its default value. + if (inputModelProperty.ConstantValue is null || !inputModelProperty.IsRequired) + { + return null; + } + + // [TODO]: Consolidate property initializer generation between HLC and DPG + if (Configuration.Generation1ConvenienceClient) + { + return null; + } + + var constant = inputModelProperty.ConstantValue is { } constantValue && !propertyType.IsNullable + ? BuilderHelpers.ParseConstant(constantValue.Value, propertyType) + : Constant.NewInstanceOf(propertyType); + + return new ConstantExpression(constant); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ObjectPropertyInitializer.cs b/logger/autorest.csharp/common/Output/Models/Types/ObjectPropertyInitializer.cs new file mode 100644 index 0000000..ad65d3d --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ObjectPropertyInitializer.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Requests; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ObjectPropertyInitializer + { + public ObjectPropertyInitializer(ObjectTypeProperty property, ReferenceOrConstant value, ReferenceOrConstant? defaultValue = null) + { + Property = property; + Value = value; + DefaultValue = defaultValue; + } + + public ObjectTypeProperty Property { get; } + public ReferenceOrConstant Value { get; } + public ReferenceOrConstant? DefaultValue { get; } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ObjectType.cs b/logger/autorest.csharp/common/Output/Models/Types/ObjectType.cs new file mode 100644 index 0000000..f68fada --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ObjectType.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Shared; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal abstract class ObjectType : TypeProvider + { + private ObjectTypeConstructor[]? _constructors; + private ObjectTypeProperty[]? _properties; + private CSharpType? _inheritsType; + private ObjectTypeConstructor? _serializationConstructor; + private ObjectTypeConstructor? _initializationConstructor; + private FormattableString? _description; + private IReadOnlyList? _methods; + private ObjectTypeDiscriminator? _discriminator; + + protected ObjectType(BuildContext context) + : base(context) + { + } + + protected ObjectType(string defaultNamespace, SourceInputModel? sourceInputModel) + : base(defaultNamespace, sourceInputModel) + { + } + + protected bool IsInheritableCommonType { get; init; } = false; + protected bool SkipInitializerConstructor { get; init; } + public bool IsUnknownDerivedType { get; protected init; } + public bool IsPropertyBag { get; protected init; } + public bool IsStruct => ExistingType?.IsValueType ?? false; + protected override TypeKind TypeKind => IsStruct ? TypeKind.Struct : TypeKind.Class; + public IReadOnlyList Constructors => _constructors ??= BuildConstructors().ToArray(); + public IReadOnlyList Properties => _properties ??= BuildProperties().ToArray(); + + public CSharpType? Inherits => _inheritsType ??= CreateInheritedType(); + public ObjectTypeConstructor SerializationConstructor => _serializationConstructor ??= BuildSerializationConstructor(); + public IReadOnlyList Methods => _methods ??= BuildMethods().ToArray(); + public ObjectTypeDiscriminator? Discriminator => _discriminator ??= BuildDiscriminator(); + + public ObjectTypeConstructor InitializationConstructor => _initializationConstructor ??= BuildInitializationConstructor(); + + private ObjectTypeConstructor? _emptyConstructor; + public ObjectTypeConstructor? EmptyConstructor => _emptyConstructor ??= BuildEmptyConstructor(); + + public FormattableString Description => _description ??= $"{CreateDescription()}{CreateExtraDescriptionWithDiscriminator()}"; + public abstract ObjectTypeProperty? AdditionalPropertiesProperty { get; } + protected abstract ObjectTypeConstructor BuildInitializationConstructor(); + protected abstract ObjectTypeConstructor BuildSerializationConstructor(); + protected ObjectTypeConstructor? BuildEmptyConstructor() + { + if (!Configuration.UseModelReaderWriter) + return null; + + // check if any other ctor has parameters + var initCtorParameterCount = SkipInitializerConstructor ? int.MaxValue : InitializationConstructor.Signature.Parameters.Count; // if the ctor is skipped, we return a large number to avoid the case that the skipped ctor has 0 parameter. + var serializationCtorParameterCount = SerializationConstructor.Signature.Parameters.Count; + + if (initCtorParameterCount > 0 && serializationCtorParameterCount > 0) + { + var accessibility = IsStruct ? MethodSignatureModifiers.Public : + IsInheritableCommonType ? MethodSignatureModifiers.Protected : MethodSignatureModifiers.Internal; + return new( + new ConstructorSignature(Type, null, $"Initializes a new instance of {Type:C} for deserialization.", accessibility, Array.Empty()), + Array.Empty(), + null); + } + + return null; + } + protected abstract CSharpType? CreateInheritedType(); + protected abstract IEnumerable BuildProperties(); + protected abstract FormattableString CreateDescription(); + + protected virtual IEnumerable BuildMethods() + { + return Array.Empty(); + } + + public IEnumerable EnumerateHierarchy() + { + ObjectType? type = this; + while (type != null) + { + yield return type; + + if (type.Inherits?.IsFrameworkType == false && type.Inherits.Implementation is ObjectType o) + { + type = o; + } + else + { + type = null; + } + } + } + + protected abstract IEnumerable BuildConstructors(); + + protected ObjectType? GetBaseObjectType() + => Inherits is { IsFrameworkType: false, Implementation: ObjectType objectType } ? objectType : null; + + protected virtual ObjectTypeDiscriminator? BuildDiscriminator() + { + return null; + } + + public static readonly IReadOnlyList DiscriminatorDescFixedPart = new List { "Please note ", + " is the base class. According to the scenario, a derived class of the base class might need to be assigned here, or this property needs to be casted to one of the possible derived classes.", + "The available derived classes include " }; + + public virtual FormattableString CreateExtraDescriptionWithDiscriminator() + { + if (Discriminator?.HasDescendants == true) + { + List childrenList = new List(); + foreach (var implementation in Discriminator.Implementations) + { + // when the base type is public and the implementation type is not public, we skip it + if (Type.IsPublic && !implementation.Type.IsPublic) + continue; + childrenList.Add($"{implementation.Type:C}"); + } + return childrenList.Count > 0 ? + (FormattableString)$"{Environment.NewLine}{DiscriminatorDescFixedPart[0]}{Type:C}{DiscriminatorDescFixedPart[1]}{Environment.NewLine}{DiscriminatorDescFixedPart[2]}{childrenList.Join(", ", " and ")}." : + $"{Environment.NewLine}{DiscriminatorDescFixedPart[0]}{Type:C}{DiscriminatorDescFixedPart[1]}."; + } + return FormattableStringHelpers.Empty; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeConstructor.cs b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeConstructor.cs new file mode 100644 index 0000000..18f78e5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeConstructor.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ObjectTypeConstructor + { + public ObjectTypeConstructor(ConstructorSignature signature, IReadOnlyList initializers, ObjectTypeConstructor? baseConstructor = null) + { + Signature = signature; + Initializers = initializers; + BaseConstructor = baseConstructor; + } + + public ObjectTypeConstructor(CSharpType type, MethodSignatureModifiers modifiers, IReadOnlyList parameters, IReadOnlyList initializers, ObjectTypeConstructor? baseConstructor = null) + : this( + new ConstructorSignature( + type, + $"Initializes a new instance of {type:C}", + null, + modifiers, + parameters, + Initializer: new(isBase: true, baseConstructor?.Signature.Parameters ?? Array.Empty())), + initializers, + baseConstructor) + { + } + + public ConstructorSignature Signature { get; } + public IReadOnlyList Initializers { get; } + public ObjectTypeConstructor? BaseConstructor { get; } + + public ObjectTypeProperty? FindPropertyInitializedByParameter(Parameter constructorParameter) + { + foreach (var propertyInitializer in Initializers) + { + var value = propertyInitializer.Value; + if (value.IsConstant) + continue; + + if (value.Reference.Name == constructorParameter.Name) + { + return propertyInitializer.Property; + } + } + + return BaseConstructor?.FindPropertyInitializedByParameter(constructorParameter); + } + + public Parameter? FindParameterByInitializedProperty(ObjectTypeProperty property) + { + foreach (var propertyInitializer in Initializers) + { + if (propertyInitializer.Property == property) + { + if (propertyInitializer.Value.IsConstant) + { + continue; + } + + var parameterName = propertyInitializer.Value.Reference.Name; + return Signature.Parameters.Single(p => p.Name == parameterName); + } + } + + return BaseConstructor?.FindParameterByInitializedProperty(property); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeDiscriminator.cs b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeDiscriminator.cs new file mode 100644 index 0000000..a18423c --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeDiscriminator.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class ObjectTypeDiscriminator + { + public ObjectTypeProperty Property { get; } + public string SerializedName { get; } + + public Constant? Value { get; } + + public ObjectTypeDiscriminatorImplementation[] Implementations { get; } + + public SerializableObjectType DefaultObjectType { get; } + + public ObjectTypeDiscriminator(ObjectTypeProperty property, string serializedName, ObjectTypeDiscriminatorImplementation[] implementations, Constant? value, SerializableObjectType defaultObjectType) + { + Property = property; + Implementations = implementations; + Value = value; + SerializedName = serializedName; + DefaultObjectType = defaultObjectType; + } + + public bool HasDescendants => Implementations.Any(); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeDiscriminatorImplementation.cs b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeDiscriminatorImplementation.cs new file mode 100644 index 0000000..44430f2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeDiscriminatorImplementation.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal record ObjectTypeDiscriminatorImplementation(string Key, CSharpType Type); +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeProperty.cs b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeProperty.cs new file mode 100644 index 0000000..7ec02d5 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/ObjectTypeProperty.cs @@ -0,0 +1,464 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Output.Models.Serialization; +using Azure.ResourceManager.Models; + +namespace AutoRest.CSharp.Output.Models.Types +{ + [DebuggerDisplay("Name: {Declaration.Name}, Type: {Declaration.Type}")] + internal class ObjectTypeProperty + { + public ObjectTypeProperty(FieldDeclaration field, InputModelProperty? inputModelProperty) + : this(declaration: new MemberDeclarationOptions(field.Accessibility, field.Name, field.Type), + parameterDescription: field.Description?.ToString() ?? string.Empty, + isReadOnly: field.Modifiers.HasFlag(FieldModifiers.ReadOnly), + isRequired: field.IsRequired, + valueType: field.ValueType, + inputModelProperty: inputModelProperty, + optionalViaNullability: field.OptionalViaNullability, + getterModifiers: field.GetterModifiers, + setterModifiers: field.SetterModifiers, + serializationFormat: field.SerializationFormat) + { + InitializationValue = field.InitializationValue; + } + + public ObjectTypeProperty(MemberDeclarationOptions declaration, string parameterDescription, bool isReadOnly, InputModelProperty? inputModelProperty, CSharpType? valueType = null, bool optionalViaNullability = false) + : this(declaration, parameterDescription, isReadOnly, inputModelProperty, inputModelProperty?.IsRequired ?? false, valueType: valueType, optionalViaNullability: optionalViaNullability) + { + } + + private ObjectTypeProperty(MemberDeclarationOptions declaration, string parameterDescription, bool isReadOnly, InputModelProperty? inputModelProperty, bool isRequired, CSharpType? valueType = null, bool optionalViaNullability = false, bool isFlattenedProperty = false, FieldModifiers? getterModifiers = null, FieldModifiers? setterModifiers = null, SerializationFormat serializationFormat = SerializationFormat.Default) + { + IsReadOnly = isReadOnly; + OptionalViaNullability = optionalViaNullability; + ValueType = valueType ?? declaration.Type; + Declaration = declaration; + IsRequired = isRequired; + InputModelProperty = inputModelProperty; + _baseParameterDescription = parameterDescription; + SerializationFormat = serializationFormat; + FormattedDescription = CreatePropertyDescription(parameterDescription, isReadOnly); + Description = FormattedDescription.ToString(); + IsFlattenedProperty = isFlattenedProperty; + GetterModifiers = getterModifiers; + SetterModifiers = setterModifiers; + } + + public ObjectTypeProperty MarkFlatten() + { + var newDeclaration = new MemberDeclarationOptions("internal", Declaration.Name, Declaration.Type); + + return new ObjectTypeProperty( + newDeclaration, + _baseParameterDescription, + IsReadOnly, + InputModelProperty, + IsRequired, + valueType: ValueType, + optionalViaNullability: OptionalViaNullability, + isFlattenedProperty: true); + } + + public SerializationFormat SerializationFormat { get; } + + public ValueExpression? InitializationValue { get; } + + public virtual string SerializedName => InputModelProperty?.SerializedName ?? InputModelProperty?.SerializedName ?? Declaration.Name; + + private bool IsFlattenedProperty { get; } + + private FlattenedObjectTypeProperty? _flattenedProperty; + public FlattenedObjectTypeProperty? FlattenedProperty => EnsureFlattenedProperty(); + + private FlattenedObjectTypeProperty? EnsureFlattenedProperty() + { + if (IsFlattenedProperty && _flattenedProperty == null) + { + var hierarchyStack = FlattenedObjectTypeProperty.GetHierarchyStack(this); + // we can only get in this method when the property has a single property type, therefore the hierarchy stack here is guaranteed to have at least two values + var innerProperty = hierarchyStack.Pop(); + var immediateParentProperty = hierarchyStack.Pop(); + + var myPropertyName = FlattenedObjectTypeProperty.GetCombinedPropertyName(innerProperty, immediateParentProperty); + var childPropertyName = this.Equals(immediateParentProperty) ? innerProperty.Declaration.Name : myPropertyName; + + var propertyType = innerProperty.Declaration.Type; + + var isOverriddenValueType = innerProperty.Declaration.Type.IsValueType && !innerProperty.Declaration.Type.IsNullable; + if (isOverriddenValueType) + propertyType = propertyType.WithNullable(isOverriddenValueType); + + var declaration = new MemberDeclarationOptions(innerProperty.Declaration.Accessibility, myPropertyName, propertyType); + + // determines whether this property should has a setter + var (isReadOnly, includeGetterNullCheck, includeSetterNullCheck) = FlattenedObjectTypeProperty.GetFlags(this, innerProperty); + + _flattenedProperty = new FlattenedObjectTypeProperty(declaration, innerProperty._baseParameterDescription, this, isReadOnly, includeGetterNullCheck, includeSetterNullCheck, childPropertyName, isOverriddenValueType); + } + + return _flattenedProperty; + } + + public virtual Stack BuildHierarchyStack() + { + if (FlattenedProperty != null) + return FlattenedProperty.BuildHierarchyStack(); + + var stack = new Stack(); + stack.Push(this); + + return stack; + } + + public virtual IEnumerable? FlattenedNames => InputModelProperty?.FlattenedNames; + + public string GetWirePath() + { + string? wrapper = FlattenedNames?.FirstOrDefault(); + wrapper = wrapper is null ? string.Empty : $"{wrapper}."; + return $"{wrapper}{SerializedName}"; + } + + public static FormattableString CreateDefaultPropertyDescription(string nameToUse, bool isReadOnly) + { + String splitDeclarationName = string.Join(" ", Utilities.StringExtensions.SplitByCamelCase(nameToUse)).ToLower(); + if (isReadOnly) + { + return $"Gets the {splitDeclarationName}"; + } + else + { + return $"Gets or sets the {splitDeclarationName}"; + } + } + + public bool IsRequired { get; } + public MemberDeclarationOptions Declaration { get; } + public string Description { get; } + public FormattableString FormattedDescription { get; } + private FormattableString? _propertyDescription; + public FormattableString PropertyDescription => _propertyDescription ??= $"{FormattedDescription}{CreateExtraPropertyDiscriminatorSummary(ValueType)}"; + public InputModelProperty? InputModelProperty { get; } + private FormattableString? _parameterDescription; + private string _baseParameterDescription; // inherited type "FlattenedObjectTypeProperty" need to pass this value into the base constructor so that some appended information will not be appended again in the flattened property + public FormattableString ParameterDescription => _parameterDescription ??= $"{_baseParameterDescription}{CreateExtraPropertyDiscriminatorSummary(ValueType)}"; + + /// + /// Gets or sets the value indicating whether nullable type of this property represents optionality of the value. + /// + public bool OptionalViaNullability { get; } + + /// + /// When property is not required we transform the type to be able to express "omitted" value. + /// For example we turn int type into int?. + /// ValueType property contains the original type the property had before the transformation was applied to it. + /// + public CSharpType ValueType { get; } + public bool IsReadOnly { get; } + + public FieldModifiers? GetterModifiers { get; } + public FieldModifiers? SetterModifiers { get; } + + /// + /// This method attempts to retrieve the description for each of the union type items. For items that are lists, + /// the description will include details about the element type. + /// For items that are literals, the description will include the literal value. + /// + /// the list of union type items. + /// A list of FormattableString representing the description of each union item. + /// + internal static IReadOnlyList GetUnionTypesDescriptions(IReadOnlyList unionItems) + { + var values = new List(); + + foreach (CSharpType item in unionItems) + { + FormattableString description; + + if (item.IsLiteral && item.Literal?.Value != null) + { + var literalValue = item.Literal.Value.Value; + if (item.FrameworkType == typeof(string)) + { + description = $"{literalValue:L}"; + } + else + { + description = $"{literalValue}"; + } + } + else + { + description = $"{item:C}"; + } + + values.Add(description); + } + + return values.Distinct().ToList(); + } + + /// + /// This method constructs the description for a list type. If the list type contains an element type, + /// then the description will include details about the element type. + /// + /// The input list type to construct the description for. + /// A value indicating whether the list type is the base element of the original list. + /// A constructed FormattedString representing the description of the list type. + public static FormattableString ConstructDetailsForListType(CSharpType? input, bool isBaseElement) + { + if (input == null) + { + return $""; + } + + string itemName = input.TryGetCSharpFriendlyName(out var keywordName) ? keywordName : input.Name; + CSharpType? elementType = null; + FormattableString typeDescription = $"{itemName}"; + + if (isBaseElement) + { + typeDescription = $""; + } + + if (input.IsList || input.IsArray) + { + elementType = input.ElementType; + typeDescription = $"{itemName}"; + } + else if (input.IsDictionary) + { + typeDescription = $"{itemName}{{TKey, TValue}}"; + } + + // validate if the item contains an element type + if (elementType != null) + { + typeDescription = $"{typeDescription}{{{ConstructDetailsForListType(elementType, false)}}}"; + + if (isBaseElement) + { + typeDescription = $"{typeDescription}"; + } + } + + return typeDescription; + } + + internal string CreateExtraDescriptionWithManagedServiceIdentity() + { + var extraDescription = string.Empty; + var originalModelType = (InputModelProperty?.Type.GetImplementType()) as InputModelType; + var identityType = originalModelType?.GetAllProperties()!.FirstOrDefault(p => p.SerializedName == "type")!.Type; + if (identityType != null) + { + var supportedTypesToShow = new List(); + var commonMsiSupportedTypeCount = typeof(ManagedServiceIdentityType).GetProperties().Length; + if (identityType.GetImplementType() is InputEnumType enumType && enumType.Values.Count < commonMsiSupportedTypeCount) + { + supportedTypesToShow = enumType.Values.Select(c => c.GetValueString()).ToList(); + } + if (supportedTypesToShow.Count > 0) + { + extraDescription = $"Current supported identity types: {string.Join(", ", supportedTypesToShow)}"; + } + } + return extraDescription; + } + + /// + /// This method is used to create the description for the property. It will append the extra description for the property if it is a binary data property. + /// + /// The parameter description. + /// Flag to determine if a property is read only. + /// The formatted property description string. + private FormattableString CreatePropertyDescription(string parameterDescription, bool isPropReadOnly) + { + FormattableString description; + if (string.IsNullOrEmpty(parameterDescription)) + { + description = CreateDefaultPropertyDescription(Declaration.Name, isPropReadOnly); + } + else + { + description = $"{parameterDescription}"; + } + + FormattableString binaryDataExtraDescription = CreateBinaryDataExtraDescription(Declaration.Type, SerializationFormat); + description = $"{description}{binaryDataExtraDescription}"; + + return description; + } + + /// + /// This method will construct an additional description for properties that are binary data. For properties whose values are union types, + /// the description will include the types of values that are allowed. + /// + /// The CSharpType of the property. + /// The serialization format of the property. + /// The formatted description string for the property. + private FormattableString CreateBinaryDataExtraDescription(CSharpType type, SerializationFormat serializationFormat) + { + if (type.IsFrameworkType) + { + // TODO -- need to fix this so that we could have the reference of unioned models. + string typeSpecificDesc; + var unionTypes = GetUnionTypes(type); + IReadOnlyList unionTypeDescriptions = Array.Empty(); + if (unionTypes.Count > 0) + { + unionTypeDescriptions = GetUnionTypesDescriptions(unionTypes); + } + + if (type.FrameworkType == typeof(BinaryData)) + { + typeSpecificDesc = "this property"; + return ConstructBinaryDataDescription(typeSpecificDesc, serializationFormat, unionTypeDescriptions); + } + if (type.IsList && HasBinaryData(type.ElementType)) + { + typeSpecificDesc = "the element of this property"; + return ConstructBinaryDataDescription(typeSpecificDesc, serializationFormat, unionTypeDescriptions); + } + if (type.IsDictionary && HasBinaryData(type.ElementType)) + { + typeSpecificDesc = "the value of this property"; + return ConstructBinaryDataDescription(typeSpecificDesc, serializationFormat, unionTypeDescriptions); + } + } + return FormattableStringHelpers.Empty; + + // recursively get the union types if any. + static IReadOnlyList GetUnionTypes(CSharpType type) + { + if (type.IsCollection) + { + return GetUnionTypes(type.ElementType); + } + else if (type.IsUnion) + { + return type.UnionItemTypes; + } + + return Array.Empty(); + } + + // recursively get if any element or argument of this type is BinaryData + static bool HasBinaryData(CSharpType type) + { + if (type.IsCollection) + { + return HasBinaryData(type.ElementType); + } + + return type.IsFrameworkType && type.FrameworkType == typeof(BinaryData); + } + } + + private FormattableString ConstructBinaryDataDescription(string typeSpecificDesc, SerializationFormat serializationFormat, IReadOnlyList unionTypeDescriptions) + { + FormattableString unionTypesAdditionalDescription = $""; + + if (unionTypeDescriptions.Count > 0) + { + unionTypesAdditionalDescription = $"\n\nSupported types:\n\n"; + foreach (FormattableString unionTypeDescription in unionTypeDescriptions) + { + unionTypesAdditionalDescription = $"{unionTypesAdditionalDescription}\n{unionTypeDescription}\n\n"; + } + unionTypesAdditionalDescription = $"{unionTypesAdditionalDescription}\n"; + } + switch (serializationFormat) + { + case SerializationFormat.Bytes_Base64Url: //intentional fall through + case SerializationFormat.Bytes_Base64: + return $@" + +To assign a byte[] to {typeSpecificDesc} use . +The byte[] will be serialized to a Base64 encoded string. + +{unionTypesAdditionalDescription} +Examples: + + +BinaryData.FromBytes(new byte[] {{ 1, 2, 3 }}) +Creates a payload of ""AQID"". + + +"; + + default: + return $@" + +To assign an object to {typeSpecificDesc} use . + + +To assign an already formatted json string to this property use . + +{unionTypesAdditionalDescription} +Examples: + + +BinaryData.FromObjectAsJson(""foo"") +Creates a payload of ""foo"". + + +BinaryData.FromString(""\""foo\"""") +Creates a payload of ""foo"". + + +BinaryData.FromObjectAsJson(new {{ key = ""value"" }}) +Creates a payload of {{ ""key"": ""value"" }}. + + +BinaryData.FromString(""{{\""key\"": \""value\""}}"") +Creates a payload of {{ ""key"": ""value"" }}. + + +"; + } + } + + private static FormattableString CreateExtraPropertyDiscriminatorSummary(CSharpType valueType) + { + FormattableString? updatedDescription = null; + if (valueType.IsFrameworkType) + { + if (valueType.IsList) + { + if (!valueType.Arguments.First().IsFrameworkType && valueType.Arguments.First().Implementation is ObjectType objectType) + { + updatedDescription = objectType.CreateExtraDescriptionWithDiscriminator(); + } + } + else if (valueType.IsDictionary) + { + var objectTypes = valueType.Arguments.Where(arg => arg is { IsFrameworkType: false, Implementation: ObjectType }).ToList(); + if (objectTypes.Any()) + { + var subDescription = objectTypes.Select(o => ((ObjectType)o.Implementation).CreateExtraDescriptionWithDiscriminator()).ToArray(); + updatedDescription = subDescription.Join(""); + } + } + } + else if (valueType.Implementation is ObjectType objectType) + { + updatedDescription = objectType.CreateExtraDescriptionWithDiscriminator(); + } + return updatedDescription ?? $""; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/OutputLibrary.cs b/logger/autorest.csharp/common/Output/Models/Types/OutputLibrary.cs new file mode 100644 index 0000000..abedce1 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/OutputLibrary.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal abstract class OutputLibrary + { + public abstract CSharpType? FindTypeByName(string originalName); + + /// + /// Returns CSharpType that matches the enum definition provided in the input model, or a value type that enum can be converted to if enum wasn't defined in the input + /// + public abstract CSharpType ResolveEnum(InputEnumType enumType); + + /// + /// Returns CSharpType that matches the model definition provided in the input model, or simple object type if model type wasn't defined in the input + /// + public abstract CSharpType ResolveModel(InputModelType model); + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/SchemaObjectType.cs b/logger/autorest.csharp/common/Output/Models/Types/SchemaObjectType.cs new file mode 100644 index 0000000..723a2ed --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/SchemaObjectType.cs @@ -0,0 +1,799 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Serialization.Multipart; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal abstract class SchemaObjectType : SerializableObjectType + { + private readonly SerializationBuilder _serializationBuilder; + private readonly InputModelTypeUsage _usage; + private readonly TypeFactory _typeFactory; + private ObjectTypeProperty? _additionalPropertiesProperty; + private CSharpType? _implementsDictionaryType; + + protected SchemaObjectType(InputModelType inputModel, string defaultNamespace, TypeFactory typeFactory, SourceInputModel? sourceInputModel, SerializableObjectType? defaultDerivedType = null) + : base(defaultNamespace, sourceInputModel) + { + _typeFactory = typeFactory; + DefaultName = inputModel.CSharpName(); + DefaultNamespace = GetDefaultModelNamespace(null, defaultNamespace); + InputModel = inputModel; + _serializationBuilder = new SerializationBuilder(); + _usage = inputModel.Usage; + + DefaultAccessibility = inputModel.Access ?? "public"; + + // Update usage from code attribute + if (ModelTypeMapping?.Usage != null) + { + foreach (var usage in ModelTypeMapping.Usage) + { + _usage |= Enum.Parse(usage, true); + } + } + + _defaultDerivedType = defaultDerivedType + //if I have children and parents then I am my own defaultDerivedType + ?? (inputModel.DerivedModels.Any() && inputModel.BaseModel is { DiscriminatorProperty: not null } ? this :(inputModel.IsUnknownDiscriminatorModel ? this : null)); + IsUnknownDerivedType = inputModel.IsUnknownDiscriminatorModel; + // we skip the init ctor when there is an extension telling us to, or when this is an unknown derived type in a discriminated set + SkipInitializerConstructor = IsUnknownDerivedType; + IsInheritableCommonType = MgmtReferenceType.IsTypeReferenceType(InputModel) || MgmtReferenceType.IsReferenceType(InputModel); + + JsonConverter = InputModel.UseSystemTextJsonConverter ? new JsonConverterProvider(this, _sourceInputModel) : null; + } + + internal InputModelType InputModel { get; } + + protected override string DefaultName { get; } + protected override string DefaultNamespace { get; } + protected override string DefaultAccessibility { get; } = "public"; + + private SerializableObjectType? _defaultDerivedType; + + protected override bool IsAbstract => MgmtReferenceType.IsReferenceType(InputModel) || (!Configuration.SuppressAbstractBaseClasses.Contains(DefaultName) && InputModel.DiscriminatorProperty != null && InputModel.DiscriminatorValue == null); + + public override ObjectTypeProperty? AdditionalPropertiesProperty + { + get + { + if (_additionalPropertiesProperty != null || ImplementsDictionaryType == null) + { + return _additionalPropertiesProperty; + } + + // We use a $ prefix here as AdditionalProperties comes from a swagger concept + // and not a swagger model/operation name to disambiguate from a possible property with + // the same name. + var existingMember = ModelTypeMapping?.GetMemberByOriginalName("$AdditionalProperties"); + + // find the type of the additional properties + var additionalPropertiesType = BuilderHelpers.CreateAdditionalPropertiesPropertyType(ImplementsDictionaryType, _typeFactory.UnknownType); + + _additionalPropertiesProperty = new ObjectTypeProperty( + BuilderHelpers.CreateMemberDeclaration("AdditionalProperties", additionalPropertiesType, "public", existingMember, _typeFactory), + "Additional Properties", + true, + null + ); + + return _additionalPropertiesProperty; + } + } + + protected internal override InputModelTypeUsage GetUsage() => (InputModelTypeUsage)_usage; + + private ObjectTypeProperty? _rawDataField; + public override ObjectTypeProperty? RawDataField + { + get + { + if (_rawDataField != null) + return _rawDataField; + + if (!ShouldHaveRawData) + return null; + + // if we have an additional properties property, and its value type is also BinaryData, we should not have a raw data field + if (AdditionalPropertiesProperty != null) + { + var valueType = AdditionalPropertiesProperty.Declaration.Type.ElementType; + if (valueType.EqualsIgnoreNullable(_typeFactory.UnknownType)) + { + return null; + } + } + + // when this model has derived types, the accessibility should change from private to `protected internal` + string accessibility = HasDerivedTypes() ? "private protected" : "private"; + + _rawDataField = new ObjectTypeProperty( + BuilderHelpers.CreateMemberDeclaration(PrivateAdditionalPropertiesPropertyName, + _privateAdditionalPropertiesPropertyType, accessibility, null, _typeFactory), + PrivateAdditionalPropertiesPropertyDescription, + true, + null); + + return _rawDataField; + } + } + + private bool HasDerivedTypes() + { + if (InputModel.DerivedModels.Count > 0) + return true; + + if (InputModel.DiscriminatorProperty is not null) + return true; + + return false; + } + + protected override ObjectTypeConstructor BuildSerializationConstructor() + { + bool ownsDiscriminatorProperty = false; + + List serializationConstructorParameters = new List(); + List serializationInitializers = new List(); + ObjectTypeConstructor? baseSerializationCtor = null; + List baseParameterInitializers = new List(); + + if (Inherits is { IsFrameworkType: false, Implementation: ObjectType objectType }) + { + baseSerializationCtor = objectType.SerializationConstructor; + foreach (var p in baseSerializationCtor.Signature.Parameters) + { + if (p.IsRawData && AdditionalPropertiesProperty != null) + { + baseParameterInitializers.Add(Snippets.Null); + // do not add into the list + } + else + { + baseParameterInitializers.Add(p); + serializationConstructorParameters.Add(p); + } + } + } + + foreach (var property in Properties) + { + // skip the flattened properties, we should never include them in serialization + if (property is FlattenedObjectTypeProperty) + continue; + + var type = property.Declaration.Type; + + var deserializationParameter = new Parameter( + property.Declaration.Name.ToVariableName(), + property.ParameterDescription, + type, + null, + ValidationType.None, + null + ); + + ownsDiscriminatorProperty |= property == Discriminator?.Property; + + serializationConstructorParameters.Add(deserializationParameter); + + serializationInitializers.Add(new ObjectPropertyInitializer(property, deserializationParameter, GetPropertyDefaultValue(property))); + } + + // add the raw data to serialization ctor parameter list + if (RawDataField != null) + { + var deserializationParameter = new Parameter( + RawDataField.Declaration.Name.ToVariableName(), + RawDataField.ParameterDescription, + RawDataField.Declaration.Type, + null, + ValidationType.None, + null + ) + { + IsRawData = true + }; + serializationConstructorParameters.Add(deserializationParameter); + serializationInitializers.Add(new ObjectPropertyInitializer(RawDataField, deserializationParameter, null)); + } + + if (InitializationConstructor.Signature.Parameters + .Select(p => p.Type) + .SequenceEqual(serializationConstructorParameters.Select(p => p.Type))) + { + return InitializationConstructor; + } + + if (Discriminator != null) + { + // Add discriminator initializer to constructor at every level of hierarchy + if (!ownsDiscriminatorProperty && + baseSerializationCtor != null) + { + var discriminatorParameter = baseSerializationCtor.FindParameterByInitializedProperty(Discriminator.Property); + Debug.Assert(discriminatorParameter != null); + ReferenceOrConstant? defaultValue = null; + if (discriminatorParameter.Type.CanBeInitializedInline(Discriminator.Value)) + { + defaultValue = Discriminator.Value; + } + serializationInitializers.Add(new ObjectPropertyInitializer(Discriminator.Property, discriminatorParameter, defaultValue)); + } + } + + var initializer = new ConstructorInitializer(true, baseParameterInitializers); + + var signature = new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + null, + IsInheritableCommonType ? Protected : Internal, + serializationConstructorParameters, + Initializer: initializer); + + return new ObjectTypeConstructor( + signature, + serializationInitializers, + baseSerializationCtor); + } + + private ReferenceOrConstant? GetPropertyDefaultValue(ObjectTypeProperty property) + { + if (property == Discriminator?.Property && + Discriminator.Value != null) + { + return Discriminator.Value; + } + + return null; + } + + protected override ObjectTypeConstructor BuildInitializationConstructor() + { + List defaultCtorParameters = new List(); + List defaultCtorInitializers = new List(); + + ObjectTypeConstructor? baseCtor = GetBaseObjectType()?.InitializationConstructor; + if (baseCtor is not null) + defaultCtorParameters.AddRange(baseCtor.Signature.Parameters); + + foreach (var property in Properties) + { + // we do not need to add intialization for raw data field + if (property == RawDataField) + { + continue; + } + // Only required properties that are not discriminators go into default ctor + // skip the flattened properties, we should never include them in the constructors + if (property == Discriminator?.Property || property is FlattenedObjectTypeProperty) + { + continue; + } + + ReferenceOrConstant? initializationValue; + Constant? defaultInitializationValue = null; + + var propertyType = property.Declaration.Type; + if (property.InputModelProperty?.ConstantValue is not null && property.IsRequired) + { + // Turn constants into initializers + initializationValue = BuilderHelpers.ParseConstant(property.InputModelProperty!.ConstantValue.Value, propertyType); + } + else if (IsStruct || property.InputModelProperty?.IsRequired == true) + { + // For structs all properties become required + Constant? defaultParameterValue = null; + var constantValue = property.InputModelProperty?.ConstantValue; + Constant? clientDefaultValue = constantValue != null ? BuilderHelpers.ParseConstant(constantValue.Value, _typeFactory.CreateType(constantValue.Type)) : null; + if (clientDefaultValue is object defaultValueObject) + { + defaultInitializationValue = BuilderHelpers.ParseConstant(defaultValueObject, propertyType); + } + + var inputType = propertyType.InputType; + if (defaultParameterValue != null && !property.ValueType.CanBeInitializedInline(defaultParameterValue)) + { + inputType = inputType.WithNullable(true); + defaultParameterValue = Constant.Default(inputType); + } + + var validate = property.InputModelProperty?.Type is not InputNullableType && !inputType.IsValueType && property.InputModelProperty?.IsReadOnly != true ? ValidationType.AssertNotNull : ValidationType.None; + var defaultCtorParameter = new Parameter( + property.Declaration.Name.ToVariableName(), + property.ParameterDescription, + inputType, + defaultParameterValue, + validate, + null + ); + + defaultCtorParameters.Add(defaultCtorParameter); + initializationValue = defaultCtorParameter; + } + else + { + initializationValue = GetPropertyDefaultValue(property); + + if (initializationValue == null && propertyType.IsCollection) + { + if (propertyType.IsReadOnlyMemory) + { + initializationValue = propertyType.IsNullable ? null : Constant.FromExpression($"{propertyType}.{nameof(ReadOnlyMemory.Empty)}", propertyType); + } + else + { + initializationValue = Constant.NewInstanceOf(propertyType.PropertyInitializationType); + } + } + } + + if (initializationValue != null) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(property, initializationValue.Value, defaultInitializationValue)); + } + } + + if (Discriminator?.Value != null) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(Discriminator.Property, Discriminator.Value.Value)); + } + + if (AdditionalPropertiesProperty != null && + !defaultCtorInitializers.Any(i => i.Property == AdditionalPropertiesProperty)) + { + defaultCtorInitializers.Add(new ObjectPropertyInitializer(AdditionalPropertiesProperty, Constant.NewInstanceOf(AdditionalPropertiesProperty.Declaration.Type.InitializationType))); + } + + return new ObjectTypeConstructor( + Type, + IsAbstract ? Protected : _usage.HasFlag(InputModelTypeUsage.Input) ? Public : Internal, + defaultCtorParameters, + defaultCtorInitializers, + baseCtor); + } + + public CSharpType? ImplementsDictionaryType => _implementsDictionaryType ??= CreateInheritedDictionaryType(); + protected override IEnumerable BuildConstructors() + { + // Skip initialization ctor if this instance is used to support forward compatibility in polymorphism. + if (!SkipInitializerConstructor) + yield return InitializationConstructor; + + // Skip serialization ctor if they are the same + if (InitializationConstructor != SerializationConstructor) + yield return SerializationConstructor; + + if (EmptyConstructor != null) + yield return EmptyConstructor; + } + + protected override ObjectTypeDiscriminator? BuildDiscriminator() + { + var discriminatorProperty = InputModel.DiscriminatorProperty; + if (discriminatorProperty is null) + { + discriminatorProperty = GetCombinedSchemas().FirstOrDefault(m => m.DiscriminatorProperty != null)?.DiscriminatorProperty; + } + if (discriminatorProperty is null) + { + return null; + } + + // If the parent discriminator exists, the current discriminator should have the same name + var parentDiscriminator = GetBaseObjectType()?.Discriminator; + var property = Properties.FirstOrDefault(p => p.InputModelProperty is not null && p.InputModelProperty.IsDiscriminator && (parentDiscriminator is null || (parentDiscriminator != null && p.InputModelProperty.Name == parentDiscriminator.Property.SerializedName))) + ?? parentDiscriminator?.Property; + + //neither me nor my parent are discriminators so I can bail + if (property is null) + { + return null; + } + + Constant? value = null; + + //only load implementations for the base type + // TODO: remove the order by + var implementationDictionary = new Dictionary(); + GetDerivedTypes(InputModel.DerivedModels, implementationDictionary); + var implementations = implementationDictionary.Values.OrderBy(x => x.Key).ToArray(); + + if (InputModel.DiscriminatorValue != null) + { + value = BuilderHelpers.ParseConstant(InputModel.DiscriminatorValue, property.Declaration.Type.WithNullable(false)); + } + + return new ObjectTypeDiscriminator( + property, + discriminatorProperty.SerializedName, + implementations, + value, + _defaultDerivedType! + ); + } + + private HashSet GetParentPropertySerializedNames() + { + // TODO -- this is not really getting the serialized name of the properties as advertised in the method name + // this is just getting the name of the schema property. + // this is a guard of not having compilation errors because we cannot define the properties with the same name as properties defined in base types, therefore here we should get the set by the declaration name of the property + return EnumerateHierarchy() + .Skip(1) + .SelectMany(type => type.Properties) + .Select(p => p.InputModelProperty?.Name) + .ToHashSet(); + } + + private HashSet GetParentPropertyDeclarationNames() + { + return EnumerateHierarchy() + .Skip(1) + .SelectMany(type => type.Properties) + .Select(p => p.Declaration.Name) + .ToHashSet(); + } + + protected override IEnumerable BuildProperties() + { + var propertiesFromSpec = GetParentPropertyDeclarationNames(); + var existingProperties = GetParentPropertySerializedNames(); + + foreach (var model in InputModel.GetSelfAndBaseModels()) + { + foreach (var property in model.Properties) + { + if (existingProperties.Contains(property.Name)) + { + continue; + } + var prop = CreateProperty(property); + + // If "Type" property is "String" and "ResourceType" exists in parents properties, skip it since they are the same. + // This only applies for TypeSpec input, for swagger input we have renamed "type" property to "resourceType" in FrameworkTypeUpdater.ValidateAndUpdate + if (property.CSharpName() == "Type" && prop.ValueType.Name == "String" && existingProperties.Contains("ResourceType")) + { + continue; + } + propertiesFromSpec.Add(prop.Declaration.Name); + yield return prop; + } + } + + if (AdditionalPropertiesProperty is ObjectTypeProperty additionalPropertiesProperty) + { + propertiesFromSpec.Add(additionalPropertiesProperty.Declaration.Name); + yield return additionalPropertiesProperty; + } + + if (ModelTypeMapping != null) + { + foreach (var propertyWithSerialization in ModelTypeMapping.GetPropertiesWithSerialization()) + { + if (propertiesFromSpec.Contains(propertyWithSerialization.Name)) + continue; + + var csharpType = BuilderHelpers.GetTypeFromExisting(propertyWithSerialization, typeof(object), _typeFactory); + var isReadOnly = BuilderHelpers.IsReadOnly(propertyWithSerialization, csharpType); + var accessibility = propertyWithSerialization.DeclaredAccessibility == Accessibility.Public ? "public" : "internal"; + yield return new ObjectTypeProperty( + new MemberDeclarationOptions(accessibility, propertyWithSerialization.Name, csharpType), + string.Empty, + isReadOnly, + null); + } + } + } + + protected ObjectTypeProperty CreateProperty(InputModelProperty property) + { + var name = BuilderHelpers.DisambiguateName(Type, property.CSharpName()); + var existingMember = ModelTypeMapping?.GetMemberByOriginalName(name); + + var accessibility = property.IsDiscriminator == true ? "internal" : "public"; + + var propertyType = GetDefaultPropertyType(property); + + // We represent property being optional by making it nullable + // Except in the case of collection where there is a special handling + bool optionalViaNullability = !property.IsRequired && + property.Type is not InputNullableType && + !propertyType.IsCollection; + + if (optionalViaNullability) + { + propertyType = propertyType.WithNullable(true); + } + + var memberDeclaration = BuilderHelpers.CreateMemberDeclaration( + name, + propertyType, + accessibility, + existingMember, + _typeFactory); + + var type = memberDeclaration.Type; + + var valueType = type; + if (optionalViaNullability) + { + valueType = valueType.WithNullable(false); + } + + bool isCollection = type is { IsCollection: true, IsReadOnlyMemory: false }; + + bool propertyShouldOmitSetter = IsStruct || + !_usage.HasFlag(InputModelTypeUsage.Input) || + property.IsReadOnly; + + + if (isCollection) + { + propertyShouldOmitSetter |= property.Type is not InputNullableType; + } + else + { + // In mixed models required properties are not readonly + propertyShouldOmitSetter |= property.IsRequired && + _usage.HasFlag(InputModelTypeUsage.Input) && + !_usage.HasFlag(InputModelTypeUsage.Output); + } + + // we should remove the setter of required constant + if (property.ConstantValue is not null && property.IsRequired) + { + propertyShouldOmitSetter = true; + } + + if (property.IsDiscriminator == true) + { + // Discriminator properties should be writeable + propertyShouldOmitSetter = false; + } + + var objectTypeProperty = new ObjectTypeProperty( + memberDeclaration, + BuilderHelpers.EscapeXmlDocDescription(property.Description), + propertyShouldOmitSetter, + property, + valueType, + optionalViaNullability); + return objectTypeProperty; + } + + private CSharpType GetDefaultPropertyType(InputModelProperty property) + { + var valueType = _typeFactory.CreateType(property.Type); + if (!_usage.HasFlag(InputModelTypeUsage.Input) || + property.IsReadOnly) + { + valueType = valueType.OutputType; + } + + return valueType; + } + + protected internal IEnumerable GetCombinedSchemas() + { + if (InputModel.IsUnknownDiscriminatorModel) + { + if (InputModel.BaseModel is not null) + { + yield return InputModel.BaseModel; + } + } + else + { + foreach (var inputModel in InputModel.GetAllBaseModels()) + { + yield return inputModel; + } + } + } + + protected override CSharpType? CreateInheritedType() + { + if (GetExistingBaseType() is { } existingBaseType) + { + return existingBaseType; + } + + return InputModel.BaseModel is { } baseModel + ? _typeFactory.CreateType(baseModel) + : null; + } + + protected CSharpType? GetExistingBaseType() + { + INamedTypeSymbol? existingBaseType = GetSourceBaseType(); + + if (existingBaseType is { } type && _typeFactory.TryCreateType(type, out CSharpType? baseType)) + { + return baseType; + } + + return null; + } + + private INamedTypeSymbol? GetSourceBaseType() + { + return ExistingType?.BaseType is { } sourceBaseType && sourceBaseType.SpecialType != SpecialType.System_ValueType && sourceBaseType.SpecialType != SpecialType.System_Object + ? sourceBaseType + : null; + } + + private CSharpType? CreateInheritedDictionaryType() + { + if (InputModel.AdditionalProperties is not null) + { + return new CSharpType( + _usage.HasFlag(InputModelTypeUsage.Input) ? typeof(IDictionary<,>) : typeof(IReadOnlyDictionary<,>), + typeof(string), + _typeFactory.CreateType(InputModel.AdditionalProperties)); + } + + return null; + } + + public virtual ObjectTypeProperty GetPropertyForSchemaProperty(InputModelProperty property, bool includeParents = false) + { + if (!TryGetPropertyForSchemaProperty(p => p.InputModelProperty == property, out ObjectTypeProperty? objectProperty, includeParents)) + { + throw new InvalidOperationException($"Unable to find object property for schema property {property.SerializedName} in schema {DefaultName}"); + } + + return objectProperty; + } + + public ObjectTypeProperty GetPropertyBySerializedName(string serializedName, bool includeParents = false) + { + if (!TryGetPropertyForSchemaProperty(p => p.InputModelProperty?.SerializedName == serializedName, out ObjectTypeProperty? objectProperty, includeParents)) + { + throw new InvalidOperationException($"Unable to find object property with serialized name '{serializedName}' in schema {DefaultName}"); + } + + return objectProperty; + } + + public ObjectTypeProperty GetPropertyForGroupedParameter(string groupedParameterName, bool includeParents = false) + { + if (!TryGetPropertyForSchemaProperty( + p => p.InputModelProperty?.Name == groupedParameterName, + out ObjectTypeProperty? objectProperty, includeParents)) + { + throw new InvalidOperationException($"Unable to find object property for grouped parameter {groupedParameterName} in schema {DefaultName}"); + } + + return objectProperty; + } + + protected bool TryGetPropertyForSchemaProperty(Func propertySelector, [NotNullWhen(true)] out ObjectTypeProperty? objectProperty, bool includeParents = false) + { + objectProperty = null; + + foreach (var type in EnumerateHierarchy()) + { + objectProperty = type.Properties.SingleOrDefault(propertySelector); + if (objectProperty != null || !includeParents) + { + break; + } + } + + return objectProperty != null; + } + + protected override FormattableString CreateDescription() + { + return $"{InputModel.Description}"; + } + + protected override bool EnsureIncludeSerializer() + { + // TODO -- this should always return true when use model reader writer is enabled. + return Configuration.UseModelReaderWriter || _usage.HasFlag(InputModelTypeUsage.Input); + } + + protected override bool EnsureIncludeDeserializer() + { + // TODO -- this should always return true when use model reader writer is enabled. + return !MgmtReferenceType.IsReferenceType(InputModel) && (Configuration.UseModelReaderWriter || _usage.HasFlag(InputModelTypeUsage.Output)); + } + + protected override JsonObjectSerialization? BuildJsonSerialization() + { + return !InputModel.Serialization.Json ? null : _serializationBuilder.BuildJsonObjectSerialization(InputModel, this); + } + + protected override ObjectTypeSerialization BuildSerialization() + { + bool isReference = MgmtReferenceType.IsReferenceType(InputModel); + var json = BuildJsonSerialization(); + var xml = isReference ? null : BuildXmlSerialization(); + var bicep = isReference ? null : BuildBicepSerialization(json); + var multipart = isReference ? null : BuildMultipartSerialization(); + // select interface model type here + var modelType = IsUnknownDerivedType && Inherits is { IsFrameworkType: false, Implementation: { } baseModel } ? baseModel.Type : Type; + var interfaces = isReference ? null : new SerializationInterfaces(IncludeSerializer, IsStruct, modelType, json is not null, xml is not null); + return new ObjectTypeSerialization(this, json, xml, bicep, multipart, interfaces); + } + + protected override BicepObjectSerialization? BuildBicepSerialization(JsonObjectSerialization? json) + { + if (json == null) + return null; + // if this.Usages does not contain Output bit, then return null + // alternate - is one of ancestors resource data or contained on a resource data + var usage = GetUsage(); + + return Configuration.AzureArm && Configuration.UseModelReaderWriter && Configuration.EnableBicepSerialization && + usage.HasFlag(InputModelTypeUsage.Output) + ? _serializationBuilder.BuildBicepObjectSerialization(this, json) : null; + } + + protected override XmlObjectSerialization? BuildXmlSerialization() + { + return InputModel.Serialization?.Xml is not null + ? SerializationBuilder.BuildXmlObjectSerialization(InputModel.Serialization.Xml.Name ?? InputModel.Name, this, _typeFactory) + : null; + } + + protected override MultipartObjectSerialization? BuildMultipartSerialization() + { + return InputModel.Usage.HasFlag(InputModelTypeUsage.MultipartFormData) ? _serializationBuilder.BuildMultipartObjectSerialization(InputModel, this) : null; + } + protected override IEnumerable BuildMethods() + { + foreach (var method in SerializationMethodsBuilder.BuildSerializationMethods(this)) + { + yield return method; + } + } + + // There are derived models with duplicate discriminator values, which is not allowed + // Follow the same logic as modelerfour to remove derived models with duplicate discriminator values + private void GetDerivedTypes(IReadOnlyList derivedInputTypes, Dictionary implementations) + { + foreach (var derivedInputType in derivedInputTypes) + { + var derivedType = (SchemaObjectType)_typeFactory.CreateType(derivedInputType).Implementation; + var derivedTypeImplementation = new Dictionary(); + GetDerivedTypes(derivedType.InputModel.DerivedModels, derivedTypeImplementation); + foreach (var discriminatorImplementation in derivedTypeImplementation) + { + implementations[discriminatorImplementation.Key] = discriminatorImplementation.Value; + } + + implementations[derivedInputType.DiscriminatorValue!] = new ObjectTypeDiscriminatorImplementation(derivedInputType.DiscriminatorValue!, derivedType.Type); + } + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/SerializableObjectType.cs b/logger/autorest.csharp/common/Output/Models/Types/SerializableObjectType.cs new file mode 100644 index 0000000..e283366 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/SerializableObjectType.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Serialization.Multipart; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Serialization.Bicep; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Serialization.Xml; +using AutoRest.CSharp.Output.Models.Types; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Common.Output.Models.Types +{ + internal abstract class SerializableObjectType : ObjectType + { + private readonly Lazy _modelTypeMapping; + + protected SerializableObjectType(string defaultNamespace, SourceInputModel? sourceInputModel) : base(defaultNamespace, sourceInputModel) + { + _modelTypeMapping = new Lazy(() => _sourceInputModel?.CreateForModel(ExistingType)); + } + + public INamedTypeSymbol? GetExistingType() => ExistingType; + + private protected ModelTypeMapping? ModelTypeMapping => _modelTypeMapping.Value; + + private bool? _includeSerializer; + public bool IncludeSerializer => _includeSerializer ??= EnsureIncludeSerializer(); + + private bool? _includeDeserializer; + public bool IncludeDeserializer => _includeDeserializer ??= EnsureIncludeDeserializer(); + + private ObjectTypeSerialization? _modelSerialization; + public ObjectTypeSerialization Serialization => _modelSerialization ??= BuildSerialization(); + + protected virtual ObjectTypeSerialization BuildSerialization() + { + var json = BuildJsonSerialization(); + var xml = BuildXmlSerialization(); + var bicep = BuildBicepSerialization(json); + var multipart = BuildMultipartSerialization(); + // select interface model type here + var modelType = IsUnknownDerivedType && Inherits is { IsFrameworkType: false, Implementation: { } baseModel } ? baseModel.Type : Type; + var interfaces = new SerializationInterfaces(IncludeSerializer, IsStruct, modelType, json is not null, xml is not null); + return new ObjectTypeSerialization(this, json, xml, bicep, multipart, interfaces); + } + + protected abstract JsonObjectSerialization? BuildJsonSerialization(); + protected abstract XmlObjectSerialization? BuildXmlSerialization(); + protected abstract BicepObjectSerialization? BuildBicepSerialization(JsonObjectSerialization? json); + protected abstract MultipartObjectSerialization? BuildMultipartSerialization(); + + protected abstract bool EnsureIncludeSerializer(); + protected abstract bool EnsureIncludeDeserializer(); + + public JsonConverterProvider? JsonConverter { get; protected init; } + protected internal abstract InputModelTypeUsage GetUsage(); + + // TODO -- despite this is actually a field if present, we have to make it a property to work properly with other functionalities in the generator, such as the `CodeWriter.WriteInitialization` method + public virtual ObjectTypeProperty? RawDataField => null; + + private bool? _shouldHaveRawData; + protected bool ShouldHaveRawData => _shouldHaveRawData ??= EnsureShouldHaveRawData(); + + private bool EnsureShouldHaveRawData() + { + if (!Configuration.UseModelReaderWriter) + return false; + + if (IsPropertyBag) + return false; + + if (Inherits != null && Inherits is not { IsFrameworkType: false, Implementation: SystemObjectType }) + return false; + + return true; + } + + protected const string PrivateAdditionalPropertiesPropertyDescription = "Keeps track of any properties unknown to the library."; + protected const string PrivateAdditionalPropertiesPropertyName = "_serializedAdditionalRawData"; + protected const string InternalAdditionalPropertiesPropertyName = "SerializedAdditionalRawData"; + protected static readonly CSharpType _privateAdditionalPropertiesPropertyType = typeof(IDictionary); + + protected internal SourcePropertySerializationMapping? GetForMemberSerialization(string propertyDeclaredName) + { + foreach (var obj in EnumerateHierarchy()) + { + if (obj is not SerializableObjectType so) + continue; + + var serialization = so.ModelTypeMapping?.GetForMemberSerialization(propertyDeclaredName); + if (serialization is not null) + return serialization; + } + + return null; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/SignatureType.cs b/logger/autorest.csharp/common/Output/Models/Types/SignatureType.cs new file mode 100644 index 0000000..4cd3c2a --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/SignatureType.cs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Shared; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models.Types +{ + /// + /// This type holds three portions of codes: + /// - current + /// - custom + /// - baseline contract + /// current union custom compare with baseline contract outputs the changeset, we can apply different rules with it. + /// + internal class SignatureType + { + private readonly TypeFactory _typeFactory; + private readonly string _namespace; + private readonly string _name; + private readonly SignatureType? _customization; + private readonly SignatureType? _baselineContract; + private readonly MethodChangeset? _methodChangeset; + + // Missing means the method with the same name is missing from the current contract + // Updated means the method with the same name is updated in the current contract, and the list contains the previous method and current methods including overload ones + private record MethodChangeset(IReadOnlyList Missing, IReadOnlyList<(List Current, MethodSignature Previous)> Updated) { } + + public SignatureType(TypeFactory typeFactory, IReadOnlyList methods, SourceInputModel? sourceInputModel, string @namespace, string name) + { + // This can only be used for Mgmt now, because there are custom/hand-written code in HLC can't be loaded into CsharpType such as generic methods + if (!Configuration.AzureArm) + { + throw new InvalidOperationException("This type should only be used for Mgmt"); + } + + _typeFactory = typeFactory; + Methods = methods; + _namespace = @namespace; + _name = name; + if (sourceInputModel is not null) + { + _customization = new SignatureType(typeFactory, PopulateMethodsFromCompilation(sourceInputModel?.Customization), null, @namespace, name); + _baselineContract = new SignatureType(typeFactory, PopulateMethodsFromCompilation(sourceInputModel?.PreviousContract), null, @namespace, name); + _methodChangeset ??= CompareMethods(Methods.Union(_customization?.Methods ?? Array.Empty(), MethodSignature.ParameterAndReturnTypeEqualityComparer), _baselineContract?.Methods); + } + } + + private IReadOnlyList? _overloadMethods; + public IReadOnlyList OverloadMethods => _overloadMethods ??= EnsureOverloadMethods(); + + private IReadOnlyList EnsureOverloadMethods() + { + var overloadMethods = new List(); + if (_methodChangeset?.Updated is not { } updated) + { + return Array.Empty(); + } + + foreach (var (current, previous) in updated) + { + if (TryGetPreviousMethodWithLessOptionalParameters(current, previous, out var currentMethodToCall, out var missingParameters)) + { + var overloadMethodSignature = new OverloadMethodSignature(currentMethodToCall, previous.WithParametersRequired(), missingParameters, previous.Description); + var previousMethodSignature = overloadMethodSignature.PreviousMethodSignature with { Attributes = new CSharpAttribute[]{new CSharpAttribute(typeof(EditorBrowsableAttribute), FrameworkEnumValue(EditorBrowsableState.Never)) }}; + overloadMethods.Add(new Method(previousMethodSignature, BuildOverloadMethodBody(overloadMethodSignature))); + } + } + return overloadMethods; + } + + private MethodBodyStatement BuildOverloadMethodBody(OverloadMethodSignature overloadMethodSignature) + => Return(new InvokeInstanceMethodExpression(null, overloadMethodSignature.MethodSignature.Name, BuildOverloadMethodParameters(overloadMethodSignature), null, overloadMethodSignature.PreviousMethodSignature.Modifiers.HasFlag(MethodSignatureModifiers.Async))); + + private IReadOnlyList BuildOverloadMethodParameters(OverloadMethodSignature overloadMethodSignature) + { + var parameters = new List(); + var set = overloadMethodSignature.MissingParameters.ToHashSet(Parameter.TypeAndNameEqualityComparer); + foreach (var parameter in overloadMethodSignature.MethodSignature.Parameters) + { + if (set.Contains(parameter)) + { + parameters.Add(new PositionalParameterReference(parameter.Name, Default.CastTo(parameter.Type))); + continue; + } + parameters.Add(new PositionalParameterReference(parameter)); + } + return parameters; + } + + private IReadOnlySet? _methodsToSkip; + public IReadOnlySet MethodsToSkip => _methodsToSkip ??= EnsureMethodsToSkip(); + private IReadOnlySet EnsureMethodsToSkip() + { + if (_customization is null) + { + return new HashSet(); + } + return Methods.Intersect(_customization.Methods, MethodSignature.ParameterAndReturnTypeEqualityComparer).ToHashSet(MethodSignature.ParameterAndReturnTypeEqualityComparer); + } + + private bool TryGetPreviousMethodWithLessOptionalParameters(IList currentMethods, MethodSignature previousMethod, [NotNullWhen(true)] out MethodSignature? currentMethodToCall, [NotNullWhen(true)] out IReadOnlyList? missingParameters) + { + foreach (var item in currentMethods) + { + if (item.Parameters.Count <= previousMethod.Parameters.Count) + { + continue; + } + + if (!CurrentContainAllPreviousParameters(previousMethod, item)) + { + continue; + } + + if (previousMethod.ReturnType is null && item.ReturnType is not null) + { + continue; + } + + // We can't use CsharpType.Equals here because they could have different implementations from different versions + if (previousMethod.ReturnType is not null && !previousMethod.ReturnType.AreNamesEqual(item.ReturnType)) + { + continue; + } + + var parameters = item.Parameters.Except(previousMethod.Parameters, Parameter.TypeAndNameEqualityComparer); + if (parameters.All(x => x.IsOptionalInSignature)) + { + missingParameters = parameters.ToList(); + currentMethodToCall = item; + return true; + } + } + missingParameters = null; + currentMethodToCall = null; + return false; + } + + private bool CurrentContainAllPreviousParameters(MethodSignature previousMethod, MethodSignature currentMethod) + { + var set = currentMethod.Parameters.ToHashSet(Parameter.TypeAndNameEqualityComparer); + foreach (var parameter in previousMethod.Parameters) + { + if (!set.Contains(parameter)) + { + return false; + } + } + return true; + } + + private static MethodChangeset? CompareMethods(IEnumerable currentMethods, IEnumerable? previousMethods) + { + if (previousMethods is null) + { + return null; + } + var missing = new List(); + var updated = new List<(List Current, MethodSignature Previous)>(); + var set = currentMethods.ToHashSet(MethodSignature.ParameterAndReturnTypeEqualityComparer); + var dict = new Dictionary>(); + foreach (var item in currentMethods) + { + if (!dict.TryGetValue(item.Name, out var list)) + { + dict.Add(item.Name, new List { item }); + } + else + { + list.Add(item); + } + } + foreach (var item in previousMethods) + { + if (!set.Contains(item)) + { + if (dict.TryGetValue(item.Name, out var currentOverloadMethods)) + { + updated.Add((currentOverloadMethods, item)); + } + else + { + missing.Add(item); + } + } + } + return new(missing, updated); + } + + public IReadOnlyList Methods { get; } + + private IReadOnlyList PopulateMethodsFromCompilation(Compilation? compilation) + { + if (compilation is null) + { + return Array.Empty(); + } + var type = compilation.GetTypeByMetadataName($"{_namespace}.{_name}"); + if (type is null) + { + return Array.Empty(); + } + return PopulateMethods(type); + } + + private IReadOnlyList PopulateMethods(INamedTypeSymbol? typeSymbol) + { + if (typeSymbol is null) + { + // TODO: handle missing type + return Array.Empty(); + } + var result = new List(); + var methods = typeSymbol.GetMembers().OfType(); + foreach (var method in methods) + { + var description = method.GetDocumentationCommentXml(); + if (!_typeFactory.TryCreateType(method.ReturnType, out var returnType)) + { + // TODO: handle missing method return type from MgmtOutputLibrary + continue; + } + + // TODO: handle missing parameter type from MgmtOutputLibrary + var parameters = new List(); + bool isParameterTypeMissing = false; + foreach (var parameter in method.Parameters) + { + var methodParameter = FromParameterSymbol(parameter); + + // If any parameter can't be created, it means the type was removed from current version + if (methodParameter is null) + { + isParameterTypeMissing = true; + break; + } + else + { + parameters.Add(methodParameter); + } + } + + // Since we don't have the ability to create the missing types, if any parameter type is missing we can't continue to generate overload methods + if (isParameterTypeMissing) + { + continue; + } + result.Add(new MethodSignature(method.Name, null, $"{description}", BuilderHelpers.MapModifiers(method), returnType, null, parameters, IsRawSummaryText: true)); + } + return result; + } + + private Parameter? FromParameterSymbol(IParameterSymbol parameterSymbol) + { + var parameterName = parameterSymbol.Name; + if (_typeFactory.TryCreateType(parameterSymbol.Type, out var parameterType)) + { + return new Parameter(parameterName, null, parameterType, null, ValidationType.None, null); + } + + // If the parameter type can't be found from type factory, the type was removed from current version + // Since we don't have the ability to create the missing types, we can't continue to generate overload methods + return null; + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/SystemObjectType.cs b/logger/autorest.csharp/common/Output/Models/Types/SystemObjectType.cs new file mode 100644 index 0000000..dbd9c0b --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/SystemObjectType.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Azure.ResourceManager.Models; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class SystemObjectType : ObjectType + { + private static ConcurrentDictionary _typeCache = new ConcurrentDictionary(); + + public static SystemObjectType Create(Type type, string defaultNamespace, SourceInputModel? sourceInputModel, IEnumerable? backingProperties = null) + { + if (_typeCache.TryGetValue(type, out var result)) + return result; + + result = new SystemObjectType(type, defaultNamespace, sourceInputModel, backingProperties); + _typeCache.TryAdd(type, result); + return result; + } + + private readonly Type _type; + private readonly IReadOnlyDictionary _backingProperties; + + private SystemObjectType(Type type, string defaultNamespace, SourceInputModel? sourceInputModel, IEnumerable? backingProperties) : base(defaultNamespace, sourceInputModel) + { + _type = type; + DefaultName = GetNameWithoutGeneric(type); + _backingProperties = backingProperties?.ToDictionary(p => p.Declaration.Name) ?? new Dictionary(); + } + + protected override string DefaultName { get; } + protected override string DefaultNamespace => _type.Namespace ?? base.DefaultNamespace; + + public override ObjectTypeProperty? AdditionalPropertiesProperty => null; + + internal override Type? SerializeAs => GetSerializeAs(_type); + + protected override string DefaultAccessibility { get; } = "public"; + + internal Type SystemType => _type; + + internal static bool TryGetCtor(Type type, string attributeType, [MaybeNullWhen(false)] out ConstructorInfo result) + { + foreach (var ctor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.CreateInstance)) + { + if (ctor.GetCustomAttributes().FirstOrDefault(a => a.GetType().Name == attributeType) != null) + { + result = ctor; + return true; + } + } + + result = null; + return false; + } + + private static ConstructorInfo GetCtor(Type type, string attributeType) + { + if (TryGetCtor(type, attributeType, out var ctor)) + return ctor; + + throw new InvalidOperationException($"{attributeType} ctor was not found for {type.Name}"); + } + + private static Type? GetSerializeAs(Type type) => type.Name switch + { + _ when type == typeof(ResourceIdentifier) => type, + _ when type == typeof(SystemData) => type, + _ => null, + }; + + private static string GetNameWithoutGeneric(Type t) + { + if (!t.IsGenericType) + return t.Name; + + string name = t.Name; + int index = name.IndexOf('`'); + return index == -1 ? name : name.Substring(0, index); + } + + private ObjectTypeConstructor BuildConstructor(ConstructorInfo ctor, ObjectTypeConstructor? baseConstructor) + { + var parameters = new List(); + foreach (var param in ctor.GetParameters()) + { + _backingProperties.TryGetValue(param.Name!.ToCleanName(), out var backingProperty); + var parameter = new Parameter(param.Name!, FormattableStringHelpers.FromString(backingProperty?.Description) ?? $"The {param.Name}", GetCSharpType(param.ParameterType), null, ValidationType.None, null); + parameters.Add(parameter); + } + + // we should only add initializers when there is a corresponding parameter + List initializers = new List(); + foreach (var property in Properties) + { + var parameter = parameters.FirstOrDefault(p => p.Name == property.Declaration.Name.ToVariableName()); + if (parameter is not null) + { + initializers.Add(new ObjectPropertyInitializer(property, parameter)); + } + } + + var modifiers = ctor.IsFamily ? Protected : Public; + + return new ObjectTypeConstructor(Type, modifiers, parameters, initializers.ToArray(), baseConstructor); + } + + // TODO -- this might be unnecessary, need to investigate the ctor of CSharpType to make sure it could correctly handle typeof System.Nullable + private static CSharpType GetCSharpType(Type type) + { + var unwrapNullable = Nullable.GetUnderlyingType(type); + + return unwrapNullable == null ? new CSharpType(type, IsNullable(type)) : new CSharpType(unwrapNullable, true); + + static bool IsNullable(Type type) + { + if (type == null) + return true; // obvious + if (type.IsClass) + return true; // classes are nullable + if (Nullable.GetUnderlyingType(type) != null) + return true; // Nullable + return false; // value-type + } + } + + protected override ObjectTypeConstructor BuildInitializationConstructor() + => BuildConstructor(GetCtor(_type, ReferenceClassFinder.InitializationCtorAttributeName), GetBaseObjectType()?.InitializationConstructor); + + protected override IEnumerable BuildProperties() + { + var internalPropertiesToInclude = new List(); + PropertyMatchDetection.AddInternalIncludes(_type, internalPropertiesToInclude); + + foreach (var property in _type.GetProperties().Where(p => p.DeclaringType == _type).Concat(internalPropertiesToInclude)) + { + // try to get the backing property if any + _backingProperties.TryGetValue(property.Name, out var backingProperty); + + // construct the property + var getter = property.GetMethod; + var setter = property.SetMethod; + var declarationType = GetCSharpType(property.PropertyType); + MemberDeclarationOptions memberDeclarationOptions = new MemberDeclarationOptions( + getter != null && getter.IsPublic ? "public" : "internal", + property.Name, + declarationType); + InputModelProperty inputModelProperty; + if (backingProperty?.InputModelProperty is not null) + { + inputModelProperty = backingProperty.InputModelProperty; + } + else + { + //We are only handling a small subset of cases because the set of reference types used from Azure.ResourceManager is known + //If in the future we add more types which have unique cases we might need to update this code, but it will be obvious + //given that the generation will fail with the new types + InputType inputType = InputPrimitiveType.Boolean; + if (declarationType.IsDictionary) + { + inputType = new InputDictionaryType(string.Empty, InputPrimitiveType.Boolean, InputPrimitiveType.Boolean); + } + else if (declarationType.IsList) + { + inputType = new InputListType(string.Empty, string.Empty, InputPrimitiveType.Boolean); + } + inputModelProperty = new InputModelProperty(property.Name, GetSerializedName(property.Name, SystemType), GetPropertySummary(setter != null, property.Name), inputType, null, IsRequired(property, SystemType), property.IsReadOnly(), false); + + } + + yield return new ObjectTypeProperty(memberDeclarationOptions, inputModelProperty.Description, inputModelProperty.IsReadOnly, inputModelProperty, new CSharpType(property.PropertyType) + { + SerializeAs = GetSerializeAs(property.PropertyType) + }); + } + + static bool IsRequired(PropertyInfo property, Type systemType) + { + var dict = ReferenceClassFinder.GetPropertyMetadata(systemType); + + return dict[property.Name].Required; + } + + static string GetPropertySummary(bool hasSetter, string name) + { + var builder = new StringBuilder("Gets "); + if (hasSetter) + builder.Append("or sets "); + builder.Append(name); + return builder.ToString(); + } + + static string GetSerializedName(string name, Type systemType) + { + var dict = ReferenceClassFinder.GetPropertyMetadata(systemType); + + return dict[name].SerializedName; + } + } + + protected override IEnumerable BuildConstructors() + { + yield return BuildInitializationConstructor(); + yield return BuildSerializationConstructor(); + } + + protected override ObjectTypeConstructor BuildSerializationConstructor() + => BuildConstructor(GetCtor(_type, ReferenceClassFinder.SerializationCtorAttributeName), GetBaseObjectType()?.SerializationConstructor); + + protected override CSharpType? CreateInheritedType() + { + return _type.BaseType == null || _type.BaseType == typeof(object) ? null : CSharpType.FromSystemType(_type.BaseType, base.DefaultNamespace, _sourceInputModel); + } + + protected override FormattableString CreateDescription() + { + throw new NotImplementedException("Currently we don't support getting description in SystemObjectType"); + } + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/TypeDeclarationOptions.cs b/logger/autorest.csharp/common/Output/Models/Types/TypeDeclarationOptions.cs new file mode 100644 index 0000000..887d6d2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/TypeDeclarationOptions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class TypeDeclarationOptions + { + public TypeDeclarationOptions(string name, string ns, string accessibility, bool isAbstract, bool isUserDefined) + { + Name = name; + Namespace = ns; + Accessibility = accessibility; + IsAbstract = isAbstract; + IsUserDefined = isUserDefined; + } + + public string Name { get; } + public string Namespace { get; } + public string Accessibility { get; } + public bool IsAbstract { get; } + public bool IsUserDefined { get; } + public string FullName => $"{this.Namespace}.{this.Name}"; + } +} diff --git a/logger/autorest.csharp/common/Output/Models/Types/TypeProvider.cs b/logger/autorest.csharp/common/Output/Models/Types/TypeProvider.cs new file mode 100644 index 0000000..2c5d319 --- /dev/null +++ b/logger/autorest.csharp/common/Output/Models/Types/TypeProvider.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Input; +using System.Diagnostics; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; + +namespace AutoRest.CSharp.Output.Models.Types +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal abstract class TypeProvider + { + private readonly Lazy _existingType; + + protected string? _deprecation; + + private TypeDeclarationOptions? _type; + protected readonly SourceInputModel? _sourceInputModel; + + protected TypeProvider(string defaultNamespace, SourceInputModel? sourceInputModel) + { + _sourceInputModel = sourceInputModel; + DefaultNamespace = defaultNamespace; + _existingType = new Lazy(() => sourceInputModel?.FindForType(DefaultNamespace, DefaultName)); + } + + protected TypeProvider(BuildContext context) : this(context.DefaultNamespace, context.SourceInputModel) { } + + public CSharpType Type => new(this, TypeArguments); + public TypeDeclarationOptions Declaration => _type ??= BuildType(); + + protected abstract string DefaultName { get; } + protected virtual string DefaultNamespace { get; } + protected abstract string DefaultAccessibility { get; } + + public bool IsValueType => TypeKind is TypeKind.Struct or TypeKind.Enum; + public virtual bool IsEnum { get; protected init; } = false; + + private IReadOnlyList? _typeArguments; + protected IReadOnlyList TypeArguments => _typeArguments ??= BuildTypeArguments().ToArray(); + + public TypeProvider? DeclaringTypeProvider { get; protected init; } + + public string? Deprecated => _deprecation; + protected virtual TypeKind TypeKind { get; } = TypeKind.Class; + protected virtual bool IsAbstract { get; } = false; + protected INamedTypeSymbol? ExistingType => _existingType.Value; + public virtual SignatureType? SignatureType => null; + + internal virtual Type? SerializeAs => null; + + protected virtual IEnumerable BuildTypeArguments() + { + yield break; + } + + private TypeDeclarationOptions BuildType() + { + return BuilderHelpers.CreateTypeAttributes( + DefaultName, + DefaultNamespace, + DefaultAccessibility, + ExistingType, + existingTypeOverrides: TypeKind == TypeKind.Enum, + isAbstract: IsAbstract); + } + + public static string GetDefaultModelNamespace(string? namespaceOverride, string defaultNamespace) + { + if (namespaceOverride != default) + { + return namespaceOverride; + } + + if (Configuration.ModelNamespace) + { + return $"{defaultNamespace}.Models"; + } + + return defaultNamespace; + } + + public override bool Equals(object? obj) + { + if (obj is null) + return false; + if (obj is not TypeProvider other) + return false; + + return string.Equals(DefaultName, other.DefaultName, StringComparison.Ordinal) && + string.Equals(DefaultNamespace, other.DefaultNamespace, StringComparison.Ordinal) && + string.Equals(DefaultAccessibility, other.DefaultAccessibility, StringComparison.Ordinal) && + TypeKind == other.TypeKind; + } + + public override int GetHashCode() + { + return HashCode.Combine(DefaultName, DefaultNamespace, DefaultAccessibility, TypeKind); + } + + private string GetDebuggerDisplay() + { + if (_type is null) + return ""; + + return $"TypeProvider ({Declaration.Accessibility} {Declaration.Namespace}.{Declaration.Name}"; + } + } +} diff --git a/logger/autorest.csharp/common/Output/PostProcessing/PostProcessor.cs b/logger/autorest.csharp/common/Output/PostProcessing/PostProcessor.cs new file mode 100644 index 0000000..56e12b2 --- /dev/null +++ b/logger/autorest.csharp/common/Output/PostProcessing/PostProcessor.cs @@ -0,0 +1,437 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Plugins; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; + +namespace AutoRest.CSharp.Common.Output.PostProcessing; + +internal class PostProcessor +{ + private const string AspDotNetExtensionNamespace = "Microsoft.Extensions.Azure"; + private readonly string? _modelFactoryFullName; + private readonly string? _aspExtensionClassName; + private readonly ImmutableHashSet _modelsToKeep; + + public PostProcessor(ImmutableHashSet modelsToKeep, string? modelFactoryFullName = null, string? aspExtensionClassName = null) + { + _modelsToKeep = modelsToKeep; + _modelFactoryFullName = modelFactoryFullName; + _aspExtensionClassName = aspExtensionClassName; + } + + protected record TypeSymbols(ImmutableHashSet DeclaredSymbols, INamedTypeSymbol? ModelFactorySymbol, IReadOnlyDictionary> DeclaredNodesCache, IReadOnlyDictionary> DocumentsCache); + + /// + /// This method reads the project, returns the types defined in it and build symbol caches to acceralate the calculation + /// By default, the types defined in shared documents are not included. Please override to tweak this behaviour + /// + /// The of the + /// The project to extract type symbols from + /// If is true, only public types will be included. If is false, all types will be included + /// A instance of which includes the information of the declared symbols of the given accessibility, along with some useful cache that is useful in this class. + protected async Task GetTypeSymbolsAsync(Compilation compilation, Project project, bool publicOnly = true) + { + var result = new HashSet(SymbolEqualityComparer.Default); + var declarationCache = new Dictionary>(SymbolEqualityComparer.Default); + var documentCache = new Dictionary>(); + + INamedTypeSymbol? modelFactorySymbol = null; + if (_modelFactoryFullName != null) + modelFactorySymbol = compilation.GetTypeByMetadataName(_modelFactoryFullName); + INamedTypeSymbol? aspDotNetExtensionSymbol = null; + if (_aspExtensionClassName != null) + aspDotNetExtensionSymbol = compilation.GetTypeByMetadataName(_aspExtensionClassName); + + foreach (var document in project.Documents) + { + if (ShouldIncludeDocument(document)) + { + var root = await document.GetSyntaxRootAsync(); + if (root == null) + continue; + + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + + foreach (var typeDeclaration in root.DescendantNodes().OfType()) + { + var symbol = semanticModel.GetDeclaredSymbol(typeDeclaration); + if (symbol == null) + continue; + if (publicOnly && symbol.DeclaredAccessibility != Accessibility.Public && !document.Name.StartsWith("Internal/", StringComparison.Ordinal)) + continue; + + // we do not add the model factory symbol to the declared symbol list so that it will never be included in any process of internalization or removal + if (!SymbolEqualityComparer.Default.Equals(symbol, modelFactorySymbol) + && !SymbolEqualityComparer.Default.Equals(symbol, aspDotNetExtensionSymbol)) + result.Add(symbol); + + declarationCache.AddInList(symbol, typeDeclaration); + documentCache.AddInList(document, symbol, () => new HashSet(SymbolEqualityComparer.Default)); + } + } + } + + return new TypeSymbols(result.ToImmutableHashSet(SymbolEqualityComparer.Default), + modelFactorySymbol, + declarationCache.ToDictionary(kv => kv.Key, kv => kv.Value.ToImmutableHashSet(), (IEqualityComparer)SymbolEqualityComparer.Default), + documentCache.ToDictionary(kv => kv.Key, kv => kv.Value.ToImmutableHashSet(SymbolEqualityComparer.Default))); + } + + protected virtual bool ShouldIncludeDocument(Document document) => !GeneratedCodeWorkspace.IsSharedDocument(document) && !GeneratedCodeWorkspace.IsGeneratedTestDocument(document); + + /// + /// This method marks the "not publicly" referenced types as internal if they are previously defined as public. It will do this job in the following steps: + /// 1. This method will read all the public types defined in the given , and build a cache for those symbols + /// 2. Build a public reference map for those symbols + /// 3. Finds all the root symbols, please override the to control which document you would like to include + /// 4. Visit all the symbols starting from the root symbols following the reference map to get all unvisited symbols + /// 5. Change the accessibility of the unvisited symbols in step 4 to internal + /// + /// The project to process + /// The processed . is immutable, therefore this should usually be a new instance + public async Task InternalizeAsync(Project project) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) + return project; + + // get the type names suppressed by the attribute + var suppressedTypeNames = GeneratedCodeWorkspace.GetSuppressedTypeNames(compilation); + // first get all the declared symbols + var definitions = await GetTypeSymbolsAsync(compilation, project, true); + // build the reference map + var referenceMap = await new ReferenceMapBuilder(compilation, project, HasDiscriminator).BuildPublicReferenceMapAsync(definitions.DeclaredSymbols, definitions.DeclaredNodesCache); + // get the root symbols + var rootSymbols = GetRootSymbols(project, definitions); + // traverse all the root and recursively add all the things we met + var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + + var symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + + var nodesToInternalize = new Dictionary(); + foreach (var symbol in symbolsToInternalize) + { + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodesToInternalize[node] = project.GetDocumentId(node.SyntaxTree)!; + } + } + + foreach (var (model, documentId) in nodesToInternalize) + { + project = MarkInternal(project, model, documentId); + } + + var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text).Concat(suppressedTypeNames); + project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); + + return project; + } + + private async Task RemoveMethodsFromModelFactoryAsync(Project project, TypeSymbols definitions, HashSet namesToRemove) + { + var modelFactorySymbol = definitions.ModelFactorySymbol; + if (modelFactorySymbol == null) + return project; + + var nodesToRemove = new List(); + + foreach (var method in modelFactorySymbol.GetMembers().OfType()) + { + if (namesToRemove.Contains(method.Name)) + { + foreach (var reference in method.DeclaringSyntaxReferences) + { + var node = await reference.GetSyntaxAsync(); + nodesToRemove.Add(node); + } + } + } + + // find the GENERATED document of model factory (we may have the customized document of this for overloads) + Document? modelFactoryGeneratedDocument = null; + // the nodes corresponding to the model factory symbol has never been changed therefore the nodes inside the cache are still usable + foreach (var declarationNode in definitions.DeclaredNodesCache[modelFactorySymbol]) + { + var document = project.GetDocument(declarationNode.SyntaxTree); + if (document != null && GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + modelFactoryGeneratedDocument = document; + break; + } + } + + // maybe this is possible, for instance, we could be adding the customization all entries previously inside the generated model factory so that the generated model factory is empty and removed. + if (modelFactoryGeneratedDocument == null) + return project; + + var root = await modelFactoryGeneratedDocument.GetSyntaxRootAsync(); + Debug.Assert(root is not null); + root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia)!; + modelFactoryGeneratedDocument = modelFactoryGeneratedDocument.WithSyntaxRoot(root); + + // see if this class still has any method, if it contains nothing, we should remove this document + var methods = root.DescendantNodes().OfType(); + if (!methods.Any()) + { + return project.RemoveDocument(modelFactoryGeneratedDocument.Id); + } + + return modelFactoryGeneratedDocument.Project; + } + + /// + /// This method removes the no-referenced types from the . It will do this job in the following steps: + /// 1. This method will read all the defined types in the given , and build a cache for those symbols + /// 2. Build a reference map for those symbols (including non-public usage) + /// 3. Finds all the root symbols, please override the to control which document you would like to include + /// 4. Visit all the symbols starting from the root symbols following the reference map to get all unvisited symbols + /// 5. Remove the definition of the unvisited symbols in step 4 + /// + /// The project to process + /// The processed . is immutable, therefore this should usually be a new instance + public async Task RemoveAsync(Project project) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) + return project; + + // find all the declarations, including non-public declared + var definitions = await GetTypeSymbolsAsync(compilation, project, false); + // build reference map + var referenceMap = await new ReferenceMapBuilder(compilation, project, HasDiscriminator).BuildAllReferenceMapAsync(definitions.DeclaredSymbols, definitions.DocumentsCache); + // get root symbols + var rootSymbols = GetRootSymbols(project, definitions); + // traverse the map to determine the declarations that we are about to remove, starting from root nodes + var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + + referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); + + var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSymbols); + + var nodesToRemove = new List(); + foreach (var symbol in symbolsToRemove) + { + nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); + } + + // remove them one by one + project = await RemoveModelsAsync(project, nodesToRemove); + + return project; + } + + private IEnumerable AddSampleSymbols(IEnumerable referencedSymbols, ImmutableHashSet declaredSymbols) + { + List symbolsToAdd = new List(); + foreach (var symbol in declaredSymbols) + { + if (symbol.ContainingNamespace.Name == "Samples" && symbol.Name.StartsWith("Samples_") && !referencedSymbols.Any(s => s.Name == symbol.Name)) + { + symbolsToAdd.Add(symbol); + } + } + return referencedSymbols.Concat(symbolsToAdd); + } + + /// + /// Do a BFS starting from the by following the + /// + /// + /// + /// + private static IEnumerable VisitSymbolsFromRootAsync(IEnumerable rootSymbols, ReferenceMap referenceMap) + { + var queue = new Queue(rootSymbols.Concat(referenceMap.GlobalReferencedSymbols)); + var visited = new HashSet(SymbolEqualityComparer.Default); + while (queue.Count > 0) + { + var definition = queue.Dequeue(); + if (visited.Contains(definition)) + continue; + visited.Add(definition); + // add this definition to the result + yield return definition; + // add every type referenced by this node to the queue + foreach (var child in GetReferencedTypes(definition, referenceMap)) + { + queue.Enqueue(child); + } + } + } + + private static IEnumerable GetReferencedTypes(T definition, IReadOnlyDictionary> referenceMap) where T : notnull + { + if (referenceMap.TryGetValue(definition, out var references)) + return references; + + return Enumerable.Empty(); + } + + private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarationNode, DocumentId documentId) + { + var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); + var tree = declarationNode.SyntaxTree; + var document = project.GetDocument(documentId)!; + var newRoot = tree.GetRoot().ReplaceNode(declarationNode, newNode).WithAdditionalAnnotations(Simplifier.Annotation); + document = document.WithSyntaxRoot(newRoot); + return document.Project; + } + + private async Task RemoveModelsAsync(Project project, IEnumerable unusedModels) + { + // accumulate the definitions from the same document together + var documents = new Dictionary>(); + foreach (var model in unusedModels) + { + var document = project.GetDocument(model.SyntaxTree); + Debug.Assert(document != null); + if (!documents.ContainsKey(document)) + documents.Add(document, new HashSet()); + + documents[document].Add(model); + } + + foreach (var models in documents.Values) + { + project = await RemoveModelsFromDocumentAsync(project, models); + } + + return project; + } + + private static BaseTypeDeclarationSyntax ChangeModifier(BaseTypeDeclarationSyntax memberDeclaration, SyntaxKind from, SyntaxKind to) + { + var originalTokenInList = memberDeclaration.Modifiers.FirstOrDefault(token => token.IsKind(from)); + + // skip this if there is nothing to replace + if (originalTokenInList == default) + return memberDeclaration; + + var newToken = SyntaxFactory.Token(originalTokenInList.LeadingTrivia, to, originalTokenInList.TrailingTrivia); + var newModifiers = memberDeclaration.Modifiers.Replace(originalTokenInList, newToken); + return memberDeclaration.WithModifiers(newModifiers); + } + + private async Task RemoveModelsFromDocumentAsync(Project project, IEnumerable models) + { + var tree = models.First().SyntaxTree; + var document = project.GetDocument(tree); + if (document == null) + return project; + var root = await tree.GetRootAsync(); + root = root.RemoveNodes(models, SyntaxRemoveOptions.KeepNoTrivia); + document = document.WithSyntaxRoot(root!); + return document.Project; + } + + protected HashSet GetRootSymbols(Project project, TypeSymbols modelSymbols) + { + var result = new HashSet(SymbolEqualityComparer.Default); + foreach (var symbol in modelSymbols.DeclaredSymbols) + { + foreach (var declarationNode in modelSymbols.DeclaredNodesCache[symbol]) + { + var document = project.GetDocument(declarationNode.SyntaxTree); + if (document == null) + continue; + if (IsRootDocument(document)) + { + result.Add(symbol); + break; + // if any of the declaring document of this symbol is considered as a root document, we add the symbol to the root list, skipping the processing of any other documents of this symbol + } + } + } + + return result; + } + + protected virtual bool IsRootDocument(Document document) + { + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + // a document is a root document, when + // 1. it is a custom document (not generated or shared) + // 2. it is a client + // 3. user exceptions + return GeneratedCodeWorkspace.IsCustomDocument(document) || IsClientDocument(document) || ShouldKeepModel(root, _modelsToKeep); + } + + private static bool ShouldKeepModel(SyntaxNode? root, ImmutableHashSet modelsToKeep) + { + if (root is null) + return false; + + // use `BaseTypeDeclarationSyntax` to also include enums because `EnumDeclarationSyntax` extends `BaseTypeDeclarationSyntax` + // `ClassDeclarationSyntax` and `StructDeclarationSyntax` both inherit `TypeDeclarationSyntax` + var typeNodes = root.DescendantNodes().OfType(); + // there is possibility that we have multiple types defined in the same document (for instance, custom code) + return typeNodes.Any(t => modelsToKeep.Contains(t.Identifier.Text)); + } + + private static bool IsClientDocument(Document document) + { + return document.Name.EndsWith("Client.cs", StringComparison.Ordinal); + } + + protected virtual bool HasDiscriminator(BaseTypeDeclarationSyntax node, [MaybeNullWhen(false)] out HashSet identifiers) + { + identifiers = null; + // only class models will have discriminators + if (node is ClassDeclarationSyntax classDeclaration) + { + if (classDeclaration.HasLeadingTrivia) + { + var syntaxTriviaList = classDeclaration.GetLeadingTrivia(); + var filteredTriviaList = syntaxTriviaList.Where(syntaxTrivia => DiscriminatorDescFixedPart.All(syntaxTrivia.ToFullString().Contains)); + if (filteredTriviaList.Count() == 1) + { + var descendantNodes = filteredTriviaList.First().GetStructure()?.DescendantNodes().ToList(); + var filteredDescendantNodes = FilterTriviaWithDiscriminator(descendantNodes); + var identifierNodes = filteredDescendantNodes.SelectMany(node => node.DescendantNodes().OfType()); + // this is getting plain the cref content out, therefore if we write `cref="global::Azure.ResourceManager.Models.Cat"`, we will have + // global::Azure.ResourceManager.Models.Cat. But in the place we consume this set, we only need the name, therefore here we just trim off the prefixes here + identifiers = identifierNodes.Select(identifier => GetCleanName(identifier.Cref)).ToHashSet(); + return true; + } + } + return false; + } + + return false; + } + + private static string GetCleanName(CrefSyntax cref) + { + var fullString = cref.ToFullString(); + return fullString.Split('.', StringSplitOptions.RemoveEmptyEntries).Last(); + } + + private static IEnumerable FilterTriviaWithDiscriminator(List? nodes) + { + if (nodes is null) + { + return Enumerable.Empty(); + } + + // If the base class has discriminator, we will add a description at the end of the original description to add the known derived types + // Here we use the added description to filter the syntax nodes coming from xml comment to get all the derived types exactly + var targetIndex = nodes.FindLastIndex(node => node.ToFullString().Contains(DiscriminatorDescFixedPart.Last())); + return nodes.Where((val, index) => index >= targetIndex); + } + + private static readonly IReadOnlyList DiscriminatorDescFixedPart = ObjectType.DiscriminatorDescFixedPart; +} diff --git a/logger/autorest.csharp/common/Output/PostProcessing/ReferenceMap.cs b/logger/autorest.csharp/common/Output/PostProcessing/ReferenceMap.cs new file mode 100644 index 0000000..bc45e48 --- /dev/null +++ b/logger/autorest.csharp/common/Output/PostProcessing/ReferenceMap.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Common.Output.PostProcessing; + +internal class ReferenceMap : IReadOnlyDictionary> +{ + private Dictionary> _referenceMap; + + private List _globalReference; + + public ReferenceMap() + { + _referenceMap = new(SymbolEqualityComparer.Default); + _globalReference = new(); + } + + /// + /// Adds the value into the list corresponding to key + /// + /// + /// + /// true if the value is added into the list, false if the value already exists in the list + internal bool AddInList(INamedTypeSymbol key, INamedTypeSymbol value) + { + if (_referenceMap.TryGetValue(key, out var list)) + { + // the list is guaranteed to be a HashSet + var set = (HashSet)list; + return set.Add(value); + } + else + { + var newList = new HashSet(SymbolEqualityComparer.Default) + { + value + }; + _referenceMap.Add(key, newList); + return true; + } + } + + internal void AddGlobal(INamedTypeSymbol typeSymbol) + { + _globalReference.Add(typeSymbol); + } + + public IEnumerable GlobalReferencedSymbols => _globalReference; + + public IEnumerable this[INamedTypeSymbol key] => _referenceMap[key]; + + public IEnumerable Keys => _referenceMap.Keys; + + public IEnumerable> Values => _referenceMap.Values; + + public int Count => _referenceMap.Count; + + public bool ContainsKey(INamedTypeSymbol key) => _referenceMap.ContainsKey(key); + + public IEnumerator>> GetEnumerator() => _referenceMap.GetEnumerator(); + + public bool TryGetValue(INamedTypeSymbol key, [MaybeNullWhen(false)] out IEnumerable value) => _referenceMap.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => _referenceMap.GetEnumerator(); +} diff --git a/logger/autorest.csharp/common/Output/PostProcessing/ReferenceMapBuilder.cs b/logger/autorest.csharp/common/Output/PostProcessing/ReferenceMapBuilder.cs new file mode 100644 index 0000000..a12c94a --- /dev/null +++ b/logger/autorest.csharp/common/Output/PostProcessing/ReferenceMapBuilder.cs @@ -0,0 +1,291 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; + +namespace AutoRest.CSharp.Common.Output.PostProcessing +{ + internal class ReferenceMapBuilder + { + internal delegate bool HasDiscriminatorDelegate(BaseTypeDeclarationSyntax node, [MaybeNullWhen(false)] out HashSet identifiers); + + private readonly Compilation _compilation; + private readonly Project _project; + private readonly HasDiscriminatorDelegate _hasDiscriminatorFunc; + + public ReferenceMapBuilder(Compilation compilation, Project project, HasDiscriminatorDelegate hasDiscriminatorFunc) + { + _compilation = compilation; + _project = project; + _hasDiscriminatorFunc = hasDiscriminatorFunc; + } + + public async Task BuildPublicReferenceMapAsync(IEnumerable definitions, IReadOnlyDictionary> nodeCache) + { + var referenceMap = new ReferenceMap(); + foreach (var definition in definitions) + { + await ProcessPublicSymbolAsync(definition, referenceMap, nodeCache); + } + + return referenceMap; + } + + public async Task BuildAllReferenceMapAsync(IEnumerable definitions, IReadOnlyDictionary> documentCache) + { + var referenceMap = new ReferenceMap(); + foreach (var definition in definitions) + { + await ProcessSymbolAsync(definition, referenceMap, documentCache); + } + + return referenceMap; + } + + private async Task ProcessPublicSymbolAsync(INamedTypeSymbol symbol, ReferenceMap referenceMap, IReadOnlyDictionary> cache) + { + // only add to reference when myself is public + if (symbol.DeclaredAccessibility != Accessibility.Public) + return; + + // process myself, adding base and generic arguments + AddTypeSymbol(symbol, symbol, referenceMap); + + // add my sibling classes + foreach (var declaration in cache[symbol]) + { + if (_hasDiscriminatorFunc(declaration, out var identifierCandidates)) + { + // first find all the derived types from this type + foreach (var derivedTypeSymbol in await SymbolFinder.FindDerivedClassesAsync(symbol, _project.Solution)) + { + if (identifierCandidates.Contains(derivedTypeSymbol.Name)) + { + AddTypeSymbol(symbol, derivedTypeSymbol, referenceMap); + } + } + } + } + + // go over all the members + foreach (var member in symbol.GetMembers()) + { + // only go through the public members + if (member.DeclaredAccessibility != Accessibility.Public) + continue; + + switch (member) + { + case IMethodSymbol methodSymbol: + ProcessMethodSymbol(symbol, methodSymbol, referenceMap); + break; + case IPropertySymbol propertySymbol: + ProcessPropertySymbol(symbol, propertySymbol, referenceMap); + break; + case IFieldSymbol fieldSymbol: + ProcessFieldSymbol(symbol, fieldSymbol, referenceMap); + break; + case IEventSymbol eventSymbol: + ProcessEventSymbol(symbol, eventSymbol, referenceMap); + break; + case INamedTypeSymbol innerTypeSymbol: + break; // do nothing for the inner types + default: + throw new InvalidOperationException($"This case has not been covered {member.GetType()}"); + } + } + } + + private async Task ProcessSymbolAsync(INamedTypeSymbol symbol, ReferenceMap referenceMap, IReadOnlyDictionary> documentCache) + { + foreach (var reference in await SymbolFinder.FindReferencesAsync(symbol, _project.Solution)) + { + await AddReferenceToReferenceMapAsync(symbol, reference, referenceMap, documentCache); + } + + // static class can have direct references, like ClassName.Method, but the extension methods might not have direct reference to the class itself + // therefore here we find the references of all its members and add them to the reference map + await ProcessExtensionSymbol(symbol, referenceMap, documentCache); + } + + private async Task ProcessExtensionSymbol(INamedTypeSymbol extensionClassSymbol, ReferenceMap referenceMap, IReadOnlyDictionary> documentCache) + { + if (!extensionClassSymbol.IsStatic) + return; + + foreach (var member in extensionClassSymbol.GetMembers()) + { + if (member is not IMethodSymbol methodSymbol) + continue; + + if (!methodSymbol.IsExtensionMethod) + continue; + + foreach (var reference in await SymbolFinder.FindReferencesAsync(member, _project.Solution)) + { + await AddReferenceToReferenceMapAsync(extensionClassSymbol, reference, referenceMap, documentCache); + } + + // this is to hook the extension class like this: + // internal static class FooExtensions + // { + // public static string ToSerialString(this Foo foo) => foo.ToString(); + // public static Foo ToFoo(this string foo) => // omit body + // } + + // if this is an extension method, we add it to the reference map of the type it is extending to pretend that this class is a part of that type + // handle the first method above + if (methodSymbol.Parameters.FirstOrDefault()?.Type is INamedTypeSymbol typeSymbol) + referenceMap.AddInList(typeSymbol, extensionClassSymbol); + + // we also add the return type into the reference map of the extension class to cover both cases + // handle the second method above + if (methodSymbol.ReturnType is INamedTypeSymbol returnTypeSymbol) + referenceMap.AddInList(returnTypeSymbol, extensionClassSymbol); + } + } + + private async Task AddReferenceToReferenceMapAsync(INamedTypeSymbol symbol, ReferencedSymbol reference, ReferenceMap referenceMap, IReadOnlyDictionary> documentCache) + { + foreach (var location in reference.Locations) + { + var document = location.Document; + + // skip this reference if it comes from a document that does not define any symbol + if (!documentCache.TryGetValue(document, out var candidateReferenceSymbols)) + continue; + + if (candidateReferenceSymbols.Count == 1) + { + referenceMap.AddInList(candidateReferenceSymbols.Single(), symbol); + } + else + { + // fallback to calculate the symbol when the document contains multiple type symbols + // this should never happen in the generated code + // customized code might have this issue + var root = await document.GetSyntaxRootAsync(); + if (root == null) + continue; + // get the node of this reference + var node = root.FindNode(location.Location.SourceSpan); + var owner = GetOwnerTypeOfReference(node); + if (owner == null) + { + referenceMap.AddGlobal(symbol); + } + else + { + var semanticModel = _compilation.GetSemanticModel(owner.SyntaxTree); + var ownerSymbol = semanticModel.GetDeclaredSymbol(owner); + + if (ownerSymbol == null) + continue; + // add it to the map + referenceMap.AddInList(ownerSymbol, symbol); + } + } + } + } + + /// + /// This method recusively adds all related types in to the reference map as the value of key + /// + /// + /// + /// + private void AddTypeSymbol(ITypeSymbol keySymbol, ITypeSymbol? valueSymbol, ReferenceMap referenceMap) + { + if (keySymbol is not INamedTypeSymbol keyTypeSymbol) + return; + if (valueSymbol is not INamedTypeSymbol valueTypeSymbol) + return; + // add the class and all its partial classes to the map + // this will make all the partial classes are referencing each other in the reference map + // when we make the travesal over the reference map, we will not only remove one of the partial class, instead we will either keep all the partial classes (if at least one of them has references), or remove all of them (if none of them has references) + if (!referenceMap.AddInList(keyTypeSymbol, valueTypeSymbol)) + { + return; // we short cut if the valueTypeSymbol has already existed in the list to avoid infinite loops + } + // add the base type + AddTypeSymbol(keyTypeSymbol, valueTypeSymbol.BaseType, referenceMap); + // add the interfaces if there is any + foreach (var interfaceSymbol in valueTypeSymbol.Interfaces) + { + AddTypeSymbol(keyTypeSymbol, interfaceSymbol, referenceMap); + } + // add the generic type arguments + foreach (var typeArgument in valueTypeSymbol.TypeArguments) + { + AddTypeSymbol(keyTypeSymbol, typeArgument, referenceMap); + } + } + + private void ProcessMethodSymbol(INamedTypeSymbol keySymbol, IMethodSymbol methodSymbol, ReferenceMap referenceMap) + { + // add the return type + AddTypeSymbol(keySymbol, methodSymbol.ReturnType, referenceMap); + // add the parameters + foreach (var parameter in methodSymbol.Parameters) + { + AddTypeSymbol(keySymbol, parameter.Type, referenceMap); + } + } + + private void ProcessPropertySymbol(INamedTypeSymbol keySymbol, IPropertySymbol propertySymbol, ReferenceMap referenceMap) + { + AddTypeSymbol(keySymbol, propertySymbol.Type, referenceMap); + + // find the node that defines myself + var xml = propertySymbol.GetDocumentationCommentXml(); + if (string.IsNullOrEmpty(xml)) + { + return; + } + + var xdoc = XDocument.Parse(xml); + var crefs = xdoc.Descendants().Attributes("cref").Select(a => a.Value).Where(a => a[0] == 'T' && a[1] == ':').Select(a => a.Substring(2)); + + foreach (var cref in crefs) + { + var symbol = _compilation.GetTypeByMetadataName(cref); + AddTypeSymbol(keySymbol, symbol, referenceMap); + } + } + + private void ProcessFieldSymbol(INamedTypeSymbol keySymbol, IFieldSymbol fieldSymbol, ReferenceMap referenceMap) => AddTypeSymbol(keySymbol, fieldSymbol.Type, referenceMap); + + private void ProcessEventSymbol(INamedTypeSymbol keySymbol, IEventSymbol eventSymbol, ReferenceMap referenceMap) => AddTypeSymbol(keySymbol, eventSymbol.Type, referenceMap); + + /// + /// Returns the node that defines inside the document, which should be , or + /// The here should come from the result of , therefore a result is guaranteed + /// + /// + /// + private static BaseTypeDeclarationSyntax? GetOwnerTypeOfReference(SyntaxNode node) + { + SyntaxNode? current = node; + while (current != null) + { + if (current is BaseTypeDeclarationSyntax declarationNode) + return declarationNode; + + current = current.Parent; + } + + // this means owner of the reference is outside a type definition. For instance, we could have an assembly attribute that is referencing a class using `nameof` + return null; + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/AutoRestLogger.cs b/logger/autorest.csharp/common/Utilities/AutoRestLogger.cs new file mode 100644 index 0000000..f133a36 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/AutoRestLogger.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using AutoRest.CSharp.AutoRest.Communication; + +namespace AutoRest.CSharp.Common.Utilities +{ + internal static class AutoRestLogger + { + private static IPluginCommunication? _autoRest = null; + public static void Initialize(IPluginCommunication autoRest) + { + _autoRest = autoRest; + } + + public static async Task Warning(string message) + { + if (_autoRest is not null) + await _autoRest.Warning(message); + } + + public static async Task Fatal(string message) + { + if (_autoRest is not null) + await _autoRest.Fatal(message); + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/AzureCoreExtensions.cs b/logger/autorest.csharp/common/Utilities/AzureCoreExtensions.cs new file mode 100644 index 0000000..df89a04 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/AzureCoreExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Input; +using Azure.Core; + +namespace AutoRest.CSharp.Utilities +{ + internal static class AzureCoreExtensions + { + public static string ToRequestMethodName(this RequestMethod method) => method.ToString() switch + { + "GET" => nameof(RequestMethod.Get), + "POST" => nameof(RequestMethod.Post), + "PUT" => nameof(RequestMethod.Put), + "PATCH" => nameof(RequestMethod.Patch), + "DELETE" => nameof(RequestMethod.Delete), + "HEAD" => nameof(RequestMethod.Head), + "OPTIONS" => nameof(RequestMethod.Options), + "TRACE" => nameof(RequestMethod.Trace), + _ => String.Empty + }; + + public static RequestMethod ToCoreRequestMethod(this HttpMethod method) => method switch + { + HttpMethod.Delete => RequestMethod.Delete, + HttpMethod.Get => RequestMethod.Get, + HttpMethod.Head => RequestMethod.Head, + HttpMethod.Options => RequestMethod.Options, + HttpMethod.Patch => RequestMethod.Patch, + HttpMethod.Post => RequestMethod.Post, + HttpMethod.Put => RequestMethod.Put, + HttpMethod.Trace => RequestMethod.Trace, + _ => RequestMethod.Get + }; + } +} diff --git a/logger/autorest.csharp/common/Utilities/DebuggerAwaiter.cs b/logger/autorest.csharp/common/Utilities/DebuggerAwaiter.cs new file mode 100644 index 0000000..1725539 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/DebuggerAwaiter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace AutoRest.CSharp.Utilities +{ + public static class DebuggerAwaiter + { + public static void AwaitAttach() + { + while (!Debugger.IsAttached) + { + Console.Error.WriteLine($"Waiting for debugger to attach to process {Process.GetCurrentProcess().Id}"); + foreach (var _ in Enumerable.Range(0, 50)) + { + if (Debugger.IsAttached) break; + Thread.Sleep(100); + Console.Error.Write("."); + } + Console.Error.WriteLine(); + } + Debugger.Break(); + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/DictionaryExtensions.cs b/logger/autorest.csharp/common/Utilities/DictionaryExtensions.cs new file mode 100644 index 0000000..f9c7efc --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/DictionaryExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace AutoRest.CSharp.Common.Utilities +{ + + internal static class DictionaryExtensions + { + public static void CreateAndCacheResult(this IDictionary> cache, TKey key, Func createValue) where TKey : notnull + { + if (cache.TryGetValue(key, out var createCache)) + { + return; + } + + TValue? value = default; + createCache = () => value ??= createValue(); + cache[key] = createCache; + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/Disposable.cs b/logger/autorest.csharp/common/Utilities/Disposable.cs new file mode 100644 index 0000000..86b5456 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/Disposable.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Threading; + +namespace AutoRest.CSharp.Utilities +{ + /// + /// Provides a set of static methods for creating Disposables. + /// + public static class Disposable + { + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + return new AnonymousDisposable(dispose); + } + + /// + /// Creates a disposable wrapper for a disposable that will be invoked at most once. + /// + /// Disposable that will be wrapped. Disposable is guaranteed to be run at most once. + /// The disposable object that disposes wrapped object. + /// is null. + public static IDisposable Create(IDisposable disposable) + { + if (disposable == null) + { + throw new ArgumentNullException(nameof(disposable)); + } + return new DisposableWrapper(disposable); + } + + /// + /// Gets the disposable that does nothing when disposed. + /// + public static IDisposable Empty { get; } = new AnonymousDisposable(null); + + /// + /// Represents an Action-based disposable. + /// + private sealed class AnonymousDisposable : IDisposable + { + private Action? _dispose; + + /// + /// Constructs a new disposable with the given action used for disposal. + /// + /// Disposal action which will be run upon calling Dispose. + public AnonymousDisposable(Action? dispose) + { + _dispose = dispose; + } + + /// + /// Calls the disposal action if and only if the current instance hasn't been disposed yet. + /// + public void Dispose() + { + var action = Interlocked.Exchange(ref _dispose, null); + action?.Invoke(); + } + } + + /// + /// Represents a disposable wrapper. + /// + private sealed class DisposableWrapper : IDisposable + { + private IDisposable? _disposable; + + public DisposableWrapper(IDisposable disposable) + { + _disposable = disposable; + } + + /// + /// Disposes wrapped object if and only if the current instance hasn't been disposed yet. + /// + public void Dispose() + { + var disposable = Interlocked.Exchange(ref _disposable, null); + disposable?.Dispose(); + } + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/EnumExtensions.cs b/logger/autorest.csharp/common/Utilities/EnumExtensions.cs new file mode 100644 index 0000000..237595f --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/EnumExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; + +namespace AutoRest.CSharp.Utilities +{ + internal static class EnumExtensions + { + public static string? GetEnumMemberValue(this MemberInfo? memberInfo) => memberInfo?.GetCustomAttributes(true).Select(ema => ema.Value).FirstOrDefault(); + + public static string? GetEnumMemberValue(this T value) + => value?.GetType().GetMember(Enum.GetName(value.GetType(), value) ?? String.Empty).FirstOrDefault()?.GetEnumMemberValue(); + } +} diff --git a/logger/autorest.csharp/common/Utilities/EnumerableExtensions.cs b/logger/autorest.csharp/common/Utilities/EnumerableExtensions.cs new file mode 100644 index 0000000..fd8c394 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/EnumerableExtensions.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoRest.CSharp.Utilities +{ + internal static class EnumerableExtensions + { + public static IEnumerable WhereNotNull(this IEnumerable source) + { + foreach (var element in source) + { + if (element != null) + { + yield return element; + } + } + } + + //https://stackoverflow.com/a/58600254/294804 + public static void ForEachLast(this IEnumerable collection, Action? actionExceptLast = null, Action? actionOnLast = null) => + // ReSharper disable once IteratorMethodResultIsIgnored + collection.SelectLast(i => { actionExceptLast?.Invoke(i); return true; }, i => { actionOnLast?.Invoke(i); return true; }).ToArray(); + + public static IEnumerable SelectLast(this IEnumerable collection, Func? selectorExceptLast = null, Func? selectorOnLast = null) + { + using var enumerator = collection.GetEnumerator(); + var isNotLast = enumerator.MoveNext(); + while (isNotLast) + { + var current = enumerator.Current; + isNotLast = enumerator.MoveNext(); + var selector = isNotLast ? selectorExceptLast : selectorOnLast; + //https://stackoverflow.com/a/32580613/294804 + if (selector != null) + { + yield return selector.Invoke(current); + } + } + } + + //https://stackoverflow.com/a/58755692/294804 + public static List ToRecursiveOrderList(this IEnumerable collection, Expression>> childCollection) + { + var resultList = new List(); + var currentItems = new Queue<(int Index, T Item, int Depth)>(collection.Select(i => (0, i, 0))); + var depthItemCounter = 0; + var previousItemDepth = 0; + var childProperty = (PropertyInfo)((MemberExpression)childCollection.Body).Member; + while (currentItems.Count > 0) + { + var currentItem = currentItems.Dequeue(); + // Reset counter for number of items at this depth when the depth changes. + if (currentItem.Depth != previousItemDepth) depthItemCounter = 0; + var resultIndex = currentItem.Index + depthItemCounter++; + resultList.Insert(resultIndex, currentItem.Item); + + var childItems = childProperty.GetValue(currentItem.Item) as IEnumerable ?? Enumerable.Empty(); + foreach (var childItem in childItems) + { + currentItems.Enqueue((resultIndex + 1, childItem, currentItem.Depth + 1)); + } + previousItemDepth = currentItem.Depth; + } + + return resultList; + } + + [return: MaybeNull] + public static TValue GetValue(this IDictionary? dictionary, string key) => + ((dictionary?.ContainsKey(key) ?? false) && dictionary![key] is TValue item) ? item : default; + + [return: MaybeNull] + public static T GetValue(this IDictionary? dictionary, string key) => + ((dictionary?.ContainsKey(key) ?? false) && dictionary![key] is T item) ? item : default; + + public static void AddInList(this Dictionary dictionary, TKey key, TValue value, Func? collectionConstructor = null) where TKey : notnull where TList : ICollection, new() + { + if (dictionary.TryGetValue(key, out var list)) + { + list.Add(value); + } + else + { + TList newList; + if (collectionConstructor == null) + newList = new TList(); + else + newList = collectionConstructor(); + newList.Add(value); + dictionary.Add(key, newList); + } + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/ErrorHelpers.cs b/logger/autorest.csharp/common/Utilities/ErrorHelpers.cs new file mode 100644 index 0000000..06fdfac --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/ErrorHelpers.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +using System; + +namespace AutoRest.CSharp.Utilities +{ + internal static class ErrorHelpers + { + internal static string FileIssueText = "\n Please file an issue at https://github.com/Azure/autorest.csharp/issues/new. \n Attach the written 'Configuration.json' and 'CodeModel.yaml' or the original swagger so we can reproduce your error.\n"; + internal static string UpdateSwaggerOrFile = "\n\nPlease review your swagger and make necessary corrections.\n\nIf the definition is correct file an issue at https://github.com/Azure/autorest.csharp/issues/new with details to reproduce.\n"; + + // We wrap all 'reported' errors in a custom exception type so CSharpGen can recognize them as not internal errors and not show 'Internal error in AutoRest.CSharp' + internal static void ThrowError (string errorText) + { + throw new ErrorException (errorText); + } + + internal class ErrorException : Exception + { + internal string ErrorText; + + internal ErrorException(string errorText) : base(errorText) + { + ErrorText = errorText; + } + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/HashSetExtensions.cs b/logger/autorest.csharp/common/Utilities/HashSetExtensions.cs new file mode 100644 index 0000000..d1c1bd2 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/HashSetExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Common.Utilities +{ + internal static class HashSetExtensions + { + /// + /// Push an item into the set, which will be popped later when the returned is disposed. + /// + /// HashSet item + /// + /// Item to push + /// + internal static IDisposable Push(this HashSet hashSet, T item) + { + hashSet.Add(item); + return Disposable.Create(() => + { + hashSet.Remove(item); + }); + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/IsExternalInit.cs b/logger/autorest.csharp/common/Utilities/IsExternalInit.cs new file mode 100644 index 0000000..583620c --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/IsExternalInit.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace System.Runtime.CompilerServices +{ + internal class IsExternalInit{} +} diff --git a/logger/autorest.csharp/common/Utilities/JsonElementExtensions.cs b/logger/autorest.csharp/common/Utilities/JsonElementExtensions.cs new file mode 100644 index 0000000..208b4a1 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/JsonElementExtensions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; + +namespace AutoRest.CSharp.Utilities +{ + internal static class JsonElementExtensions + { + public static JsonElement? GetPropertyOrNull(this JsonElement element, string propertyName) => + element.TryGetProperty(propertyName, out var value) ? value : (JsonElement?)null; + public static JsonProperty? GetPropertyOrNull(this IEnumerable properties, string propertyName) => + properties.FirstOrDefault(p => p?.Name == propertyName); + + public static TValue? ToObject(this JsonElement element, JsonSerializerOptions? options = null) => + JsonSerializer.Deserialize(element.GetRawText(), options); + + public static JsonElement? Parse(this string jsonText) + { + try { return JsonDocument.Parse(jsonText).RootElement; } + catch { return null; } + } + + public static JsonElement[] Unwrap(this JsonElement element) => + element.ValueKind == JsonValueKind.Array ? element.EnumerateArray().ToArray() : new[] { element }; + + public static string[]? ToStringArray(this JsonElement? element) => element switch + { + { ValueKind: JsonValueKind.String } jsonElement => jsonElement.GetString()!.Split(";"), + { ValueKind: JsonValueKind.Array } jsonElement => jsonElement.EnumerateArray().Select(e => e.GetString()!).ToArray(), + _ => null + }; + + public static string? ToStringValue(this JsonElement? element) => + element?.ValueKind == JsonValueKind.String ? element.Value.GetString() : null; + public static int? ToNumber(this JsonElement? element) => + element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt32() : (int?)null; + public static bool? ToBoolean(this JsonElement? element) => + element?.ValueKind == JsonValueKind.True || element?.ValueKind == JsonValueKind.False ? element.Value.GetBoolean() : (bool?)null; + + public static bool IsNull(this JsonElement? element) => element?.ValueKind == JsonValueKind.Null; + public static bool IsObject(this JsonElement? element) => element?.ValueKind == JsonValueKind.Object; + + public static T ToType(this JsonElement? element) => + typeof(T) switch + { + var t when t == typeof(string) => (T)(object?)element.ToStringValue()!, + var t when t == typeof(string[]) => (T)(object?)element.ToStringArray()!, + var t when t == typeof(int?) => (T)(object?)element.ToNumber()!, + var t when t == typeof(bool?) => (T)(object?)element.ToBoolean()!, + var t when t == typeof(JsonElement?) => (T)(object?)element!, + _ => throw new NotSupportedException($"Type {typeof(T)} is not a supported response type.") + }; + } +} diff --git a/logger/autorest.csharp/common/Utilities/JsonExtensions.cs b/logger/autorest.csharp/common/Utilities/JsonExtensions.cs new file mode 100644 index 0000000..481d33b --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/JsonExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AutoRest.CSharp.Utilities +{ + internal static class JsonExtensions + { + public static string ToJsonArray(this IEnumerable? values) => values != null ? $"[{String.Join(",", values.Select(v => $@"""{v}"""))}]" : "[]"; + public static string ToJsonArray(this IEnumerable? values) => values != null ? $"[{String.Join(",", values.Select(v => v.ToString()))}]" : "[]"; + public static string ToJsonArray(this IEnumerable? values) => values != null ? $"[{String.Join(",", values.Select(sl => sl.ToString()))}]" : "[]"; + + public static string ToJsonBool(this bool value) => value ? "true" : "false"; + + public static string TextOrEmpty(this object? value, string text) => value != null ? text : String.Empty; + + + public static string? ToStringLiteral(this string? text) => + !String.IsNullOrEmpty(text) + ? text + .Replace("\\", "\\\\") // backslashes + .Replace("\"", "\\\"") // quotes + .Replace("\0", "\\0") // nulls + .Replace("\a", "\\a") // alert + .Replace("\b", "\\b") // backspace + .Replace("\f", "\\f") // form feed + .Replace("\n", "\\n") // newline + .Replace("\r", "\\r") // return + .Replace("\t", "\\t") // tab + .Replace("\v", "\\v") // vertical tab + : text; + + public static string ToJsonStringOrNull(this string? text) => !text.IsNullOrEmpty() ? $@"""{text}""" : "null"; + } +} diff --git a/logger/autorest.csharp/common/Utilities/NamedTypeSymbolExtensions.cs b/logger/autorest.csharp/common/Utilities/NamedTypeSymbolExtensions.cs new file mode 100644 index 0000000..a6819c3 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/NamedTypeSymbolExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using AutoRest.CSharp.Generation.Types; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Utilities +{ + internal static class NamedTypeSymbolExtensions + { + private const string GeneratedLibrary = "GeneratedCode"; + private static readonly SymbolDisplayFormat FullyQualifiedNameFormat = new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + // TODO -- to be removed when we refactor the code of sample generator in DPG. This method duplicated the existing method TypeFactory.CreateType + public static CSharpType? GetCSharpType(this INamedTypeSymbol symbol, TypeFactory factory) + { + if (symbol.ContainingAssembly.Name == GeneratedLibrary) + { + return factory.GetLibraryTypeByName(symbol.Name); + } + else + { + return GetCSharpType(symbol); + } + } + + public static CSharpType GetCSharpType(this INamedTypeSymbol symbol) + { + if (symbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + { + return GetCSharpType((INamedTypeSymbol)symbol.TypeArguments[0]).WithNullable(true); + } + + var symbolName = symbol.ToDisplayString(FullyQualifiedNameFormat); + var assemblyName = symbol.ContainingAssembly.Name; + if (symbol.TypeArguments.Length > 0) + { + symbolName += $"`{symbol.TypeArguments.Length}"; + } + + var type = Type.GetType(symbolName) ?? Type.GetType($"{symbolName}, {assemblyName}") ?? throw new InvalidOperationException($"Type '{symbolName}' can't be found in assembly '{assemblyName}'."); + return symbol.TypeArguments.Length > 0 + ? new CSharpType(type, symbol.TypeArguments.Cast().Select(GetCSharpType).ToArray()) + : type; + } + + public static bool IsSameType(this INamedTypeSymbol symbol, CSharpType type) + { + if (type.IsValueType && type.IsNullable) // for cases such as `int?` + { + if (symbol.ConstructedFrom.SpecialType != SpecialType.System_Nullable_T) + return false; + return IsSameType((INamedTypeSymbol)symbol.TypeArguments.Single(), type.WithNullable(false)); + } + + if (symbol.ContainingNamespace.ToString() != type.Namespace || symbol.Name != type.Name || symbol.TypeArguments.Length != type.Arguments.Count) + { + return false; + } + + for (int i = 0; i < type.Arguments.Count; ++i) + { + if (!IsSameType((INamedTypeSymbol)symbol.TypeArguments[i], type.Arguments[i])) + { + return false; + } + } + return true; + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/ReflectionExtensions.cs b/logger/autorest.csharp/common/Utilities/ReflectionExtensions.cs new file mode 100644 index 0000000..5447ecc --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/ReflectionExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Reflection; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Common.Utilities +{ + internal static class ReflectionExtensions + { + internal static bool IsReadOnly(this PropertyInfo property, bool allowInternal = false) + { + CSharpType propertyType = property.PropertyType; + if (propertyType.IsCollection) + { + return propertyType.IsReadOnlyDictionary || propertyType.IsReadOnlyList; + } + + var setMethod = property.GetSetMethod(nonPublic: allowInternal); + + return setMethod == null || setMethod is { IsPublic: false, IsAssembly: false }; + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/StringExtensions.cs b/logger/autorest.csharp/common/Utilities/StringExtensions.cs new file mode 100644 index 0000000..fde68ef --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/StringExtensions.cs @@ -0,0 +1,485 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Report; +using Humanizer; +using Humanizer.Inflections; +using Microsoft.CodeAnalysis.CSharp; + +namespace AutoRest.CSharp.Utilities +{ + internal static class StringExtensions + { + static StringExtensions() + { + Vocabularies.Default.AddUncountable("data"); + // "S".Singularize() throws exception, github issue link: https://github.com/Humanizr/Humanizer/issues/1154 + Vocabularies.Default.AddUncountable("S"); + Vocabularies.Default.AddIrregular("redis", "redis"); + } + + public static bool IsNullOrEmpty(this string? text) => string.IsNullOrEmpty(text); + public static bool IsNullOrWhiteSpace(this string? text) => string.IsNullOrWhiteSpace(text); + + private static bool IsWordSeparator(char c) => !SyntaxFacts.IsIdentifierPartCharacter(c) || c == '_'; + + [return: NotNullIfNotNull("name")] + public static string ToCleanName(this string name, bool isCamelCase = true) + { + if (name.IsNullOrEmpty()) + { + return name; + } + StringBuilder nameBuilder = new StringBuilder(); + + int i = 0; + + if (char.IsDigit(name[0])) + { + nameBuilder.Append("_"); + } + else + { + while (!SyntaxFacts.IsIdentifierStartCharacter(name[i])) + { + i++; + } + } + + bool upperCase = false; + int firstWordLength = 1; + for (; i < name.Length; i++) + { + var c = name[i]; + if (IsWordSeparator(c)) + { + upperCase = true; + continue; + } + + if (nameBuilder.Length == 0 && isCamelCase) + { + c = char.ToUpper(c); + upperCase = false; + } + else if (nameBuilder.Length < firstWordLength && !isCamelCase) + { + c = char.ToLower(c); + upperCase = false; + // grow the first word length when this letter follows by two other upper case letters + // this happens in OSProfile, where OS is the first word + if (i + 2 < name.Length && char.IsUpper(name[i + 1]) && (char.IsUpper(name[i + 2]) || IsWordSeparator(name[i + 2]))) + firstWordLength++; + // grow the first word length when this letter follows by another upper case letter and an end of the string + // this happens when the string only has one word, like OS, DNS + if (i + 2 == name.Length && char.IsUpper(name[i + 1])) + firstWordLength++; + } + + if (upperCase) + { + c = char.ToUpper(c); + upperCase = false; + } + + nameBuilder.Append(c); + } + + return nameBuilder.ToString(); + } + + [return: NotNullIfNotNull("name")] + public static string ToVariableName(this string name) => Configuration.AzureArm ? name.ToMgmtVariableName() : ToCleanName(name, isCamelCase: false); + + [return: NotNullIfNotNull("name")] + private static string ToMgmtVariableName(this string name) + { + string? tempName = name; + var newName = NameTransformer.Instance.EnsureNameCase(name, (applyStep) => + { + // for variable name, only log when some real changes occur. + if (tempName != applyStep.NewName.VariableName) + { + var finalName = ToCleanName(applyStep.NewName.VariableName, isCamelCase: false); + if (applyStep.MappingValue.RawValue is not null) + { + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + TransformTypeName.AcronymMapping, applyStep.MappingKey, applyStep.MappingValue.RawValue, + $"Variables.{name}", + $"ApplyAcronymMappingOnVariable", tempName, $"{applyStep.NewName.VariableName}(ToCleanName={finalName})"); + } + tempName = applyStep.NewName.VariableName; + } + }); + + return ToCleanName(newName.VariableName, isCamelCase: false); + } + + public static GetPathPartsEnumerator GetPathParts(string? path) => new GetPathPartsEnumerator(path); + + public ref struct GetPathPartsEnumerator + { + private ReadOnlySpan _path; + public Part Current { get; private set; } + + public GetPathPartsEnumerator(ReadOnlySpan path) + { + _path = path; + Current = default; + } + + public GetPathPartsEnumerator GetEnumerator() => this; + + public bool MoveNext() + { + var span = _path; + if (span.Length == 0) + { + return false; + } + + var separatorIndex = span.IndexOfAny('{', '}'); + + if (separatorIndex == -1) + { + Current = new Part(span, true); + _path = ReadOnlySpan.Empty; + return true; + } + + var separator = span[separatorIndex]; + // Handle {{ and }} escape sequences + if (separatorIndex + 1 < span.Length && span[separatorIndex + 1] == separator) + { + Current = new Part(span.Slice(0, separatorIndex + 1), true); + _path = span.Slice(separatorIndex + 2); + return true; + } + + var isLiteral = separator == '{'; + + // Skip empty literals + if (isLiteral && separatorIndex == 0 && span.Length > 1) + { + separatorIndex = span.IndexOf('}'); + if (separatorIndex == -1) + { + Current = new Part(span.Slice(1), true); + _path = ReadOnlySpan.Empty; + return true; + } + + Current = new Part(span.Slice(1, separatorIndex - 1), false); + } + else + { + Current = new Part(span.Slice(0, separatorIndex), isLiteral); + } + + _path = span.Slice(separatorIndex + 1); + return true; + } + + public readonly ref struct Part + { + public Part(ReadOnlySpan span, bool isLiteral) + { + Span = span; + IsLiteral = isLiteral; + } + + public ReadOnlySpan Span { get; } + public bool IsLiteral { get; } + + public void Deconstruct(out ReadOnlySpan span, out bool isLiteral) + { + span = Span; + isLiteral = IsLiteral; + } + + public void Deconstruct(out ReadOnlySpan span, out bool isLiteral, out int argumentIndex) + { + span = Span; + isLiteral = IsLiteral; + + if (IsLiteral) + { + argumentIndex = -1; + } + else + { + var formatSeparatorIndex = span.IndexOf(':'); + var indexSpan = formatSeparatorIndex == -1 ? span : span.Slice(0, formatSeparatorIndex); + argumentIndex = int.Parse(indexSpan); + } + } + } + } + + + public static bool IsCSharpKeyword(string? name) + { + switch (name) + { + case "abstract": + case "add": + case "alias": + case "as": + case "ascending": + case "async": + case "await": + case "base": + case "bool": + case "break": + case "by": + case "byte": + case "case": + case "catch": + case "char": + case "checked": + case "class": + case "const": + case "continue": + case "decimal": + case "default": + case "delegate": + case "descending": + case "do": + case "double": + case "dynamic": + case "else": + case "enum": + case "equals": + case "event": + case "explicit": + case "extern": + case "false": + case "finally": + case "fixed": + case "float": + case "for": + case "foreach": + case "from": + case "get": + case "global": + case "goto": + // `group` is a contextual to linq queries that we don't generate + //case "group": + case "if": + case "implicit": + case "in": + case "int": + case "interface": + case "internal": + case "into": + case "is": + case "join": + case "let": + case "lock": + case "long": + case "nameof": + case "namespace": + case "new": + case "null": + case "object": + case "on": + case "operator": + // `orderby` is a contextual to linq queries that we don't generate + //case "orderby": + case "out": + case "override": + case "params": + case "partial": + case "private": + case "protected": + case "public": + case "readonly": + case "ref": + case "remove": + case "return": + case "sbyte": + case "sealed": + // `select` is a contextual to linq queries that we don't generate + // case "select": + case "set": + case "short": + case "sizeof": + case "stackalloc": + case "static": + case "string": + case "struct": + case "switch": + case "this": + case "throw": + case "true": + case "try": + case "typeof": + case "uint": + case "ulong": + case "unchecked": + case "unmanaged": + case "unsafe": + case "ushort": + case "using": + // `value` is a contextual to getters that we don't generate + // case "value": + case "var": + case "virtual": + case "void": + case "volatile": + case "when": + case "where": + case "while": + case "yield": + return true; + default: + return false; + } + } + + /// + /// Change a word to its plural form. + /// Notice that this function will treat this word as a whole word instead of only changing the last word if it contains multiple words. Please use instead. + /// + /// + /// + /// + public static string ToPlural(this string single, bool inputIsKnownToBeSingular = true) + { + return single.Pluralize(inputIsKnownToBeSingular); + } + + public static string LastWordToPlural(this string single, bool inputIsKnownToBeSingular = true) + { + var words = single.SplitByCamelCase(); + var lastWord = words.Last(); + var lastWordPlural = lastWord.ToPlural(inputIsKnownToBeSingular); + if (inputIsKnownToBeSingular || lastWord != lastWordPlural) + { + return $"{string.Join("", words.SkipLast(1))}{lastWordPlural}"; + } + return single; + } + + /// + /// Change a word to its singular form. + /// Notice that this function will treat this word as a whole word instead of only changing the last word if it contains multiple words. Please use instead. + /// + /// + /// + /// + public static string ToSingular(this string plural, bool inputIsKnownToBePlural = true) + { + return plural.Singularize(inputIsKnownToBePlural); + } + + public static string LastWordToSingular(this string plural, bool inputIsKnownToBePlural = true) + { + var words = plural.SplitByCamelCase(); + var lastWord = words.Last(); + var lastWordSingular = lastWord.ToSingular(inputIsKnownToBePlural); + if (inputIsKnownToBePlural || lastWord != lastWordSingular) + { + return $"{string.Join("", words.SkipLast(1))}{lastWordSingular}"; + } + return plural; + } + + public static bool IsLastWordSingular(this string str) + { + return str == str.LastWordToSingular(false); + } + + public static string FirstCharToLowerCase(this string str) + { + if (string.IsNullOrEmpty(str) || char.IsLower(str[0])) + return str; + + return char.ToLower(str[0]) + str.Substring(1); + } + + public static string FirstCharToUpperCase(this string str) + { + if (string.IsNullOrEmpty(str) || char.IsUpper(str[0])) + return str; + + return char.ToUpper(str[0]) + str.Substring(1); + } + + public static IEnumerable SplitByCamelCase(this string camelCase) + { + return camelCase.Humanize().Split(' ').Select(w => w.FirstCharToUpperCase()); + } + + public static StringBuilder AppendIndentation(this StringBuilder builder, int indentation) + { + var indent = new string(' ', indentation); + return builder.Append(indent); + } + + //https://stackoverflow.com/a/8809437/294804 + public static string ReplaceFirst(this string text, string oldValue, string newValue) + { + var position = text.IndexOf(oldValue, StringComparison.Ordinal); + return position < 0 ? text : text.Substring(0, position) + newValue + text.Substring(position + oldValue.Length); + } + + public static string ReplaceLast(this string text, string oldValue, string newValue) + { + var position = text.LastIndexOf(oldValue, StringComparison.Ordinal); + return position < 0 ? text : text.Substring(0, position) + newValue + text.Substring(position + oldValue.Length); + } + + public static string RenameListToGet(this string methodName, string resourceName) + { + var newName = methodName; + if (methodName.Equals("List") || methodName.Equals("ListAll") || methodName.StartsWith("ListBy")) + { + var pluralResourceName = resourceName.LastWordToPlural(inputIsKnownToBeSingular: false); + var singularResourceName = resourceName.LastWordToSingular(inputIsKnownToBePlural: false); + var getMethodPrefix = pluralResourceName == singularResourceName ? "GetAll" : "Get"; + var wordToBeReplaced = methodName.StartsWith("ListBy") ? "List" : methodName; + newName = methodName.ReplaceFirst(wordToBeReplaced, $"{getMethodPrefix}{pluralResourceName}"); + } + else if (methodName.StartsWith("List")) + { + var words = methodName.SplitByCamelCase(); + var getMethodPrefix = "Get"; + // Cases like ListEntitiesAssignedWithTerm is difficult to parse which noun should be plural and will just make no changes to the nouns for now. + if (!words.Any(w => new HashSet { "By", "With" }.Contains(w))) + { + var pluralMethodName = methodName.LastWordToPlural(inputIsKnownToBeSingular: false); + var singularMethodName = methodName.LastWordToSingular(inputIsKnownToBePlural: false); + if (pluralMethodName == singularMethodName) + { + getMethodPrefix = "GetAll"; + } + newName = pluralMethodName; + } + newName = newName.ReplaceFirst("List", getMethodPrefix); + // For the next page method of List operations, Next is appended to the end of a List method and ListXXXNext will be renamed to GetXXXNexts. Here we need to check and fix this case by removing the "s" in the end. + if (words.Last().Equals("Next", StringComparison.Ordinal)) + { + newName = newName.TrimEnd('s'); + } + } + return newName; + } + + public static string RenameGetMethod(this string methodName, string resourceName) + { + if (methodName.Equals("Get")) + { + return $"Get{resourceName.LastWordToSingular(inputIsKnownToBePlural: false)}"; + } + return methodName; + } + } +} diff --git a/logger/autorest.csharp/common/Utilities/SyntaxReferenceExtensions.cs b/logger/autorest.csharp/common/Utilities/SyntaxReferenceExtensions.cs new file mode 100644 index 0000000..8dc50b7 --- /dev/null +++ b/logger/autorest.csharp/common/Utilities/SyntaxReferenceExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.Utilities +{ + internal static class SyntaxReferenceExtensions + { + public static string GetText(this SyntaxReference? syntaxReference) + => syntaxReference?.SyntaxTree.GetText().ToString(syntaxReference.Span) ?? string.Empty; + + public static FileLinePositionSpan GetFileLinePosition(this SyntaxReference? syntaxReference) + => syntaxReference?.SyntaxTree.GetLocation(syntaxReference.Span).GetLineSpan() ?? default; + } +} diff --git a/logger/autorest.csharp/data/AutoRest/DataPlaneOutputLibrary.cs b/logger/autorest.csharp/data/AutoRest/DataPlaneOutputLibrary.cs new file mode 100644 index 0000000..f4dfcf5 --- /dev/null +++ b/logger/autorest.csharp/data/AutoRest/DataPlaneOutputLibrary.cs @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Responses; +using Microsoft.CodeAnalysis.CSharp; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class DataPlaneOutputLibrary : OutputLibrary + { + private Lazy> _restClients; + private Lazy> _clients; + private Lazy> _operations; + private Lazy> _headerModels; + private IReadOnlyDictionary _enums; + private IReadOnlyDictionary _models; + private Lazy>> _protocolMethodsDictionary; + + private readonly InputNamespace _input; + private readonly SourceInputModel? _sourceInputModel; + private readonly Lazy _modelFactory; + private readonly string _libraryName; + private readonly TypeFactory _typeFactory; + + public DataPlaneOutputLibrary(InputNamespace inputNamespace, SourceInputModel? sourceInputModel) + { + _typeFactory = new TypeFactory(this, typeof(object)); + _sourceInputModel = sourceInputModel; + + _input = inputNamespace; + + _libraryName = Configuration.LibraryName; + + var enums = new Dictionary(); + var models = new Dictionary(); + + DpgOutputLibraryBuilder.CreateModels(_input.Models, models, _typeFactory, sourceInputModel); + DpgOutputLibraryBuilder.CreateEnums(_input.Enums, enums, models, _typeFactory, sourceInputModel); + + _enums = enums; + _models = models; + + var allModels = new List(_enums.Values); + allModels.AddRange(_models.Values); + Models = allModels; + + _restClients = new Lazy>(EnsureRestClients); + _clients = new Lazy>(EnsureClients); + _operations = new Lazy>(EnsureLongRunningOperations); + _headerModels = new Lazy>(EnsureHeaderModels); + _modelFactory = new Lazy(() => ModelFactoryTypeProvider.TryCreate(Models, _typeFactory, _sourceInputModel)); + _protocolMethodsDictionary = new Lazy>>(GetProtocolMethodsDictionary); + + ClientOptions = CreateClientOptions(); + Authentication = _input.Auth; + } + + private ClientOptionsTypeProvider? CreateClientOptions() + { + if (!Configuration.PublicClients || !_input.Clients.Any()) + { + return null; + } + + var clientPrefix = ClientBuilder.GetClientPrefix(_libraryName, _input.Name); + return new ClientOptionsTypeProvider(_sourceInputModel?.GetServiceVersionOverrides() ?? _input.ApiVersions, $"{clientPrefix}ClientOptions", Configuration.Namespace, $"Client options for {clientPrefix}Client.", _sourceInputModel); + } + + public ModelFactoryTypeProvider? ModelFactory => _modelFactory.Value; + public ClientOptionsTypeProvider? ClientOptions { get; } + public InputAuth Authentication { get; } + public IEnumerable Clients => _clients.Value.Values; + public IEnumerable LongRunningOperations => _operations.Value.Values; + public IEnumerable HeaderModels => _headerModels.Value.Values; + public IEnumerable Models { get; } + public IReadOnlyDictionary> ProtocolMethodsDictionary => _protocolMethodsDictionary.Value; + + public override CSharpType ResolveEnum(InputEnumType enumType) + => _enums.TryGetValue(enumType, out var typeProvider) + ? typeProvider.Type + : throw new InvalidOperationException($"No {nameof(EnumType)} has been created for `{enumType.Name}` {nameof(InputEnumType)}."); + + public override CSharpType ResolveModel(InputModelType model) + => _models.TryGetValue(model, out var modelTypeProvider) + ? modelTypeProvider.Type + : new CSharpType(typeof(object)); + + public override CSharpType? FindTypeByName(string originalName) => Models.Where(m => m.Declaration.Name == originalName).Select(m => m.Type).FirstOrDefault(); + + public LongRunningOperation FindLongRunningOperation(InputOperation operation) + { + Debug.Assert(operation.LongRunning != null); + + return _operations.Value[operation]; + } + + public DataPlaneClient? FindClient(InputClient inputClient) + { + _clients.Value.TryGetValue(inputClient, out var client); + return client; + } + + public DataPlaneResponseHeaderGroupType? FindHeaderModel(InputOperation operation) + { + _headerModels.Value.TryGetValue(operation, out var model); + return model; + } + + private LongRunningOperationInfo FindLongRunningOperationInfo(InputClient inputClient, InputOperation operation) + { + var client = FindClient(inputClient); + + Debug.Assert(client != null, "client != null, LROs should be disabled when public clients are disabled."); + + var nextOperationMethod = operation.Paging != null + ? client.RestClient.GetNextOperationMethod(operation) + : null; + + return new LongRunningOperationInfo( + client.Declaration.Accessibility, + ClientBuilder.GetClientPrefix(client.RestClient.Declaration.Name, string.Empty), + nextOperationMethod); + } + + public IEnumerable RestClients => _restClients.Value.Values; + + private Dictionary EnsureHeaderModels() + { + var headerModels = new Dictionary(); + if (Configuration.GenerateResponseHeaderModels) + { + foreach (var inputClient in _input.Clients) + { + var clientPrefix = ClientBuilder.GetClientPrefix(GetClientDeclarationName(inputClient), _input.Name); + foreach (var operation in inputClient.Operations) + { + var headers = DataPlaneResponseHeaderGroupType.TryCreate(operation, _typeFactory, clientPrefix, _sourceInputModel); + if (headers != null) + { + headerModels.Add(operation, headers); + } + } + } + } + + return headerModels; + } + + private Dictionary EnsureLongRunningOperations() + { + var operations = new Dictionary(); + + if (Configuration.PublicClients && Configuration.GenerateLongRunningOperationTypes) + { + foreach (var client in _input.Clients) + { + var clientName = _clients.Value[client].Declaration.Name; + var clientPrefix = ClientBuilder.GetClientPrefix(clientName, _input.Name); + + foreach (var operation in client.Operations) + { + if (operation.LongRunning is null) + { + continue; + } + + var existingType = _sourceInputModel?.FindForType(Configuration.Namespace, clientName); + var accessibility = existingType is not null + ? SyntaxFacts.GetText(existingType.DeclaredAccessibility) + : "public"; + + operations.Add(operation, new LongRunningOperation(operation, _typeFactory, accessibility, clientPrefix, FindLongRunningOperationInfo(client, operation), _sourceInputModel)); + } + } + } + + return operations; + } + + private Dictionary EnsureClients() + { + var clients = new Dictionary(); + + if (Configuration.PublicClients) + { + foreach (var inputClient in _input.Clients) + { + clients.Add(inputClient, new DataPlaneClient(inputClient, _restClients.Value[inputClient], GetClientDefaultName(inputClient), this, _sourceInputModel)); + } + } + + return clients; + } + + private Dictionary EnsureRestClients() + { + var restClients = new Dictionary(); + foreach (var client in _input.Clients) + { + var restClientBuilder = new RestClientBuilder(client.Parameters, client.Operations, _typeFactory, this); + restClients.Add(client, new DataPlaneRestClient(client, restClientBuilder, GetRestClientDefaultName(client), this, _typeFactory, _sourceInputModel)); + } + + return restClients; + } + + // Get a Dictionary> based on the "protocol-method-list" config + private static Dictionary> GetProtocolMethodsDictionary() + { + Dictionary> protocolMethodsDictionary = new(); + foreach (var operationId in Configuration.ProtocolMethodList) + { + var operationGroupKeyAndIdArr = operationId.Split('_'); + + // If "operationGroup_operationId" passed in the config + if (operationGroupKeyAndIdArr.Length > 1) + { + var operationGroupKey = operationGroupKeyAndIdArr[0]; + var methodName = operationGroupKeyAndIdArr[1]; + AddToProtocolMethodsDictionary(protocolMethodsDictionary, operationGroupKey, methodName); + } + // If operationGroup is not present, only operationId is passed in the config + else + { + AddToProtocolMethodsDictionary(protocolMethodsDictionary, "", operationId); + } + } + + return protocolMethodsDictionary; + } + + private static void AddToProtocolMethodsDictionary(Dictionary> protocolMethodsDictionary, string operationGroupKey, string methodName) + { + if (!protocolMethodsDictionary.ContainsKey(operationGroupKey)) + { + List methodList = new(); + methodList.Add(methodName); + protocolMethodsDictionary.Add(operationGroupKey, methodList); + } + else + { + var methodList = protocolMethodsDictionary[operationGroupKey]; + methodList.Add(methodName); + } + } + + private string GetRestClientDefaultName(InputClient inputClient) + { + var clientPrefix = ClientBuilder.GetClientPrefix(GetClientDeclarationName(inputClient), _input.Name); + return clientPrefix + "Rest" + ClientBuilder.GetClientSuffix(); + } + + private string GetClientDeclarationName(InputClient inputClient) + { + var defaultName = GetClientDefaultName(inputClient); + var existingType = _sourceInputModel?.FindForType(Configuration.Namespace, defaultName); + return existingType != null ? existingType.Name : defaultName; + } + + private string GetClientDefaultName(InputClient inputClient) + { + var clientPrefix = ClientBuilder.GetClientPrefix(inputClient.Name, _input.Name); + var clientSuffix = ClientBuilder.GetClientSuffix(); + return clientPrefix + clientSuffix; + } + } +} diff --git a/logger/autorest.csharp/data/AutoRest/DataPlaneTarget.cs b/logger/autorest.csharp/data/AutoRest/DataPlaneTarget.cs new file mode 100644 index 0000000..78afc45 --- /dev/null +++ b/logger/autorest.csharp/data/AutoRest/DataPlaneTarget.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal class DataPlaneTarget + { + public static void Execute(GeneratedCodeWorkspace project, InputNamespace inputNamespace, SourceInputModel? sourceInputModel) + { + var library = new DataPlaneOutputLibrary(inputNamespace, sourceInputModel); + var modelWriter = new ModelWriter(); + var clientWriter = new DataPlaneClientWriter(); + var restClientWriter = new RestClientWriter(); + var serializeWriter = new SerializationWriter(); + var headerModelModelWriter = new DataPlaneResponseHeaderGroupWriter(); + var longRunningOperationWriter = new LongRunningOperationWriter(); + + foreach (var helper in ExpressionTypeProvider.GetHelperProviders()) + { + var helperWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(helperWriter, helper).Write(); + project.AddGeneratedFile($"Internal/{helper.Type.Name}.cs", helperWriter.ToString()); + } + + foreach (var model in library.Models) + { + var codeWriter = new CodeWriter(); + modelWriter.WriteModel(codeWriter, model); + + var serializerCodeWriter = new CodeWriter(); + serializeWriter.WriteSerialization(serializerCodeWriter, model); + + var name = model.Type.Name; + project.AddGeneratedFile($"Models/{name}.cs", codeWriter.ToString()); + project.AddGeneratedFile($"Models/{name}.Serialization.cs", serializerCodeWriter.ToString()); + } + + var modelFactoryType = library.ModelFactory; + if (modelFactoryType != default) + { + var modelFactoryWriter = new ModelFactoryWriter(modelFactoryType); + modelFactoryWriter.Write(); + project.AddGeneratedFile($"{modelFactoryType.Type.Name}.cs", modelFactoryWriter.ToString()); + } + + foreach (var client in library.RestClients) + { + var restCodeWriter = new CodeWriter(); + restClientWriter.WriteClient(restCodeWriter, client); + + project.AddGeneratedFile($"{client.Type.Name}.cs", restCodeWriter.ToString()); + } + + foreach (DataPlaneResponseHeaderGroupType responseHeaderModel in library.HeaderModels) + { + var headerModelCodeWriter = new CodeWriter(); + headerModelModelWriter.WriteHeaderModel(headerModelCodeWriter, responseHeaderModel); + + project.AddGeneratedFile($"{responseHeaderModel.Type.Name}.cs", headerModelCodeWriter.ToString()); + } + + if (library.ClientOptions is not null) + { + var codeWriter = new CodeWriter(); + ClientOptionsWriter.WriteClientOptions(codeWriter, library.ClientOptions); + project.AddGeneratedFile($"{library.ClientOptions.Type.Name}.cs", codeWriter.ToString()); + } + + foreach (var client in library.Clients) + { + var codeWriter = new CodeWriter(); + clientWriter.WriteClient(codeWriter, client, library); + project.AddGeneratedFile($"{client.Type.Name}.cs", codeWriter.ToString()); + } + + foreach (var operation in library.LongRunningOperations) + { + var codeWriter = new CodeWriter(); + longRunningOperationWriter.Write(codeWriter, operation); + + project.AddGeneratedFile($"{operation.Type.Name}.cs", codeWriter.ToString()); + } + } + } +} diff --git a/logger/autorest.csharp/data/Generation/DataPlaneClientWriter.cs b/logger/autorest.csharp/data/Generation/DataPlaneClientWriter.cs new file mode 100644 index 0000000..c017b58 --- /dev/null +++ b/logger/autorest.csharp/data/Generation/DataPlaneClientWriter.cs @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Generation.Writers; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class DataPlaneClientWriter : ClientWriter + { + public void WriteClient(CodeWriter writer, DataPlaneClient client, DataPlaneOutputLibrary library) + { + var cs = client.Type; + var @namespace = cs.Namespace; + using (writer.Namespace(@namespace)) + { + writer.WriteXmlDocumentationSummary($"{client.Description}"); + using (writer.Scope($"{client.Declaration.Accessibility} partial class {cs.Name}")) + { + WriteClientFields(writer, client.RestClient, true); + WriteClientCtors(writer, client, library); + + foreach (var clientMethod in client.Methods) + { + WriteClientMethod(writer, clientMethod, true); + WriteClientMethod(writer, clientMethod, false); + } + + foreach (var pagingMethod in client.PagingMethods) + { + WritePagingOperation(writer, client, pagingMethod, true); + WritePagingOperation(writer, client, pagingMethod, false); + } + + foreach (var longRunningOperation in client.LongRunningOperationMethods) + { + WriteStartOperationOperation(writer, longRunningOperation, true); + WriteStartOperationOperation(writer, longRunningOperation, false); + } + } + } + } + + private void WriteClientMethod(CodeWriter writer, ClientMethod clientMethod, bool async) + { + CSharpType? bodyType = clientMethod.RestClientMethod.ReturnType; + CSharpType responseType = bodyType != null ? + new CSharpType(Configuration.ApiTypes.ResponseOfTType, bodyType) : + Configuration.ApiTypes.ResponseType; + + responseType = async ? new CSharpType(typeof(Task<>), responseType) : responseType; + + var parameters = clientMethod.RestClientMethod.Parameters; + writer.WriteXmlDocumentationSummary($"{clientMethod.RestClientMethod.SummaryText}"); + + foreach (Parameter parameter in parameters) + { + writer.WriteXmlDocumentationParameter(parameter.Name, $"{parameter.Description}"); + } + + writer.WriteXmlDocumentationParameter("cancellationToken", $"The cancellation token to use."); + writer.WriteXmlDocumentation("remarks", $"{clientMethod.RestClientMethod.DescriptionText}"); + + var methodName = CreateMethodName(clientMethod.Name, async); + var asyncText = async ? "async" : string.Empty; + writer.Append($"{clientMethod.Accessibility} virtual {asyncText} {responseType} {methodName}("); + + foreach (Parameter parameter in parameters) + { + writer.WriteParameter(parameter); + } + writer.Line($"{typeof(CancellationToken)} cancellationToken = default)"); + + using (writer.Scope()) + { + using (writer.WriteDiagnosticScope(clientMethod.Diagnostics, ClientDiagnosticsField)) + { + writer.Append($"return ("); + if (async) + { + writer.Append($"await "); + } + + writer.Append($"RestClient.{CreateMethodName(clientMethod.RestClientMethod.Name, async)}("); + foreach (var parameter in clientMethod.RestClientMethod.Parameters) + { + writer.Append($"{parameter.Name:I}, "); + } + writer.Append($"cancellationToken)"); + + if (async) + { + writer.Append($".ConfigureAwait(false)"); + } + + writer.Append($")"); + + if (bodyType == null && clientMethod.RestClientMethod.HeaderModel != null) + { + writer.Append($".{Configuration.ApiTypes.GetRawResponseName}()"); + } + + writer.Line($";"); + } + } + + writer.Line(); + } + + private string CreateStartOperationName(string name, bool async) => $"Start{name}{(async ? "Async" : string.Empty)}"; + + private const string EndpointVariable = "endpoint"; + private const string CredentialVariable = "credential"; + private const string OptionsVariable = "options"; + + private void WriteClientCtors(CodeWriter writer, DataPlaneClient client, DataPlaneOutputLibrary library) + { + writer.Line(); + writer.WriteXmlDocumentationSummary($"Initializes a new instance of {client.Type.Name} for mocking."); + using (writer.Scope($"protected {client.Type.Name:D}()")) + { + } + writer.Line(); + + var clientOptionsName = library.ClientOptions!.Declaration.Name; + + if (library.Authentication.ApiKey != null) + { + var ctorParams = client.GetClientConstructorParameters(Configuration.ApiTypes.KeyCredentialType); + writer.WriteXmlDocumentationSummary($"Initializes a new instance of {client.Type.Name}"); + foreach (Parameter parameter in ctorParams) + { + writer.WriteXmlDocumentationParameter(parameter.Name, $"{parameter.Description}"); + } + writer.WriteXmlDocumentationParameter(OptionsVariable, $"The options for configuring the client."); + + writer.Append($"public {client.Type.Name:D}("); + foreach (Parameter parameter in ctorParams) + { + writer.WriteParameter(parameter); + } + writer.Append($" {clientOptionsName} {OptionsVariable} = null)"); + + using (writer.Scope()) + { + writer.WriteParameterNullChecks(ctorParams); + writer.Line(); + + writer.Line($"{OptionsVariable} ??= new {clientOptionsName}();"); + writer.Line($"{ClientDiagnosticsField.GetReferenceFormattable()} = new {typeof(ClientDiagnostics)}({OptionsVariable});"); + writer.Line(Configuration.ApiTypes.GetHttpPipelineKeyCredentialString(PipelineField, OptionsVariable, CredentialVariable, library.Authentication.ApiKey.Name)); + writer.Append($"this.RestClient = new {client.RestClient.Type}("); + foreach (var parameter in client.RestClient.Parameters) + { + if (parameter.IsApiVersionParameter) + { + writer.Append($"{OptionsVariable}.Version, "); + } + else if (parameter == KnownParameters.ClientDiagnostics) + { + writer.Append($"{ClientDiagnosticsField.GetReferenceFormattable()}, "); + } + else if (parameter == KnownParameters.Pipeline) + { + writer.Append($"{PipelineField}, "); + } + else + { + writer.Append($"{parameter.Name}, "); + } + } + writer.RemoveTrailingComma(); + writer.Append($");"); + } + writer.Line(); + } + + if (library.Authentication.OAuth2 != null) + { + var ctorParams = client.GetClientConstructorParameters(typeof(TokenCredential)); + writer.WriteXmlDocumentationSummary($"Initializes a new instance of {client.Type.Name}"); + foreach (Parameter parameter in ctorParams) + { + writer.WriteXmlDocumentationParameter(parameter.Name, $"{parameter.Description}"); + } + writer.WriteXmlDocumentationParameter(OptionsVariable, $"The options for configuring the client."); + + writer.Append($"public {client.Type.Name:D}("); + foreach (Parameter parameter in ctorParams) + { + writer.WriteParameter(parameter); + } + writer.Append($" {clientOptionsName} {OptionsVariable} = null)"); + + using (writer.Scope()) + { + writer.WriteParameterNullChecks(ctorParams); + writer.Line(); + + writer.Line($"{OptionsVariable} ??= new {clientOptionsName}();"); + writer.Line($"{ClientDiagnosticsField.GetReferenceFormattable()} = new {typeof(ClientDiagnostics)}({OptionsVariable});"); + var scopesParam = new CodeWriterDeclaration("scopes"); + writer.Append($"string[] {scopesParam:D} = "); + writer.Append($"{{ "); + foreach (var credentialScope in library.Authentication.OAuth2.Scopes) + { + writer.Append($"{credentialScope:L}, "); + } + writer.RemoveTrailingComma(); + writer.Line($"}};"); + + writer.Line(Configuration.ApiTypes.GetHttpPipelineBearerString(PipelineField, OptionsVariable, CredentialVariable, scopesParam)); + writer.Append($"this.RestClient = new {client.RestClient.Type}("); + foreach (var parameter in client.RestClient.Parameters) + { + if (parameter.IsApiVersionParameter) + { + writer.Append($"{OptionsVariable}.Version, "); + } + else if (parameter == KnownParameters.ClientDiagnostics) + { + writer.Append($"{ClientDiagnosticsField.GetReferenceFormattable()}, "); + } + else if (parameter == KnownParameters.Pipeline) + { + writer.Append($"{PipelineField}, "); + } + else + { + writer.Append($"{parameter.Name}, "); + } + } + writer.RemoveTrailingComma(); + writer.Append($");"); + } + writer.Line(); + } + + var internalConstructor = BuildInternalConstructor(client); + writer.WriteMethodDocumentation(internalConstructor); + using (writer.WriteMethodDeclaration(internalConstructor)) + { + writer + .Line($"this.RestClient = new {client.RestClient.Type}({client.RestClient.Parameters.GetIdentifiersFormattable()});") + .Line($"{ClientDiagnosticsField.GetReferenceFormattable()} = {KnownParameters.ClientDiagnostics.Name:I};") + .Line($"{PipelineField} = {KnownParameters.Pipeline.Name:I};"); + } + writer.Line(); + } + + private void WritePagingOperation(CodeWriter writer, DataPlaneClient client, PagingMethod pagingMethod, bool async) + { + var pageType = pagingMethod.PagingResponse.ItemType; + var parameters = pagingMethod.Method.Parameters + .Where(p => p.Name != KnownParameters.RequestContext.Name) + .Append(KnownParameters.CancellationTokenParameter) + .ToList(); + + var pipelineReference = new Reference(PipelineField, Configuration.ApiTypes.HttpPipelineType); + var scopeName = pagingMethod.Diagnostics.ScopeName; + var nextLinkName = pagingMethod.PagingResponse.NextLinkPropertyName; + var itemName = pagingMethod.PagingResponse.ItemPropertyName; + var signature = new MethodSignature( + pagingMethod.Name, + $"{pagingMethod.Method.SummaryText}", + $"{pagingMethod.Method.DescriptionText}", + MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + new CSharpType(typeof(Pageable<>), pageType), + null, + parameters); + + writer.WriteXmlDocumentationSummary($"{pagingMethod.Method.SummaryText}"); + + foreach (Parameter parameter in parameters) + { + writer.WriteXmlDocumentationParameter(parameter.Name, $"{parameter.Description}"); + } + + writer.WriteXmlDocumentationRequiredParametersException(parameters); + writer.WriteXmlDocumentation("remarks", $"{pagingMethod.Method.DescriptionText}"); + writer.WritePageable(signature, pageType, new Reference(RestClientField, client.RestClient.Type), pagingMethod.Method, pagingMethod.NextPageMethod, ClientDiagnosticsField, pipelineReference, scopeName, itemName, nextLinkName, async); + } + + private void WriteStartOperationOperation(CodeWriter writer, LongRunningOperationMethod lroMethod, bool async) + { + RestClientMethod originalMethod = lroMethod.StartMethod; + CSharpType returnType = async ? new CSharpType(typeof(Task<>), lroMethod.Operation.Type) : lroMethod.Operation.Type; + var parameters = originalMethod.Parameters; + + writer.WriteXmlDocumentationSummary($"{originalMethod.SummaryText}"); + + foreach (Parameter parameter in parameters) + { + writer.WriteXmlDocumentationParameter(parameter.Name, $"{parameter.Description}"); + } + writer.WriteXmlDocumentationParameter("cancellationToken", $"The cancellation token to use."); + writer.WriteXmlDocumentationRequiredParametersException(parameters); + writer.WriteXmlDocumentation("remarks", $"{originalMethod.DescriptionText}"); + + string asyncText = async ? "async " : string.Empty; + writer.Append($"{lroMethod.Accessibility} virtual {asyncText}{returnType} {CreateStartOperationName(lroMethod.Name, async)}("); + foreach (Parameter parameter in parameters) + { + writer.WriteParameter(parameter); + } + writer.Line($"{typeof(CancellationToken)} cancellationToken = default)"); + + using (writer.Scope()) + { + writer.WriteParameterNullChecks(parameters); + + using (writer.WriteDiagnosticScope(lroMethod.Diagnostics, ClientDiagnosticsField)) + { + string awaitText = async ? "await" : string.Empty; + string configureText = async ? ".ConfigureAwait(false)" : string.Empty; + writer.Append($"var originalResponse = {awaitText} RestClient.{CreateMethodName(originalMethod.Name, async)}("); + foreach (Parameter parameter in parameters) + { + writer.Append($"{parameter.Name}, "); + } + + writer.Line($"cancellationToken){configureText};"); + + writer.Append($"return new {lroMethod.Operation.Type}({ClientDiagnosticsField.GetReferenceFormattable()}, {PipelineField}, RestClient.{RequestWriterHelpers.CreateRequestMethodName(originalMethod.Name)}("); + foreach (Parameter parameter in parameters) + { + writer.Append($"{parameter.Name}, "); + } + writer.RemoveTrailingComma(); + writer.Append($").{Configuration.ApiTypes.HttpMessageRequestName}, originalResponse"); + + var nextPageMethod = lroMethod.Operation.NextPageMethod; + if (nextPageMethod != null) + { + writer.Append($", (_, nextLink) => RestClient.{RequestWriterHelpers.CreateRequestMethodName(nextPageMethod)}(nextLink, {parameters.GetIdentifiersFormattable()})"); + } + + writer.Line($");"); + } + + } + writer.Line(); + } + + private static ConstructorSignature BuildInternalConstructor(DataPlaneClient client) + { + var constructorParameters = new[]{KnownParameters.ClientDiagnostics, KnownParameters.Pipeline}.Union(client.RestClient.Parameters).ToArray(); + return new ConstructorSignature(client.Type, $"Initializes a new instance of {client.Declaration.Name}", null, MethodSignatureModifiers.Internal, constructorParameters); + } + } +} diff --git a/logger/autorest.csharp/data/Generation/DataPlaneResponseHeaderGroupWriter.cs b/logger/autorest.csharp/data/Generation/DataPlaneResponseHeaderGroupWriter.cs new file mode 100644 index 0000000..ce0b5c4 --- /dev/null +++ b/logger/autorest.csharp/data/Generation/DataPlaneResponseHeaderGroupWriter.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class DataPlaneResponseHeaderGroupWriter + { + private static readonly string ResponseParameter = Configuration.ApiTypes.ResponseParameterName; + private static readonly string ResponseField = "_" + ResponseParameter; + + public void WriteHeaderModel(CodeWriter writer, DataPlaneResponseHeaderGroupType responseHeaderGroup) + { + using (writer.Namespace(responseHeaderGroup.Declaration.Namespace)) + { + if (Configuration.IsBranded) + writer.UseNamespace(new CSharpType(typeof(ResponseHeadersExtensions)).Namespace); + + using (writer.Scope($"{responseHeaderGroup.Declaration.Accessibility} partial class {responseHeaderGroup.Declaration.Name}")) + { + WriteField(writer); + WriteConstructor(writer, responseHeaderGroup); + + foreach (var method in responseHeaderGroup.Headers) + { + WriteHeaderProperty(writer, method); + } + } + } + } + + private void WriteField(CodeWriter writer) + { + writer.Line($"private readonly {Configuration.ApiTypes.ResponseType} {ResponseField};"); + } + + private void WriteConstructor(CodeWriter writer, DataPlaneResponseHeaderGroupType responseHeaderGroup) + { + using (writer.Scope($"public {responseHeaderGroup.Declaration.Name}({Configuration.ApiTypes.ResponseType} {ResponseParameter})")) + { + writer.Line($"{ResponseField} = {ResponseParameter};"); + } + } + + private void WriteHeaderProperty(CodeWriter writer, ResponseHeader header) + { + var type = header.Type; + writer.WriteXmlDocumentationSummary($"{header.Description}"); + writer.Append($"public {type} {header.Name} => "); + if (!type.IsFrameworkType && type.Implementation is EnumType enumType) + { + writer.Append($"{ResponseField}.Headers.TryGetValue({header.SerializedName:L}, out {typeof(string)} value) ? "); + writer.AppendEnumFromString(enumType, $"value"); + writer.Append($" : ({type.WithNullable(true)}) null;"); + } + else + { + writer.Append($"{ResponseField}.Headers.TryGetValue({header.SerializedName:L}, out {type} value) ? value : null;"); + } + + writer.Line(); + } + } +} diff --git a/logger/autorest.csharp/data/Output/DataPlaneClient.cs b/logger/autorest.csharp/data/Output/DataPlaneClient.cs new file mode 100644 index 0000000..9b3e86e --- /dev/null +++ b/logger/autorest.csharp/data/Output/DataPlaneClient.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; + +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models +{ + internal class DataPlaneClient : TypeProvider + { + private readonly InputClient _inputClient; + private readonly DataPlaneOutputLibrary _library; + private PagingMethod[]? _pagingMethods; + private ClientMethod[]? _methods; + private LongRunningOperationMethod[]? _longRunningOperationMethods; + + public DataPlaneClient(InputClient inputClient, DataPlaneRestClient restClient, string defaultName, DataPlaneOutputLibrary library, SourceInputModel? sourceInputModel) : base(Configuration.Namespace, sourceInputModel) + { + _inputClient = inputClient; + _library = library; + RestClient = restClient; + DefaultName = defaultName; + } + + protected override string DefaultName { get; } + public string Description => ClientBuilder.CreateDescription(_inputClient.Description, ClientBuilder.GetClientPrefix(Declaration.Name, DefaultName)); + public DataPlaneRestClient RestClient { get; } + + public ClientMethod[] Methods => _methods ??= ClientBuilder.BuildMethods(_inputClient, RestClient, Declaration).ToArray(); + + public PagingMethod[] PagingMethods => _pagingMethods ??= ClientBuilder.BuildPagingMethods(_inputClient, RestClient, Declaration).ToArray(); + + public LongRunningOperationMethod[] LongRunningOperationMethods => _longRunningOperationMethods ??= BuildLongRunningOperationMethods().ToArray(); + + protected override string DefaultAccessibility { get; } = "public"; + + private IEnumerable BuildLongRunningOperationMethods() + { + foreach (var operation in _inputClient.Operations) + { + if (operation.LongRunning == null) + { + continue; + } + + var name = operation.CleanName; + RestClientMethod startMethod = RestClient.GetOperationMethod(operation); + + yield return new LongRunningOperationMethod( + name, + _library.FindLongRunningOperation(operation), + startMethod, + new Diagnostic($"{Declaration.Name}.Start{name}") + ); + } + } + + public IReadOnlyCollection GetClientConstructorParameters(CSharpType credentialType) + { + return RestClientBuilder.GetConstructorParameters(RestClient.ClientBuilder.GetOrderedParametersByRequired(), credentialType, false); + } + } +} diff --git a/logger/autorest.csharp/data/Output/DataPlaneResponseHeaderGroupType.cs b/logger/autorest.csharp/data/Output/DataPlaneResponseHeaderGroupType.cs new file mode 100644 index 0000000..239d5c2 --- /dev/null +++ b/logger/autorest.csharp/data/Output/DataPlaneResponseHeaderGroupType.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Output.Models.Responses +{ + internal class DataPlaneResponseHeaderGroupType: TypeProvider + { + private static string[] _knownResponseHeaders = new[] + { + "Date", + "ETag", + "x-ms-client-request-id", + "x-ms-request-id" + }; + + public DataPlaneResponseHeaderGroupType(InputOperation operation, OperationResponseHeader[] httpResponseHeaders, TypeFactory typeFactory, string clientPrefix, SourceInputModel? sourceInputModel) + : base(Configuration.Namespace, sourceInputModel) + { + ResponseHeader CreateResponseHeader(OperationResponseHeader header) + { + CSharpType type = typeFactory.CreateType(header.Type); + + return new ResponseHeader( + header.Name.ToCleanName(), + header.NameInResponse, + type, + BuilderHelpers.EscapeXmlDocDescription(header.Description)); + } + + var operationName = operation.Name.ToCleanName(); + DefaultName = clientPrefix + operationName + "Headers"; + Description = $"Header model for {operationName}"; + Headers = httpResponseHeaders.Select(CreateResponseHeader).ToArray(); + } + + protected override string DefaultName { get; } + public string Description { get; } + public ResponseHeader[] Headers { get; } + protected override string DefaultAccessibility { get; } = "internal"; + + public static DataPlaneResponseHeaderGroupType? TryCreate(InputOperation operation, TypeFactory typeFactory, string clientPrefix, SourceInputModel? sourceInputModel) + { + var operationResponseHeaders = operation.Responses.SelectMany(r => r.Headers) + .Where(h => !_knownResponseHeaders.Contains(h.NameInResponse, StringComparer.InvariantCultureIgnoreCase)) + .GroupBy(h => h.NameInResponse) + // Take first header definition with any particular name + .Select(h => h.First()) + .ToArray(); + + if (!operationResponseHeaders.Any()) + { + return null; + } + + return new DataPlaneResponseHeaderGroupType(operation, operationResponseHeaders, typeFactory, clientPrefix, sourceInputModel); + } + } +} diff --git a/logger/autorest.csharp/data/Output/DataPlaneRestClient.cs b/logger/autorest.csharp/data/Output/DataPlaneRestClient.cs new file mode 100644 index 0000000..8e5e487 --- /dev/null +++ b/logger/autorest.csharp/data/Output/DataPlaneRestClient.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Output.Models +{ + internal class DataPlaneRestClient : RestClient + { + // postpone the calculation, since it depends on the initialization of context + private readonly Lazy> _protocolMethods; + private readonly DataPlaneOutputLibrary _library; + private readonly TypeFactory _typeFactory; + + public IReadOnlyList ProtocolMethods => _protocolMethods.Value; + public RestClientBuilder ClientBuilder { get; } + public ClientFields Fields { get; } + + public DataPlaneRestClient(InputClient inputClient, RestClientBuilder clientBuilder, string restClientName, DataPlaneOutputLibrary library, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + : base(inputClient, restClientName, GetOrderedParameters(clientBuilder), sourceInputModel) + { + _library = library; + _typeFactory = typeFactory; + ClientBuilder = clientBuilder; + Fields = ClientFields.CreateForRestClient(Parameters); + _protocolMethods = new Lazy>(() => GetProtocolMethods(inputClient, _library).ToList()); + } + + protected override Dictionary EnsureNormalMethods() + { + var requestMethods = new Dictionary(); + + foreach (var operation in InputClient.Operations) + { + var headerModel = _library.FindHeaderModel(operation); + requestMethods.Add(operation, ClientBuilder.BuildMethod(operation, headerModel)); + } + + return requestMethods; + } + + private static IReadOnlyList GetOrderedParameters(RestClientBuilder clientBuilder) + { + var parameters = new List(); + parameters.Add(KnownParameters.ClientDiagnostics); + parameters.Add(KnownParameters.Pipeline); + parameters.AddRange(clientBuilder.GetOrderedParametersByRequired()); + return parameters; + } + + private IEnumerable GetProtocolMethods(InputClient inputClient, DataPlaneOutputLibrary library) + { + // At least one protocol method is found in the config for this operationGroup + if (!inputClient.Operations.Any(operation => IsProtocolMethodExists(operation, inputClient, library))) + { + return Enumerable.Empty(); + } + + // Filter protocol method requests for this operationGroup based on the config + var operations = Methods + .Select(m => m.Operation) + .Where(operation => IsProtocolMethodExists(operation, inputClient, library)); + + return LowLevelClient.BuildMethods(null, _typeFactory, operations, inputClient.Parameters, Fields, GetNamespaceName(inputClient, library), GetClientName(inputClient, library), null); + } + + private static string GetNamespaceName(InputClient inputClient, DataPlaneOutputLibrary library) + => library.FindClient(inputClient)?.Declaration.Namespace ?? Configuration.Namespace; + + private static string GetClientName(InputClient inputClient, DataPlaneOutputLibrary library) + => library.FindClient(inputClient)?.Declaration.Name ?? inputClient.Name; + + private static bool IsProtocolMethodExists(InputOperation operation, InputClient inputClient, DataPlaneOutputLibrary library) + => library.ProtocolMethodsDictionary.TryGetValue(inputClient.Key, out var protocolMethods) && + protocolMethods.Any(m => m.Equals(operation.Name, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/logger/autorest.csharp/level/AutoRest/DpgOutputLibrary.cs b/logger/autorest.csharp/level/AutoRest/DpgOutputLibrary.cs new file mode 100644 index 0000000..bc5bf77 --- /dev/null +++ b/logger/autorest.csharp/level/AutoRest/DpgOutputLibrary.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.LowLevel.Output; +using AutoRest.CSharp.LowLevel.Output.Samples; +using AutoRest.CSharp.LowLevel.Output.Tests; + +namespace AutoRest.CSharp.Output.Models.Types +{ + internal class DpgOutputLibrary : OutputLibrary + { + private readonly string _libraryName; + private readonly IReadOnlyDictionary _enums; + private readonly IReadOnlyDictionary _models; + private readonly bool _isTspInput; + private readonly SourceInputModel? _sourceInputModel; + + public TypeFactory TypeFactory { get; } + public IEnumerable Enums => _enums.Values; + public IEnumerable Models + { + get + { + // Skip the replaced model, e.g. the replaced ErrorResponse. + foreach (var (key, model) in _models) + { + var type = TypeFactory.CreateType(key); + if (type is { IsFrameworkType: false, Implementation: ModelTypeProvider implementation} && model == implementation) + { + yield return model; + } + } + } + } + public IReadOnlyList RestClients { get; } + public ClientOptionsTypeProvider ClientOptions { get; } + public IEnumerable AllModels => new List(_enums.Values).Concat(Models); + + public DpgOutputLibrary(string libraryName, IReadOnlyDictionary enums, IReadOnlyDictionary models, IReadOnlyList restClients, ClientOptionsTypeProvider clientOptions, bool isTspInput, SourceInputModel? sourceInputModel) + { + TypeFactory = new TypeFactory(this, typeof(BinaryData)); + _libraryName = libraryName; + _enums = enums; + _models = models; + _isTspInput = isTspInput; + _sourceInputModel = sourceInputModel; + RestClients = restClients; + ClientOptions = clientOptions; + } + + private IEnumerable? _accessOverriddenModels; + public IEnumerable AccessOverriddenModels => _accessOverriddenModels ??= Enums.Where(e => e.IsAccessibilityOverridden).Select(e => e.Declaration.Name) + .Concat(Models.Where(m => m.IsAccessibilityOverridden).Select(m => m.Declaration.Name)); + + private AspDotNetExtensionTypeProvider? _aspDotNetExtension; + public AspDotNetExtensionTypeProvider AspDotNetExtension => _aspDotNetExtension ??= new AspDotNetExtensionTypeProvider(RestClients, Configuration.Namespace, _sourceInputModel); + + private ModelFactoryTypeProvider? _modelFactoryProvider; + public ModelFactoryTypeProvider? ModelFactory => _modelFactoryProvider ??= ModelFactoryTypeProvider.TryCreate(AllModels, TypeFactory, _sourceInputModel); + + private DpgTestBaseProvider? _dpgTestBase; + public DpgTestBaseProvider DpgTestBase => _dpgTestBase ??= new DpgTestBaseProvider(Configuration.Namespace, RestClients, DpgTestEnvironment, _sourceInputModel); + + private DpgTestEnvironmentProvider? _dpgTestEnvironment; + public DpgTestEnvironmentProvider DpgTestEnvironment => _dpgTestEnvironment ??= new DpgTestEnvironmentProvider(Configuration.Namespace, _sourceInputModel); + + private Dictionary? _dpgClientTestProviders; + private Dictionary DpgClientTestProviders => _dpgClientTestProviders ??= EnsureDpgClientTestProviders(); + + private Dictionary EnsureDpgClientTestProviders() + { + var result = new Dictionary(); + foreach (var client in RestClients) + { + DpgClientTestProvider testCaseProvider = Configuration.IsBranded ? + new DpgClientRecordedTestProvider(Configuration.Namespace, client, DpgTestBase, _sourceInputModel) : + new SmokeTestProvider(Configuration.Namespace, client, _sourceInputModel); + if (!testCaseProvider.IsEmpty) + { + result.Add(client, testCaseProvider); + } + } + + return result; + } + + public DpgClientTestProvider? GetTestForClient(LowLevelClient client) => DpgClientTestProviders.TryGetValue(client, out var test) ? test : null; + + private Dictionary? _dpgClientSampleProviders; + private Dictionary DpgClientSampleProviders => _dpgClientSampleProviders ??= EnsureDpgSampleProviders(); + + private Dictionary EnsureDpgSampleProviders() + { + var result = new Dictionary(); + + // we do not write samples if the library is not branded, or samples are turned off + if (!Configuration.IsBranded || !Configuration.GenerateSampleProject) + return result; + + foreach (var client in RestClients) + { + var sampleProvider = new DpgClientSampleProvider(Configuration.Namespace, client, _sourceInputModel); + if (!sampleProvider.IsEmpty) + result.Add(client, sampleProvider); + } + + return result; + } + + public DpgClientSampleProvider? GetSampleForClient(LowLevelClient client) + => DpgClientSampleProviders.TryGetValue(client, out var sample) ? sample : null; + + public override CSharpType ResolveEnum(InputEnumType enumType) + { + if (!_isTspInput) + { + return TypeFactory.CreateType(enumType.ValueType); + } + + if (_enums.TryGetValue(enumType, out var typeProvider)) + { + return typeProvider.Type; + } + + throw new InvalidOperationException($"No {nameof(EnumType)} has been created for `{enumType.Name}` {nameof(InputEnumType)}."); + } + + public override CSharpType ResolveModel(InputModelType model) + => _models.TryGetValue(model, out var modelTypeProvider) ? modelTypeProvider.Type : new CSharpType(typeof(object)); + + public override CSharpType? FindTypeByName(string originalName) + { + foreach (var model in Models) + { + if (model.Declaration.Name == originalName) + return model.Type; + } + + foreach (var e in Enums) + { + if (e.Declaration.Name == originalName) + return e.Type; + } + + return null; + } + } +} diff --git a/logger/autorest.csharp/level/AutoRest/LowLevelTarget.cs b/logger/autorest.csharp/level/AutoRest/LowLevelTarget.cs new file mode 100644 index 0000000..7420923 --- /dev/null +++ b/logger/autorest.csharp/level/AutoRest/LowLevelTarget.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Generation.Writers; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.PostProcessing; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal class LowLevelTarget + { + public static async Task ExecuteAsync(GeneratedCodeWorkspace project, InputNamespace inputNamespace, SourceInputModel? sourceInputModel, bool isTspInput) + { + var library = new DpgOutputLibraryBuilder(inputNamespace, sourceInputModel).Build(isTspInput); + + foreach (var model in library.AllModels) + { + var codeWriter = new CodeWriter(); + var modelWriter = new ModelWriter(); + modelWriter.WriteModel(codeWriter, model); + var folderPath = Configuration.ModelNamespace ? "Models/" : ""; + project.AddGeneratedFile($"{folderPath}{model.Type.Name}.cs", codeWriter.ToString()); + + var serializationCodeWriter = new CodeWriter(); + var serializationWriter = new SerializationWriter(); + serializationWriter.WriteSerialization(serializationCodeWriter, model); + project.AddGeneratedFile($"{folderPath}{model.Type.Name}.Serialization.cs", serializationCodeWriter.ToString()); + } + + foreach (var client in library.RestClients) + { + var dpgClientWriter = new DpgClientWriter(library, client); + dpgClientWriter.WriteClient(); + project.AddGeneratedFile($"{client.Type.Name}.cs", dpgClientWriter.ToString()); + + var sampleProvider = library.GetSampleForClient(client); + // write samples + if (sampleProvider != null) + { + var clientExampleFilename = $"../../tests/Generated/Samples/{sampleProvider.Type.Name}.cs"; + var clientSampleWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(clientSampleWriter, sampleProvider).Write(); + project.AddGeneratedTestFile(clientExampleFilename, clientSampleWriter.ToString()); + project.AddGeneratedDocFile(dpgClientWriter.XmlDocWriter.Filename, new XmlDocumentFile(clientExampleFilename, dpgClientWriter.XmlDocWriter)); + } + } + + var optionsWriter = new CodeWriter(); + ClientOptionsWriter.WriteClientOptions(optionsWriter, library.ClientOptions); + project.AddGeneratedFile($"{library.ClientOptions.Type.Name}.cs", optionsWriter.ToString()); + + if (Configuration.IsBranded) + { + var extensionWriter = new AspDotNetExtensionWriter(library.AspDotNetExtension); + extensionWriter.Write(); + project.AddGeneratedFile($"{library.AspDotNetExtension.Type.Name}.cs", extensionWriter.ToString()); + } + + var modelFactoryProvider = library.ModelFactory; + if (modelFactoryProvider != null) + { + var modelFactoryWriter = new ModelFactoryWriter(modelFactoryProvider); + modelFactoryWriter.Write(); + project.AddGeneratedFile($"{modelFactoryProvider.Type.Name}.cs", modelFactoryWriter.ToString()); + } + + if (Configuration.GenerateTestProject) + { + if (Configuration.IsBranded) + { + // write test base and test env + var testBaseWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(testBaseWriter, library.DpgTestBase).Write(); + project.AddGeneratedTestFile($"../../tests/Generated/Tests/{library.DpgTestBase.Type.Name}.cs", testBaseWriter.ToString()); + + var testEnvWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(testEnvWriter, library.DpgTestEnvironment).Write(); + project.AddGeneratedTestFile($"../../tests/Generated/Tests/{library.DpgTestEnvironment.Type.Name}.cs", testEnvWriter.ToString()); + } + + // write the client test files + foreach (var client in library.RestClients) + { + var clientTestProvider = library.GetTestForClient(client); + if (clientTestProvider != null) + { + var clientTestFilename = $"../../tests/Generated/Tests/{clientTestProvider.Type.Name}.cs"; + var clientTestWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(clientTestWriter, clientTestProvider).Write(); + project.AddGeneratedTestFile(clientTestFilename, clientTestWriter.ToString()); + } + } + } + + foreach (var helper in ExpressionTypeProvider.GetHelperProviders()) + { + var helperWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(helperWriter, helper).Write(); + project.AddGeneratedFile($"Internal/{helper.Type.Name}.cs", helperWriter.ToString()); + } + + await project.PostProcessAsync(new PostProcessor( + modelsToKeep: library.AccessOverriddenModels.ToImmutableHashSet(), + modelFactoryFullName: modelFactoryProvider?.FullName, + aspExtensionClassName: library.AspDotNetExtension.FullName)); + } + } +} diff --git a/logger/autorest.csharp/level/Extensions/Snippets.ExampleValues.cs b/logger/autorest.csharp/level/Extensions/Snippets.ExampleValues.cs new file mode 100644 index 0000000..112756b --- /dev/null +++ b/logger/autorest.csharp/level/Extensions/Snippets.ExampleValues.cs @@ -0,0 +1,467 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Xml; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Output.Samples.Models; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using Microsoft.CodeAnalysis.CSharp; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.LowLevel.Extensions +{ + internal static partial class ExampleValueSnippets + { + private static ValueExpression GetExpression(CSharpType type, InputExampleValue exampleValue, SerializationFormat serializationFormat, bool includeCollectionInitialization = true) + => type switch + { + { IsReadOnlyMemory: true } => GetExpressionForList(type, exampleValue, serializationFormat, true), + { IsList: true } => GetExpressionForList(type, exampleValue, serializationFormat, includeCollectionInitialization), + { IsDictionary: true } => GetExpressionForDictionary(type, exampleValue, serializationFormat, includeCollectionInitialization), + { SerializeAs: { } serializedType } => GetExpressionForFrameworkType(serializedType, exampleValue, serializationFormat, includeCollectionInitialization), + { IsFrameworkType: true, FrameworkType: { } frameworkType } => GetExpressionForFrameworkType(frameworkType, exampleValue, serializationFormat, includeCollectionInitialization), + _ => GetExpressionForTypeProvider(type, exampleValue) + }; + + private static ValueExpression GetExpressionForFrameworkType(Type frameworkType, InputExampleValue exampleValue, SerializationFormat serializationFormat, bool includeCollectionInitialization = true) + { + // handle objects - we usually do not generate object types, but for some rare cases (such as union type) we generate object + // and we get this case in the free form object initialization as well + if (frameworkType == typeof(object)) + { + return GetExpressionForFreeFormObject(exampleValue, includeCollectionInitialization); + } + + // handle RequestContent + if (frameworkType == Configuration.ApiTypes.RequestContentType) + { + return GetExpressionForRequestContent(exampleValue); + } + + if (frameworkType == typeof(ETag) || + frameworkType == typeof(Uri) || + frameworkType == typeof(ResourceIdentifier) || + frameworkType == typeof(ResourceType) || + frameworkType == typeof(ContentType) || + frameworkType == typeof(RequestMethod) || + frameworkType == typeof(AzureLocation)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue != null) + return New.Instance(frameworkType, Literal(rawValue.RawValue.ToString()!)); + else + return frameworkType.IsValueType ? Default.CastTo(frameworkType) : Null.CastTo(frameworkType); + } + + if (frameworkType == typeof(IPAddress)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue != null) + return new InvokeStaticMethodExpression(typeof(IPAddress), nameof(IPAddress.Parse), new[] { Literal(rawValue.RawValue.ToString()!) }); + else + return Null.CastTo(frameworkType); + } + + if (frameworkType == typeof(BinaryData)) + { + if (exampleValue is not InputExampleRawValue rawValue || rawValue.RawValue != null) + return GetExpressionForBinaryData(exampleValue); + else + return Null.CastTo(frameworkType); + } + + if (frameworkType == typeof(TimeSpan)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is not null) + { + switch (serializationFormat) + { + case SerializationFormat.Duration_Seconds or SerializationFormat.Duration_Seconds_Float or SerializationFormat.Duration_Seconds_Double: + if (rawValue.RawValue is int or float or double) + return new InvokeStaticMethodExpression(typeof(TimeSpan), nameof(TimeSpan.FromSeconds), new[] { Literal(rawValue.RawValue) }); + break; + case SerializationFormat.Duration_ISO8601: + if (rawValue.RawValue is string duration) + return new InvokeStaticMethodExpression(typeof(XmlConvert), nameof(XmlConvert.ToTimeSpan), new[] { Literal(duration) }); + break; + case SerializationFormat.Time_ISO8601: + if (rawValue.RawValue is string time) + return new InvokeStaticMethodExpression(typeof(TimeSpan), nameof(TimeSpan.Parse), new[] { Literal(time) }); + break; + }; + } + + return Default.CastTo(frameworkType); + } + + if (frameworkType == typeof(DateTimeOffset)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is not null) + { + switch (serializationFormat) + { + case SerializationFormat.DateTime_Unix: + long? time = rawValue.RawValue switch + { + int i => i, + long l => l, + _ => null + }; + if (time != null) + return DateTimeOffsetExpression.FromUnixTimeSeconds(Long(time.Value)); + break; + case SerializationFormat.DateTime_RFC1123 or SerializationFormat.DateTime_RFC3339 or SerializationFormat.DateTime_RFC7231 or SerializationFormat.DateTime_ISO8601 or SerializationFormat.Date_ISO8601: + if (rawValue.RawValue is string s) + return DateTimeOffsetExpression.Parse(s); + break; + } + } + + return Default.CastTo(frameworkType); + } + + if (frameworkType == typeof(Guid)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is string s) + { + return GuidExpression.Parse(s); + } + + return Default.CastTo(frameworkType); + } + + if (frameworkType == typeof(char) || + frameworkType == typeof(sbyte) || + frameworkType == typeof(byte) || + frameworkType == typeof(short) || + frameworkType == typeof(int) || + frameworkType == typeof(long) || + frameworkType == typeof(float) || + frameworkType == typeof(double) || + frameworkType == typeof(decimal)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is char or short or byte or sbyte or int or long or float or double or decimal) + { + if (frameworkType == rawValue.RawValue.GetType()) + return Literal(rawValue.RawValue); + else + return new CastExpression(Literal(rawValue.RawValue), frameworkType); + } + + return Default.CastTo(frameworkType); + } + + if (frameworkType == typeof(string)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is not null) + { + return Literal(rawValue.RawValue.ToString()); + } + + return Null.CastTo(frameworkType); + } + + if (frameworkType == typeof(bool)) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is bool b) + return Literal(b); + + return Default.CastTo(frameworkType); + } + + if (frameworkType == typeof(byte[])) + { + if (exampleValue is InputExampleRawValue rawValue && rawValue.RawValue is not null) + return new TypeReference(typeof(Encoding)).Property(nameof(Encoding.UTF8)).Invoke(nameof(Encoding.GetBytes), Literal(rawValue.RawValue.ToString())); + + return Null.CastTo(frameworkType); + } + + return frameworkType.IsValueType ? Default.CastTo(frameworkType) : Null.CastTo(frameworkType); + } + + public static ValueExpression GetExpression(InputExampleParameterValue exampleParameterValue, SerializationFormat serializationFormat) + { + if (exampleParameterValue.Value != null) + return GetExpression(exampleParameterValue.Type, exampleParameterValue.Value, serializationFormat); + else if (exampleParameterValue.Expression != null) + return exampleParameterValue.Expression; + else + throw new InvalidOperationException("this should never happen"); + } + + private static ValueExpression GetExpressionForRequestContent(InputExampleValue value) + { + if (value is InputExampleRawValue rawValue && rawValue.RawValue == null) + { + return Null; + } + else + { + var freeFormObjectExpression = GetExpressionForFreeFormObject(value, includeCollectionInitialization: true); + return Configuration.ApiTypes.GetCreateFromStreamSampleExpression(freeFormObjectExpression); + } + } + + private static ValueExpression GetExpressionForList(CSharpType listType, InputExampleValue exampleValue, SerializationFormat serializationFormat, bool includeCollectionInitialization = true) + { + var exampleListValue = exampleValue as InputExampleListValue; + var elementType = listType.ElementType; + var elementExpressions = new List(); + // the collections in our generated SDK could never be assigned to, therefore if we have null value here, we can only assign an empty collection + foreach (var itemValue in exampleListValue?.Values ?? Enumerable.Empty()) + { + var elementExpression = GetExpression(elementType, itemValue, serializationFormat, includeCollectionInitialization); + elementExpressions.Add(elementExpression); + } + + // we only put the array inline when the element is framework type or enum (which is basically framework type generated by us) + var isNotInline = elementType is { IsFrameworkType: false, Implementation: not EnumType } // when the element is a complex object type (not enum) + || elementType.IsCollection // when the element is a collection + || elementType is { IsFrameworkType: true, FrameworkType: { } type } && (type == typeof(object) || type == typeof(BinaryData)); + + return includeCollectionInitialization + ? New.Array(elementType, !isNotInline, elementExpressions.ToArray()) + : new ArrayInitializerExpression(elementExpressions.ToArray()); + } + + private static ValueExpression GetExpressionForDictionary(CSharpType dictionaryType, InputExampleValue exampleValue, SerializationFormat serializationFormat, bool includeCollectionInitialization = true) + { + var exampleObjectValue = exampleValue as InputExampleObjectValue; + // since this is a dictionary, we take the first generic argument as the key type + // this is important because in our SDK, the key of a dictionary is not always a string. It could be a string-like type, for instance, a ResourceIdentifier + var keyType = dictionaryType.Arguments[0]; + // the second as the value type + var valueType = dictionaryType.Arguments[1]; + var elementExpressions = new List<(ValueExpression KeyExpression, ValueExpression ValueExpression)>(); + // the collections in our generated SDK could never be assigned to, therefore if we have null value here, we can only assign an empty collection + foreach (var (key, value) in exampleObjectValue?.Values ?? new Dictionary()) + { + var keyExpression = GetExpression(keyType, InputExampleValue.Value(InputPrimitiveType.String, key), SerializationFormat.Default); + var valueExpression = GetExpression(valueType, value, serializationFormat, includeCollectionInitialization); + elementExpressions.Add((keyExpression, valueExpression)); + } + + return includeCollectionInitialization + ? New.Dictionary(keyType, valueType, elementExpressions.ToArray()) + : new DictionaryInitializerExpression(elementExpressions.ToArray()); + } + + private static ValueExpression GetExpressionForBinaryData(InputExampleValue exampleValue) + { + //always use FromObjectAsJson for BinaryData so that the serialization works correctly. + return BinaryDataExpression.FromObjectAsJson(GetExpressionForFreeFormObject(exampleValue, true)); + } + + private static ValueExpression GetExpressionForFreeFormObject(InputExampleValue exampleValue, bool includeCollectionInitialization = true) => exampleValue switch + { + InputExampleRawValue rawValue => rawValue.RawValue == null ? + Null : + GetExpressionForFrameworkType(rawValue.RawValue.GetType(), exampleValue, SerializationFormat.Default, includeCollectionInitialization), + InputExampleListValue listValue => GetExpressionForList(typeof(IList), listValue, SerializationFormat.Default), + InputExampleObjectValue objectValue => CanBeInstantiatedByAnonymousObject(objectValue) ? + GetExpressionForAnonymousObject(objectValue, includeCollectionInitialization) : + GetExpressionForDictionary(typeof(Dictionary), objectValue, SerializationFormat.Default, includeCollectionInitialization), + InputExampleStreamValue streamValue => InvokeFileOpenRead(streamValue.Filename), + _ => throw new InvalidOperationException($"unhandled case {exampleValue}") + }; + + private static ValueExpression GetExpressionForAnonymousObject(InputExampleObjectValue exampleObjectValue, bool includeCollectionInitialization = true) + { + // the collections in our generated SDK could never be assigned to, therefore if we have null value here, we can only assign an empty collection + var keyValues = exampleObjectValue?.Values ?? new Dictionary(); + if (keyValues.Any()) + { + var properties = new Dictionary(); + foreach (var (key, value) in keyValues) + { + // we only write a property when it is not null because an anonymous object cannot have null assignments (causes compilation error) + if (value is InputExampleRawValue rawValue && rawValue.RawValue == null) + continue; + + var valueExpression = GetExpression(typeof(object), value, SerializationFormat.Default, includeCollectionInitialization); + properties.Add(key, valueExpression); + } + + return New.Anonymous(properties); + } + else + { + return New.Instance(typeof(object)); + } + } + + private static bool CanBeInstantiatedByAnonymousObject(InputExampleObjectValue objectValue) + { + foreach (var (key, _) in objectValue.Values) + { + if (!SyntaxFacts.IsValidIdentifier(key) || SyntaxFacts.IsReservedKeyword(SyntaxFacts.GetKeywordKind(key))) + { + return false; + } + } + + return true; + } + + private static ValueExpression GetExpressionForTypeProvider(CSharpType type, InputExampleValue exampleValue) + { + return type.Implementation switch + { + ObjectType objectType => GetExpressionForObjectType(objectType, (exampleValue as InputExampleObjectValue)?.Values), + EnumType enumType when exampleValue is InputExampleRawValue rawValue => GetExpressionForEnumType(enumType, rawValue.RawValue!), + _ => type.IsValueType && !type.IsNullable ? Default.CastTo(type) : Null.CastTo(type), + }; + } + + private static ObjectType GetActualImplementation(ObjectType objectType, IReadOnlyDictionary valueDict) + { + var discriminator = objectType.Discriminator; + // check if this has a discriminator + if (discriminator == null || !discriminator.HasDescendants) + return objectType; + var discriminatorPropertyName = discriminator.SerializedName; + // get value of this in the valueDict and we should always has a discriminator value in the example + if (!valueDict.TryGetValue(discriminatorPropertyName, out var exampleValue) || exampleValue is not InputExampleRawValue exampleRawValue || exampleRawValue.RawValue == null) + { + throw new InvalidOperationException($"Attempting to get the discriminator value for property `{discriminatorPropertyName}` on object type {objectType.Type.Name} but got none or non-primitive type"); + } + // the discriminator should always be a primitive type + var actualDiscriminatorValue = exampleRawValue.RawValue; + var implementation = discriminator.Implementations.FirstOrDefault(info => info.Key.Equals(actualDiscriminatorValue)); + if (implementation == null) + throw new InvalidOperationException($"Cannot find an implementation corresponding to the discriminator value {actualDiscriminatorValue} for object model type {objectType.Type.Name}"); + + return (ObjectType)implementation.Type.Implementation; + } + + private static ValueExpression GetExpressionForObjectType(ObjectType objectType, IReadOnlyDictionary? valueDict) + { + if (valueDict == null) + return Default; + + // need to get the actual ObjectType if this type has a discrinimator + objectType = GetActualImplementation(objectType, valueDict); + // get all the properties on this type, including the properties from its base type + var properties = new HashSet(objectType.EnumerateHierarchy().SelectMany(objectType => objectType.Properties)); + var constructor = objectType.InitializationConstructor; + // build a map from parameter name to property + // before the ToDictionary, we use GroupBy to group the properties by their name first because there might be cases that we define the same property in both this model and its base model + // by taking the first in the group, we are taking the property defined the lower level of the inheritance tree aka from the derived model + var propertyDict = properties.GroupBy(property => property.Declaration.Name) + .ToDictionary( + group => group.Key.ToVariableName(), group => group.First()); + // find the corresponding properties in the parameters + var arguments = new List(); + foreach (var parameter in constructor.Signature.Parameters) + { + // try every property, convert them to variable name and see if there are some of them matching + var property = propertyDict[parameter.Name]; + var propertyType = property.Declaration.Type; + ValueExpression argument; + if (valueDict.TryGetValue(property.InputModelProperty!.SerializedName, out var exampleValue)) + { + properties.Remove(property); + argument = GetExpression(propertyType, exampleValue, property.SerializationFormat, includeCollectionInitialization: true); + } + else + { + // if no match, we put default here + argument = propertyType.IsValueType && !propertyType.IsNullable ? Default : Null; + } + arguments.Add(argument); + } + var propertiesToWrite = GetPropertiesToWrite(properties, valueDict); + ObjectInitializerExpression? objectPropertyInitializer = null; + if (propertiesToWrite.Count > 0) // only write the property initializers when there are properties to write + { + var initializerDict = new Dictionary(); + foreach (var (property, exampleValue) in propertiesToWrite) + { + // we need to pass in the current type of this property to make sure its initialization is correct + var propertyExpression = GetExpression(property.Declaration.Type, exampleValue, property.SerializationFormat, includeCollectionInitialization: false); + initializerDict.Add(property.Declaration.Name, propertyExpression); + } + objectPropertyInitializer = new(initializerDict, false); + } + + return new NewInstanceExpression(objectType.Type, arguments, objectPropertyInitializer); + } + + private static IReadOnlyDictionary GetPropertiesToWrite(IEnumerable properties, IReadOnlyDictionary valueDict) + { + var propertiesToWrite = new Dictionary(); + foreach (var property in properties) + { + var propertyToDeal = property; + var inputModelProperty = propertyToDeal.InputModelProperty; + if (inputModelProperty == null) + continue; // now we explicitly ignore all the AdditionalProperties + + if (!valueDict.TryGetValue(inputModelProperty.SerializedName, out var exampleValue)) + continue; // skip the property that does not have a value + + // check if this property is safe-flattened + var flattenedProperty = propertyToDeal.FlattenedProperty; + if (flattenedProperty != null) + { + // unwrap the single property object + exampleValue = UnwrapExampleValueFromSinglePropertySchema(exampleValue, flattenedProperty); + if (exampleValue == null) + continue; + propertyToDeal = flattenedProperty; + } + + if (!IsPropertyAssignable(propertyToDeal)) + continue; // now we explicitly ignore all the AdditionalProperties + + propertiesToWrite.Add(propertyToDeal, exampleValue); + } + + return propertiesToWrite; + } + + private static InputExampleValue? UnwrapExampleValueFromSinglePropertySchema(InputExampleValue exampleValue, FlattenedObjectTypeProperty flattenedProperty) + { + var hierarchyStack = flattenedProperty.BuildHierarchyStack(); + // reverse the stack because it is a stack, iterating it will start from the innerest property + // skip the first because this stack include the property we are handling here right now + foreach (var property in hierarchyStack.Reverse().Skip(1)) + { + var schemaProperty = property.InputModelProperty; + if (schemaProperty == null || exampleValue is not InputExampleObjectValue objectValue || !objectValue.Values.TryGetValue(schemaProperty.SerializedName, out var inner)) + return null; + // get the value of this layer + exampleValue = inner; + } + return exampleValue; + } + + private static bool IsPropertyAssignable(ObjectTypeProperty property) + => property.Declaration.Accessibility == "public" && (property.Declaration.Type.IsReadWriteDictionary || property.Declaration.Type.IsReadWriteList || !property.IsReadOnly); + + private static ValueExpression GetExpressionForEnumType(EnumType enumType, object value) + { + // find value in one of the choices. + // Here we convert the values to string then compare, because the raw value has the "primitive types are deserialized into strings" issue + var choice = enumType.Values.FirstOrDefault(c => StringComparer.Ordinal.Equals(value.ToString(), c.Value.Value?.ToString())); + if (choice != null) + return EnumValue(enumType, choice); + // if we did not find a match, check if this is a SealedChoice, if so, we throw exceptions + if (!enumType.IsExtensible) + throw new InvalidOperationException($"Enum value `{value}` in example does not find in type {enumType.Type.Name}"); + return New.Instance(enumType.Type, Literal(value)); + } + } +} diff --git a/logger/autorest.csharp/level/Generation/DpgClientWriter.cs b/logger/autorest.csharp/level/Generation/DpgClientWriter.cs new file mode 100644 index 0000000..f234f5a --- /dev/null +++ b/logger/autorest.csharp/level/Generation/DpgClientWriter.cs @@ -0,0 +1,714 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using AutoRest.CSharp.Common.Generation.Writers; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Responses; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; +using Operation = Azure.Operation; +using StatusCodes = AutoRest.CSharp.Output.Models.Responses.StatusCodes; + +namespace AutoRest.CSharp.Generation.Writers +{ + internal class DpgClientWriter : ClientWriter + { + private static readonly FormattableString LroProcessMessageMethodName = $"{typeof(ProtocolOperationHelpers)}.{nameof(ProtocolOperationHelpers.ProcessMessage)}"; + private static readonly FormattableString LroProcessMessageMethodAsyncName = $"{typeof(ProtocolOperationHelpers)}.{nameof(ProtocolOperationHelpers.ProcessMessageAsync)}"; + private static readonly FormattableString LroProcessMessageWithoutResponseValueMethodName = $"{typeof(ProtocolOperationHelpers)}.{nameof(ProtocolOperationHelpers.ProcessMessageWithoutResponseValue)}"; + private static readonly FormattableString LroProcessMessageWithoutResponseValueMethodAsyncName = $"{typeof(ProtocolOperationHelpers)}.{nameof(ProtocolOperationHelpers.ProcessMessageWithoutResponseValueAsync)}"; + + private readonly DpgOutputLibrary _library; + private readonly CodeWriter _writer; + private readonly XmlDocWriter _xmlDocWriter; + private readonly LowLevelClient _client; + + public DpgClientWriter(DpgOutputLibrary library, LowLevelClient client) + { + _writer = new CodeWriter(); + _library = library; + _xmlDocWriter = new XmlDocWriter($"Docs/{client.Type.Name}.xml"); + _client = client; + } + + public XmlDocWriter XmlDocWriter => _xmlDocWriter; + + public void WriteClient() + { + var clientType = _client.Type; + using (_writer.Namespace(clientType.Namespace)) + { + WriteDPGIdentificationComment(); + _writer.WriteXmlDocumentationSummary($"{_client.Description}"); + using (_writer.Scope($"{_client.Declaration.Accessibility} partial class {clientType:D}", scopeDeclarations: _client.Fields.ScopeDeclarations)) + { + WriteClientFields(); + WriteConstructors(); + + foreach (var clientMethod in _client.ClientMethods) + { + var longRunning = clientMethod.LongRunning; + var pagingInfo = clientMethod.PagingInfo; + + if (clientMethod.ConvenienceMethod is { } convenienceMethod) + { + var samples = clientMethod.Samples.Where(s => s.IsConvenienceSample); + WriteConvenienceMethodDocumentationWithExternalXmlDoc(convenienceMethod, true); + WriteConvenienceMethod(clientMethod, convenienceMethod, longRunning, pagingInfo, true); + WriteConvenienceMethodDocumentationWithExternalXmlDoc(convenienceMethod, false); + WriteConvenienceMethod(clientMethod, convenienceMethod, longRunning, pagingInfo, false); + } + + WriteProtocolMethodDocumentationWithExternalXmlDoc(clientMethod, true); + WriteProtocolMethod(_writer, clientMethod, _client.Fields, longRunning, pagingInfo, true); + WriteProtocolMethodDocumentationWithExternalXmlDoc(clientMethod, false); + WriteProtocolMethod(_writer, clientMethod, _client.Fields, longRunning, pagingInfo, false); + } + + foreach (var clientMethod in _client.CustomMethods()) + { + //TODO: Write example docs for custom methods + //WriteProtocolMethodDocumentationWithExternalXmlDoc(clientMethod, true); + //WriteProtocolMethodDocumentationWithExternalXmlDoc(clientMethod, false); + } + + WriteSubClientFactoryMethod(); + + foreach (var method in _client.RequestMethods) + { + WriteRequestCreationMethod(_writer, method, _client.Fields); + } + + // since the non-azure libraries do not have cancellationToken parameters on their convenience methods, we do not need to add the method to convert the cancellationToken to RequestContext + if (Configuration.IsBranded && _client.ClientMethods.Any(cm => cm.ConvenienceMethod is not null)) + { + WriteCancellationTokenToRequestContextMethod(); + } + WriteResponseClassifierMethod(_writer, _client.ResponseClassifierTypes); + WriteLongRunningResultRetrievalMethods(); + } + } + } + + private void WriteLongRunningResultRetrievalMethods() + { + foreach (var method in _client.ClientMethods.Select(c => c.LongRunningResultRetrievalMethod).WhereNotNull().Distinct()) + { + _writer.Line(); + WriteLroResultRetrievalMethod(method); + } + } + + public static void WriteProtocolMethods(CodeWriter writer, ClientFields fields, LowLevelClientMethod clientMethod) + { + WriteRequestCreationMethod(writer, clientMethod.RequestMethod, fields); + + var longRunning = clientMethod.LongRunning; + var pagingInfo = clientMethod.PagingInfo; + WriteProtocolMethodDocumentation(writer, clientMethod, true); + WriteProtocolMethod(writer, clientMethod, fields, longRunning, pagingInfo, true); + WriteProtocolMethodDocumentation(writer, clientMethod, false); + WriteProtocolMethod(writer, clientMethod, fields, longRunning, pagingInfo, false); + } + + private static void WriteProtocolMethod(CodeWriter writer, LowLevelClientMethod clientMethod, ClientFields fields, OperationLongRunning? longRunning, ProtocolMethodPaging? pagingInfo, bool async) + { + switch (longRunning, pagingInfo) + { + case { longRunning: not null, pagingInfo: not null }: + WriteProtocolPageableLroMethod(writer, clientMethod, fields, pagingInfo, longRunning, async); + break; + case { longRunning: null, pagingInfo: not null }: + WriteProtocolPageableMethod(writer, clientMethod, fields, pagingInfo, async); + break; + case { longRunning: not null, pagingInfo: null }: + WriteProtocolLroMethod(writer, clientMethod, fields, longRunning, async); + break; + default: + WriteProtocolMethod(writer, clientMethod, fields, async); + break; + } + } + + private void WriteConvenienceMethod(LowLevelClientMethod clientMethod, ConvenienceMethod convenienceMethod, OperationLongRunning? longRunning, ProtocolMethodPaging? pagingInfo, bool async) + { + switch (longRunning, pagingInfo) + { + case { longRunning: not null, pagingInfo: not null }: + // Not supported yet + break; + case { longRunning: null, pagingInfo: not null }: + WriteConveniencePageableMethod(clientMethod, convenienceMethod, pagingInfo, _client.Fields, async); + break; + default: + WriteConvenienceMethod(clientMethod, convenienceMethod, _client.Fields, async); + break; + } + } + + private void WriteDPGIdentificationComment() => _writer.Line($"// Data plane generated {(_client.IsSubClient ? "sub-client" : "client")}."); + + private void WriteClientFields() + { + foreach (var field in _client.Fields) + { + _writer.WriteField(field, declareInCurrentScope: false); + } + + //TODO: make this a field?? + _writer + .Line() + .WriteXmlDocumentationSummary($"The HTTP pipeline for sending and receiving REST requests and responses.") + .Line($"public virtual {Configuration.ApiTypes.HttpPipelineType} Pipeline => {_client.Fields.PipelineField.Name};"); + + _writer.Line(); + } + + private void WriteConstructors() + { + foreach (var constructor in _client.SecondaryConstructors) + { + WriteSecondaryPublicConstructor(constructor); + } + + foreach (var constructor in _client.PrimaryConstructors) + { + WritePrimaryPublicConstructor(constructor); + } + + if (_client.IsSubClient) + { + WriteSubClientInternalConstructor(_client.SubClientInternalConstructor); + } + } + + private void WriteSecondaryPublicConstructor(ConstructorSignature signature) + { + _writer.WriteMethodDocumentation(signature); + using (_writer.WriteMethodDeclaration(signature)) + { + } + _writer.Line(); + } + + private void WritePrimaryPublicConstructor(ConstructorSignature signature) + { + _writer.WriteMethodDocumentation(signature); + using (_writer.WriteMethodDeclaration(signature)) + { + _writer.WriteParametersValidation(signature.Parameters); + _writer.Line(); + + var clientOptionsParameter = signature.Parameters.Last(p => p.Type.EqualsIgnoreNullable(_client.ClientOptions.Type)); + if (Configuration.IsBranded) + { + _writer.Line($"{_client.Fields.ClientDiagnosticsProperty.Name:I} = new {_client.Fields.ClientDiagnosticsProperty.Type}({clientOptionsParameter.Name:I}, true);"); + } + + FormattableString perCallPolicies = $"{typeof(Array)}.{nameof(Array.Empty)}<{Configuration.ApiTypes.HttpPipelinePolicyType}>()"; + FormattableString perRetryPolicies = $"{typeof(Array)}.{nameof(Array.Empty)}<{Configuration.ApiTypes.HttpPipelinePolicyType}>()"; + + var credentialParameter = signature.Parameters.FirstOrDefault(p => p.Name == "credential"); + if (credentialParameter != null) + { + var credentialField = _client.Fields.GetFieldByParameter(credentialParameter); + if (credentialField != null) + { + var fieldName = credentialField.Name; + _writer.Line($"{fieldName:I} = {credentialParameter.Name:I};"); + if (credentialField.Type.Equals(Configuration.ApiTypes.KeyCredentialType)) + { + var ctor = Configuration.IsBranded ? $"new {Configuration.ApiTypes.KeyCredentialPolicyType}" : $"{Configuration.ApiTypes.KeyCredentialPolicyType}.CreateHeaderApiKeyPolicy"; + string prefixString = _client.Fields.AuthorizationApiKeyPrefixConstant != null ? $", {_client.Fields.AuthorizationApiKeyPrefixConstant.Name}" : ""; + perRetryPolicies = $"new {Configuration.ApiTypes.HttpPipelinePolicyType}[] {{{ctor}({fieldName:I}, {_client.Fields.AuthorizationHeaderConstant!.Name}{prefixString})}}"; + } + else if (credentialField.Type.Equals(typeof(TokenCredential))) + { + var ctor = Configuration.IsBranded ? $"new {Configuration.ApiTypes.BearerAuthenticationPolicyType}" : $"{Configuration.ApiTypes.BearerAuthenticationPolicyType}.CreateBearerAuthorizationPolicy"; + perRetryPolicies = $"new {Configuration.ApiTypes.HttpPipelinePolicyType}[] {{{ctor}({fieldName:I}, {_client.Fields.ScopesConstant!.Name})}}"; + } + } + } + + _writer.Line(Configuration.ApiTypes.GetHttpPipelineClassifierString(_client.Fields.PipelineField.Name, clientOptionsParameter.Name, perCallPolicies, perRetryPolicies, $"{typeof(Array)}.{nameof(Array.Empty)}<{Configuration.ApiTypes.HttpPipelinePolicyType}>()")); + + foreach (var parameter in _client.Parameters) + { + var field = _client.Fields.GetFieldByParameter(parameter); + if (field != null) + { + if (parameter.IsApiVersionParameter) + { + _writer.Line($"{field.Name:I} = {clientOptionsParameter.Name:I}.Version;"); + } + else if (_client.ClientOptions.AdditionalParameters.Contains(parameter)) + { + _writer.Line($"{field.Name:I} = {clientOptionsParameter.Name:I}.{parameter.Name.ToCleanName()};"); + } + else + { + _writer.Line($"{field.Name:I} = {parameter.Name:I};"); + } + } + } + } + _writer.Line(); + } + + private void WriteSubClientInternalConstructor(ConstructorSignature signature) + { + _writer.WriteMethodDocumentation(signature); + using (_writer.WriteMethodDeclaration(signature)) + { + _writer.WriteParametersValidation(signature.Parameters); + _writer.Line(); + + foreach (var parameter in signature.Parameters) + { + var field = _client.Fields.GetFieldByParameter(parameter); + if (field != null) + { + _writer.Line($"{field.Name:I} = {parameter.Name:I};"); + } + } + } + _writer.Line(); + } + + private void WriteConvenienceMethod(LowLevelClientMethod clientMethod, ConvenienceMethod convenienceMethod, ClientFields fields, bool async) + { + using (WriteConvenienceMethodDeclaration(_writer, convenienceMethod, fields, async)) + { + // This method now builds statements for normal operation and lro operations + MethodBodyStatement statement = convenienceMethod.GetConvertStatements(clientMethod, async, fields.ClientDiagnosticsProperty).ToArray(); + + statement.Write(_writer); + } + _writer.Line(); + } + + private void WriteLroResultRetrievalMethod(LongRunningResultRetrievalMethod method) + { + using (_writer.WriteMethodDeclaration(method.MethodSignature)) + { + _writer.Line($"var resultJsonElement = {typeof(JsonDocument)}.{nameof(JsonDocument.Parse)}(response.{nameof(Response.Content)}).{nameof(JsonDocument.RootElement)}.{nameof(JsonElement.GetProperty)}(\"{method.ResultPath}\");"); + _writer.Line($"return {method.ReturnType}.Deserialize{method.ReturnType.Name}(resultJsonElement);"); + } + } + + private void WriteConveniencePageableMethod(LowLevelClientMethod clientMethod, ConvenienceMethod convenienceMethod, ProtocolMethodPaging pagingInfo, ClientFields fields, bool async) + { + _writer.WritePageable(convenienceMethod, clientMethod.RequestMethod, pagingInfo.NextPageMethod, fields.ClientDiagnosticsProperty, fields.PipelineField, clientMethod.ProtocolMethodDiagnostic.ScopeName, pagingInfo.ItemName, pagingInfo.NextLinkName, async); + } + + private static void WriteProtocolPageableMethod(CodeWriter writer, LowLevelClientMethod clientMethod, ClientFields fields, ProtocolMethodPaging pagingInfo, bool async) + { + writer.WritePageable(clientMethod.ProtocolMethodSignature, typeof(BinaryData), null, clientMethod.RequestMethod, pagingInfo.NextPageMethod, fields.ClientDiagnosticsProperty, fields.PipelineField, clientMethod.ProtocolMethodDiagnostic.ScopeName, pagingInfo.ItemName, pagingInfo.NextLinkName, async); + } + + private static void WriteProtocolPageableLroMethod(CodeWriter writer, LowLevelClientMethod clientMethod, ClientFields fields, ProtocolMethodPaging pagingInfo, OperationLongRunning longRunning, bool async) + { + writer.WriteLongRunningPageable(clientMethod.ProtocolMethodSignature, typeof(BinaryData), null, clientMethod.RequestMethod, pagingInfo.NextPageMethod, fields.ClientDiagnosticsProperty, fields.PipelineField, clientMethod.ProtocolMethodDiagnostic, longRunning.FinalStateVia, pagingInfo.ItemName, pagingInfo.NextLinkName, async); + } + + private static void WriteProtocolLroMethod(CodeWriter writer, LowLevelClientMethod clientMethod, ClientFields fields, OperationLongRunning longRunning, bool async) + { + using (writer.WriteMethodDeclaration(clientMethod.ProtocolMethodSignature.WithAsync(async))) + { + writer.WriteParametersValidation(clientMethod.ProtocolMethodSignature.Parameters); + var startMethod = clientMethod.RequestMethod; + var finalStateVia = longRunning.FinalStateVia; + var scopeName = clientMethod.ProtocolMethodDiagnostic.ScopeName; + + using (writer.WriteDiagnosticScope(clientMethod.ProtocolMethodDiagnostic, fields.ClientDiagnosticsProperty)) + { + var messageVariable = new CodeWriterDeclaration("message"); + var processMessageParameters = (FormattableString)$"{fields.PipelineField.Name:I}, {messageVariable}, {fields.ClientDiagnosticsProperty.Name:I}, {scopeName:L}, {typeof(OperationFinalStateVia)}.{finalStateVia}, {KnownParameters.RequestContext.Name:I}, {KnownParameters.WaitForCompletion.Name:I}"; + + writer + .Line($"using {Configuration.ApiTypes.HttpMessageType} {messageVariable:D} = {RequestWriterHelpers.CreateRequestMethodName(startMethod.Name)}({startMethod.Parameters.GetIdentifiersFormattable()});") + .AppendRaw("return ") + .WriteMethodCall(async, clientMethod.ResponseBodyType != null ? LroProcessMessageMethodAsyncName : LroProcessMessageWithoutResponseValueMethodAsyncName, clientMethod.ResponseBodyType != null ? LroProcessMessageMethodName : LroProcessMessageWithoutResponseValueMethodName, processMessageParameters); + } + } + writer.Line(); + } + + public static void WriteProtocolMethod(CodeWriter writer, LowLevelClientMethod clientMethod, ClientFields fields, bool async) + { + using (writer.WriteMethodDeclaration(clientMethod.ProtocolMethodSignature.WithAsync(async))) + { + writer.WriteParametersValidation(clientMethod.ProtocolMethodSignature.Parameters); + var restMethod = clientMethod.RequestMethod; + var headAsBoolean = restMethod.Request.HttpMethod == RequestMethod.Head && Configuration.HeadAsBoolean; + + if (clientMethod.ConditionHeaderFlag != RequestConditionHeaders.None && clientMethod.ConditionHeaderFlag != (RequestConditionHeaders.IfMatch | RequestConditionHeaders.IfNoneMatch | RequestConditionHeaders.IfModifiedSince | RequestConditionHeaders.IfUnmodifiedSince)) + { + writer.WriteRequestConditionParameterChecks(restMethod.Parameters, clientMethod.ConditionHeaderFlag); + writer.Line(); + } + + if (Configuration.IsBranded) + { + using (writer.WriteDiagnosticScope(clientMethod.ProtocolMethodDiagnostic, fields.ClientDiagnosticsProperty)) + { + WriteStatements(writer, headAsBoolean, restMethod, fields, async); + } + } + else + { + WriteStatements(writer, headAsBoolean, restMethod, fields, async); + } + } + writer.Line(); + + static void WriteStatements(CodeWriter writer, bool headAsBoolean, RestClientMethod restMethod, ClientFields fields, bool async) + { + var createMessageSignature = new MethodSignature(RequestWriterHelpers.CreateRequestMethodName(restMethod), null, null, Internal, null, null, restMethod.Parameters); + if (headAsBoolean) + { + Extensible.RestOperations.DeclareHttpMessage(createMessageSignature, out var message).Write(writer); + Extensible.RestOperations.InvokeServiceOperationCallAndReturnHeadAsBool(fields.PipelineField, message, fields.ClientDiagnosticsProperty, async).Write(writer); + } + else + { + Extensible.RestOperations.DeclareHttpMessage(createMessageSignature, out var message).Write(writer); + writer.WriteEnableHttpRedirectIfNecessary(restMethod, message); + Return(Extensible.RestOperations.InvokeServiceOperationCall(fields.PipelineField, message, async)).Write(writer); + } + } + } + + private void WriteSubClientFactoryMethod() + { + foreach (var field in _client.SubClients.Select(s => s.FactoryMethod?.CachingField)) + { + if (field != null) + { + _writer.WriteField(field); + } + } + + _writer.Line(); + + foreach (var (methodSignature, field, constructorCallParameters) in _client.SubClients.Select(s => s.FactoryMethod).WhereNotNull()) + { + _writer.WriteMethodDocumentation(methodSignature); + using (_writer.WriteMethodDeclaration(methodSignature)) + { + _writer.WriteParametersValidation(methodSignature.Parameters); + _writer.Line(); + + var references = constructorCallParameters + .Select(p => _client.Fields.GetFieldByParameter(p) ?? (Reference)p) + .ToArray(); + + if (field != null) + { + _writer + .Append($"return {typeof(Volatile)}.{nameof(Volatile.Read)}(ref {field.Name})") + .Append($" ?? {typeof(Interlocked)}.{nameof(Interlocked.CompareExchange)}(ref {field.Name}, new {methodSignature.ReturnType}({references.GetIdentifiersFormattable()}), null)") + .Line($" ?? {field.Name};"); + } + else + { + _writer.Line($"return new {methodSignature.ReturnType}({references.GetIdentifiersFormattable()});"); + } + } + _writer.Line(); + } + } + + public static void WriteRequestCreationMethod(CodeWriter writer, RestClientMethod restMethod, ClientFields fields) + { + RequestWriterHelpers.WriteRequestCreation(writer, restMethod, fields, restMethod.ResponseClassifierType.Name, false); + } + + public static void WriteResponseClassifierMethod(CodeWriter writer, IEnumerable responseClassifierTypes) + { + foreach ((string name, StatusCodes[] statusCodes) in responseClassifierTypes.Distinct()) + { + WriteResponseClassifier(writer, name, statusCodes); + } + } + + private static void WriteResponseClassifier(CodeWriter writer, string responseClassifierTypeName, StatusCodes[] statusCodes) + { + var hasStatusCodeRanges = statusCodes.Any(statusCode => statusCode.Family != null); + if (hasStatusCodeRanges) + { + // After fixing https://github.com/Azure/autorest.csharp/issues/2018 issue remove "hasStatusCodeRanges" condition and this class + using (writer.Scope($"private sealed class {responseClassifierTypeName}Override : {Configuration.ApiTypes.ResponseClassifierType}")) + { + if (Configuration.IsBranded) + { + using (writer.Scope($"public override bool {Configuration.ApiTypes.ResponseClassifierIsErrorResponseName}({Configuration.ApiTypes.HttpMessageType} message)")) + { + using (writer.Scope($"return message.{Configuration.ApiTypes.HttpMessageResponseName}.{Configuration.ApiTypes.HttpMessageResponseStatusName} switch", end: "};")) + { + foreach (var statusCode in statusCodes) + { + writer.Line($">= {statusCode.Family * 100:L} and < {statusCode.Family * 100 + 100:L} => false,"); + } + + writer.LineRaw("_ => true"); + } + } + } + else + { + using (writer.Scope($"public override bool {Configuration.ApiTypes.ResponseClassifierIsErrorResponseName}({Configuration.ApiTypes.HttpMessageType} message, out bool isError)")) + { + writer.Line($"isError = false;"); + using (writer.Scope($"if (message.Response is null)")) + { + writer.Line($"return false;"); + } + using (writer.Scope($"isError = message.{Configuration.ApiTypes.HttpMessageResponseName}.{Configuration.ApiTypes.HttpMessageResponseStatusName} switch", end: "};")) + { + foreach (var statusCode in statusCodes) + { + writer.Line($">= {statusCode.Family * 100:L} and < {statusCode.Family * 100 + 100:L} => false,"); + } + + writer.LineRaw("_ => true"); + } + writer.Line($"return true;"); + } + writer.Line(); + using (writer.Scope($"public override bool {Configuration.ApiTypes.ResponseClassifierIsErrorResponseName}({Configuration.ApiTypes.HttpMessageType} message, {typeof(Exception)} exception, out bool isRetriable)")) + { + writer.Line($"isRetriable = false;"); + writer.Line($"return false;"); + } + } + } + writer.Line(); + } + + writer.Line($"private static {Configuration.ApiTypes.ResponseClassifierType} _{responseClassifierTypeName.FirstCharToLowerCase()};"); + if (hasStatusCodeRanges) + { + writer.Append($"private static {Configuration.ApiTypes.ResponseClassifierType} {responseClassifierTypeName} => _{responseClassifierTypeName.FirstCharToLowerCase()} ??= new "); + writer.Line($"{responseClassifierTypeName}Override();"); + } + else + { + if (Configuration.IsBranded) + { + writer.Append($"private static {Configuration.ApiTypes.ResponseClassifierType} {responseClassifierTypeName} => _{responseClassifierTypeName.FirstCharToLowerCase()} ??= new "); + writer.Append($"{Configuration.ApiTypes.StatusCodeClassifierType}("); + writeStatusCodes(writer, statusCodes); + } + else + { + writer.Append($"private static {Configuration.ApiTypes.ResponseClassifierType} {responseClassifierTypeName} => _{responseClassifierTypeName.FirstCharToLowerCase()} ??= "); + writer.Append($"{Configuration.ApiTypes.StatusCodeClassifierType}.Create("); + writeStatusCodes(writer, statusCodes); + } + } + + static void writeStatusCodes(CodeWriter writer, StatusCodes[] statusCodes) + { + writer.Append($"stackalloc ushort[]{{"); + foreach (var statusCode in statusCodes) + { + if (statusCode.Code != null) + { + writer.Append($"{statusCode.Code}, "); + } + } + writer.RemoveTrailingComma(); + writer.Line($"}});"); + } + } + + private void WriteProtocolMethodDocumentationWithExternalXmlDoc(LowLevelClientMethod clientMethod, bool isAsync) + { + var methodSignature = clientMethod.ProtocolMethodSignature.WithAsync(isAsync); + + WriteConvenienceMethodOmitReasonIfNecessary(clientMethod.ConvenienceMethodOmittingMessage); + + WriteMethodDocumentation(_writer, methodSignature, clientMethod, isAsync); + + WriteSampleRefsIfNecessary(methodSignature, isAsync); + } + + private void WriteConvenienceMethodOmitReasonIfNecessary(ConvenienceMethodOmittingMessage? message) + { + // TODO -- create wiki links to provide guidance here: https://github.com/Azure/autorest.csharp/issues/3624 + if (message == null) + return; + + _writer.Line($"// {message.Message}"); + } + + private void WriteConvenienceMethodDocumentationWithExternalXmlDoc(ConvenienceMethod convenienceMethod, bool isAsync) + { + var methodSignature = convenienceMethod.Signature.WithAsync(isAsync); + + _writer.WriteMethodDocumentation(methodSignature); + _writer.WriteXmlDocumentation("remarks", methodSignature.DescriptionText); + + WriteSampleRefsIfNecessary(methodSignature, isAsync); + } + + private void WriteSampleRefsIfNecessary(MethodSignature methodSignature, bool isAsync) + { + var sampleProvider = _library.GetSampleForClient(_client); + // do not write this part when there is no sample provider + if (sampleProvider == null) + return; + + var samples = sampleProvider.GetSampleInformation(methodSignature, isAsync).ToArray(); + // do not write this part when there is no sample for this method + if (!samples.Any()) + { + return; + } + + _writer.WriteXmlDocumentationInclude(XmlDocWriter.Filename, methodSignature, out var memberId); + _xmlDocWriter.AddMember(memberId); + _xmlDocWriter.AddExamples(samples); + } + + private static void WriteProtocolMethodDocumentation(CodeWriter writer, LowLevelClientMethod clientMethod, bool isAsync) + { + var methodSignature = clientMethod.ProtocolMethodSignature.WithAsync(isAsync); + WriteMethodDocumentation(writer, methodSignature, clientMethod, isAsync); + } + + private static IDisposable WriteConvenienceMethodDeclaration(CodeWriter writer, ConvenienceMethod convenienceMethod, ClientFields fields, bool async) + { + var methodSignature = convenienceMethod.Signature.WithAsync(async); + var scope = writer.WriteMethodDeclaration(methodSignature); + writer.WriteParametersValidation(methodSignature.Parameters); + + if (convenienceMethod.Diagnostic != null) + { + var diagnosticScope = writer.WriteDiagnosticScope(convenienceMethod.Diagnostic, fields.ClientDiagnosticsProperty); + return Disposable.Create(() => + { + diagnosticScope.Dispose(); + scope.Dispose(); + }); + } + + return scope; + } + + private void WriteCancellationTokenToRequestContextMethod() + { + var defaultRequestContext = new CodeWriterDeclaration("DefaultRequestContext"); + _writer.Line($"private static {Configuration.ApiTypes.RequestContextType} {defaultRequestContext:D} = new {Configuration.ApiTypes.RequestContextType}();"); + + var methodSignature = new MethodSignature("FromCancellationToken", null, null, Internal | Static, Configuration.ApiTypes.RequestContextType, null, new List { KnownParameters.CancellationTokenParameter }); + using (_writer.WriteMethodDeclaration(methodSignature)) + { + using (_writer.Scope($"if (!{KnownParameters.CancellationTokenParameter.Name}.{nameof(CancellationToken.CanBeCanceled)})")) + { + _writer.Line($"return {defaultRequestContext:I};"); + } + + _writer.Line().Line($"return new {Configuration.ApiTypes.RequestContextType}() {{ CancellationToken = {KnownParameters.CancellationTokenParameter.Name} }};"); + } + _writer.Line(); + } + + private static FormattableString BuildProtocolMethodSummary(MethodSignature methodSignature, LowLevelClientMethod clientMethod, bool async) + { + var linkForProtocol = Configuration.IsBranded + ? "https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/ProtocolMethods.md" + : "https://aka.ms/azsdk/net/protocol-methods"; + List lines = new() + { + $"[Protocol Method] {methodSignature.SummaryText}", + $"", + $"", + $"", + $"This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.", + $"", + $"" + }; + // we only append the relative convenience method information when the convenience method is public + if (clientMethod.ShouldGenerateConvenienceMethodRef()) + { + var convenienceDocRef = clientMethod.ConvenienceMethod!.Signature.WithAsync(async).GetCRef(); + lines.AddRange(new FormattableString[] + { + $"", + $"", + $"Please try the simpler {convenienceDocRef:C} convenience overload with strongly typed models first.", + $"", + $"" + }); + } + lines.Add($""); + return lines.Join(Environment.NewLine); + } + + private static void WriteMethodDocumentation(CodeWriter codeWriter, MethodSignature methodSignature, LowLevelClientMethod clientMethod, bool async) + { + codeWriter.WriteMethodDocumentation(methodSignature, BuildProtocolMethodSummary(methodSignature, clientMethod, async)); + codeWriter.WriteXmlDocumentationException(Configuration.ApiTypes.RequestFailedExceptionType, $"Service returned a non-success status code."); + + if (methodSignature.ReturnType == null) + { + return; + } + + if (!methodSignature.ReturnType.IsFrameworkType) + { + throw new InvalidOperationException($"Xml documentation generation is supported only for protocol methods. {methodSignature.ReturnType} can't be a return type of a protocol method."); + } + + var returnType = methodSignature.ReturnType; + + FormattableString text; + if (clientMethod.PagingInfo != null && clientMethod.LongRunning != null) + { + CSharpType pageableType = methodSignature.Modifiers.HasFlag(Async) ? typeof(AsyncPageable<>) : typeof(Pageable<>); + text = $"The {typeof(Operation<>):C} from the service that will contain a {pageableType:C} containing a list of {typeof(BinaryData):C} objects once the asynchronous operation on the service has completed. Details of the body schema for the operation's final value are in the Remarks section below."; + } + else if (clientMethod.PagingInfo != null) + { + text = $"The {returnType.GetGenericTypeDefinition():C} from the service containing a list of {returnType.Arguments[0]:C} objects. Details of the body schema for each item in the collection are in the Remarks section below."; + } + else if (clientMethod.LongRunning != null) + { + text = $"The {typeof(Operation):C} representing an asynchronous operation on the service."; + } + else if (returnType.EqualsIgnoreNullable(Configuration.ApiTypes.GetTaskOfResponse()) || returnType.EqualsIgnoreNullable(Configuration.ApiTypes.ResponseType)) + { + text = $"The response returned from the service."; + } + else if (returnType.EqualsIgnoreNullable(Configuration.ApiTypes.GetTaskOfResponse(typeof(bool))) || returnType.EqualsIgnoreNullable(Configuration.ApiTypes.GetResponseOfT())) + { + text = $"The response returned from the service."; + } + else + { + throw new InvalidOperationException($"Xml documentation generation for return type {methodSignature.ReturnType} is not supported!"); + } + + codeWriter.WriteXmlDocumentationReturns(text); + } + + public override string ToString() => _writer.ToString(); + } +} diff --git a/logger/autorest.csharp/level/Output/ConvenienceMethod.cs b/logger/autorest.csharp/level/Output/ConvenienceMethod.cs new file mode 100644 index 0000000..4ef9f1d --- /dev/null +++ b/logger/autorest.csharp/level/Output/ConvenienceMethod.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Common.Output.Models.Types.HelperTypeProviders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Models +{ + internal record ConvenienceMethod(MethodSignature Signature, IReadOnlyList ProtocolToConvenienceParameterConverters, CSharpType? ResponseType, IReadOnlyList? RequestMediaTypes, IReadOnlyList? ResponseMediaTypes, Diagnostic? Diagnostic, bool IsPageable, bool IsLongRunning, string? Deprecated) + { + private record ConvenienceParameterInfo(Parameter? Convenience, ConvenienceParameterSpread? ConvenienceSpread); + + public IEnumerable GetConvertStatements(LowLevelClientMethod clientMethod, bool async, FieldDeclaration clientDiagnostics) + { + var protocolMethod = clientMethod.ProtocolMethodSignature; + var parametersDict = ProtocolToConvenienceParameterConverters.ToDictionary(converter => converter.Protocol.Name, converter => new ConvenienceParameterInfo(converter.Convenience, converter.ConvenienceSpread)); + var protocolInvocationExpressions = new List(); + VariableReference? content = null; + bool isMultipartOperation = RequestMediaTypes != null && RequestMediaTypes.Count == 1 && RequestMediaTypes.Contains("multipart/form-data"); + foreach (var protocol in protocolMethod.Parameters) + { + var (convenience, convenienceSpread) = parametersDict[protocol.Name]; + if (protocol == KnownParameters.RequestContext || protocol == KnownParameters.RequestContextRequired) + { + // convert cancellationToken to request context + // when the protocol needs a request context, we should either convert the cancellationToken to request context, or pass null if required, or do nothing if optional. + if (convenience != null) + { + // meaning we have a cancellationToken, we need to convert it + var context = new VariableReference(protocol.Type, protocol.Name); + // write: RequestContext context = FromCancellationToken(cancellationToken); + yield return Declare(context, new InvokeStaticMethodExpression(null, "FromCancellationToken", new ValueExpression[] { convenience })); + protocolInvocationExpressions.Add(context); + } + else + { + // if convenience parameter is null here, we just use the default value of the protocol parameter as value (we always do this even if the parameter is optional just in case there is an ordering issue) + protocolInvocationExpressions.Add(DefaultOf(protocol.Type)); + } + } + else if (protocol == KnownParameters.RequestContent || protocol == KnownParameters.RequestContentNullable) + { + Debug.Assert(convenience is not null); + // convert body to request content + if (convenienceSpread == null) + { + content = new VariableReference(isMultipartOperation ? MultipartFormDataRequestContentProvider.Instance.Type : protocol.Type, protocol.Name); + yield return UsingDeclare(content, convenience.GetConversionToProtocol(protocol.Type, RequestMediaTypes?.FirstOrDefault())); + protocolInvocationExpressions.Add(content); + } + else + { + // write some statements to put the spread back into a local variable + yield return GetSpreadConversion(convenienceSpread, convenience, out var spreadExpression); + if (isMultipartOperation) + { + content = new VariableReference(MultipartFormDataRequestContentProvider.Instance.Type, protocol.Name); + yield return UsingDeclare(content, spreadExpression.ToMultipartRequestContent()); + protocolInvocationExpressions.Add(content); + } + else + { + protocolInvocationExpressions.Add(spreadExpression.ToRequestContent()); + } + } + } + else + { + // process any other parameter + if (convenience != null) + { + // if convenience parameter is not null here, we convert it to protocol parameter value properly + var expression = convenience.GetConversionToProtocol(protocol.Type, RequestMediaTypes?.FirstOrDefault()); + protocolInvocationExpressions.Add(expression); + } + else + { + // if convenience parameter is null here, we just use the default value of the protocol parameter as value (we always do this even if the parameter is optional just in case there is an ordering issue) + if (isMultipartOperation && protocol.Name == "contentType" && content != null) + { + protocolInvocationExpressions.Add(MultipartFormDataRequestContentProvider.Instance.ContentTypeProperty(content)); + } + else + { + protocolInvocationExpressions.Add(DefaultOf(protocol.Type)); + } + } + } + } + + // call the protocol method using the expressions we have for the parameters in protocol method + var invocation = new InvokeInstanceMethodExpression(null, protocolMethod.WithAsync(async), protocolInvocationExpressions); + + // handles the response + // TODO -- currently we only need to build response handling for long running method and normal method + if (IsPageable) + throw new NotImplementedException("Statements for handling pageable convenience method has not been implemented yet"); + + if (IsLongRunning) + { + if (ResponseType == null) + { + // return [await] protocolMethod(parameters...)[.ConfigureAwait(false)]; + yield return Return(invocation); + } + else + { + // Operation response = [await] protocolMethod(parameters...)[.ConfigureAwait(false)]; + var response = new VariableReference(protocolMethod.ReturnType!, Configuration.ApiTypes.ResponseParameterName); + yield return Declare(response, invocation); + // return ProtocolOperationHelpers.Convert(response, r => responseType.FromResponse(r), ClientDiagnostics, scopeName); + var diagnostic = Diagnostic ?? clientMethod.ProtocolMethodDiagnostic; + yield return Return(new InvokeStaticMethodExpression( + typeof(ProtocolOperationHelpers), + nameof(ProtocolOperationHelpers.Convert), + new ValueExpression[] + { + response, + GetLongRunningConversionMethod(clientMethod.LongRunningResultRetrievalMethod, ResponseType), + clientDiagnostics, + Literal(diagnostic.ScopeName) + })); + } + } + else + { + var response = new VariableReference(protocolMethod.ReturnType!, Configuration.ApiTypes.ResponseParameterName); + yield return Declare(response, invocation); + // handle normal responses + if (ResponseType is null) + { + yield return Return(response); + } + else if (ResponseType is { IsFrameworkType: false, Implementation: SerializableObjectType { Serialization.Json: { } } serializableObjectType }) + { + yield return Return(Extensible.RestOperations.GetTypedResponseFromModel(serializableObjectType, response)); + } + else if (ResponseType is { IsFrameworkType: false, Implementation: EnumType enumType }) + { + yield return Return(Extensible.RestOperations.GetTypedResponseFromEnum(enumType, response)); + } + else if (ResponseType.IsCollection) + { + Debug.Assert(clientMethod.ResponseBodyType is not null); + var serializationFormat = SerializationBuilder.GetSerializationFormat(clientMethod.ResponseBodyType, ResponseType); + var serialization = SerializationBuilder.BuildJsonSerialization(clientMethod.ResponseBodyType, ResponseType, false, serializationFormat); + var value = new VariableReference(ResponseType, "value"); + + yield return new[] + { + Declare(value, Default), + JsonSerializationMethodsBuilder.BuildDeserializationForMethods(serialization, async, value, Extensible.RestOperations.GetContentStream(response), false, null), + Return(Extensible.RestOperations.GetTypedResponseFromValue(value, response)) + }; + } + else if (ResponseType is { IsFrameworkType: true }) + { + yield return Return(Extensible.RestOperations.GetTypedResponseFromBinaryData(ResponseType.FrameworkType, response, ResponseMediaTypes?.FirstOrDefault())); + } + } + } + + private ValueExpression GetLongRunningConversionMethod(LongRunningResultRetrievalMethod? convertMethod, CSharpType responseType) + { + if (convertMethod is null) + { + return new FormattableStringToExpression($"{responseType}.FromResponse"); + } + return new FormattableStringToExpression($"{convertMethod.MethodSignature.Name}"); + } + + private MethodBodyStatement GetSpreadConversion(ConvenienceParameterSpread convenienceSpread, Parameter convenience, out SerializableObjectTypeExpression spread) + { + var backingModel = convenienceSpread.BackingModel; + var spreadVar = new VariableReference(convenienceSpread.BackingModel.Type, convenience.Name); + var arguments = new List(); + var ctorSignature = backingModel.SerializationConstructor.Signature; + var parametersDict = convenienceSpread.SpreadedParameters.ToDictionary(p => p.Name); + foreach (var ctorParameter in ctorSignature.Parameters) + { + if (parametersDict.TryGetValue(ctorParameter.Name, out var parameter)) + { + // we have a value in the spread parameter list for a ctor parameter + ValueExpression expression = new ParameterReference(parameter); + if (parameter.Type.IsList) + { + expression = expression.GetConversion(parameter.Type, ctorParameter.Type); + if (parameter.Validation is not ValidationType.AssertNotNull or ValidationType.AssertNotNullOrEmpty) + { + // we have to add a cast otherwise this will not compile + // this could compile: a?.ToList() as IList ?? new ChangeTrackingList() + // this will not compile: a?.ToList() ?? new ChangeTrackingList() + expression = NullCoalescing(new AsExpression(expression, ctorParameter.Type), New.Instance(parameter.Type.PropertyInitializationType)); + } + } + else if (parameter.Type.IsDictionary) + { + if (parameter.Validation is not ValidationType.AssertNotNull or ValidationType.AssertNotNullOrEmpty) + { + expression = NullCoalescing(expression, New.Instance(parameter.Type.PropertyInitializationType)); + } + } + + // add the argument into the list + arguments.Add(expression); + } + else + { + // we do not have this ctor parameter in the spread parameter list, this should only happen to the raw data field parameter + arguments.Add(ctorParameter.Type.IsValueType ? Default.CastTo(ctorParameter.Type) : Null.CastTo(ctorParameter.Type)); + } + } + + spread = new SerializableObjectTypeExpression(backingModel, spreadVar); + return Declare(spreadVar, New.Instance(ctorSignature, arguments)); + } + } + + internal record ProtocolToConvenienceParameterConverter(Parameter Protocol, Parameter? Convenience, ConvenienceParameterSpread? ConvenienceSpread); + + internal record ConvenienceParameterSpread(ModelTypeProvider BackingModel, IReadOnlyList SpreadedParameters); +} diff --git a/logger/autorest.csharp/level/Output/ConvenienceMethodOmittingMessage.cs b/logger/autorest.csharp/level/Output/ConvenienceMethodOmittingMessage.cs new file mode 100644 index 0000000..e3ac298 --- /dev/null +++ b/logger/autorest.csharp/level/Output/ConvenienceMethodOmittingMessage.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Output.Models +{ + internal record ConvenienceMethodOmittingMessage + { + private ConvenienceMethodOmittingMessage(string message) + { + Message = message; + } + + public string Message { get; } + + public static ConvenienceMethodOmittingMessage NotMeaningful = new("The convenience method is omitted here because it has exactly the same parameter list as the corresponding protocol method"); + } +} diff --git a/logger/autorest.csharp/level/Output/DpgClientTestProvider.cs b/logger/autorest.csharp/level/Output/DpgClientTestProvider.cs new file mode 100644 index 0000000..6293624 --- /dev/null +++ b/logger/autorest.csharp/level/Output/DpgClientTestProvider.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Output.Samples.Models; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.LowLevel.Output +{ + internal abstract class DpgClientTestProvider : ExpressionTypeProvider + { + public DpgClientTestProvider(string defaultNamespace, string defaultName, LowLevelClient client, SourceInputModel? sourceInputModel) : base(defaultNamespace, sourceInputModel) + { + DeclarationModifiers = TypeSignatureModifiers.Public | TypeSignatureModifiers.Partial; + DefaultName = defaultName; + _client = client; + } + + protected readonly LowLevelClient _client; + + public bool IsEmpty => !Methods.Any(); + + protected override string DefaultName { get; } + + protected abstract CSharpAttribute[] GetMethodAttributes(); + + protected abstract string GetMethodName(DpgOperationSample sample, bool isAsync); + + protected MethodSignature GetMethodSignature(DpgOperationSample sample, bool isAsync) => new( + Name: GetMethodName(sample, isAsync), + Summary: null, + Description: null, + Modifiers: isAsync ? MethodSignatureModifiers.Public | MethodSignatureModifiers.Async : MethodSignatureModifiers.Public, + ReturnType: isAsync ? typeof(Task) : (CSharpType?)null, + ReturnDescription: null, + Parameters: Array.Empty(), + Attributes: GetMethodAttributes()); + + protected Method BuildSampleMethod(DpgOperationSample sample, bool isAsync) + { + var signature = GetMethodSignature(sample, isAsync); + var clientVariableStatements = new List(); + var newClientStatement = BuildGetClientStatement(sample, sample.ClientInvocationChain, clientVariableStatements, out var clientVar); + + // the method invocation statements go here + var operationVariableStatements = new List(); + var operationInvocationStatements = BuildSampleOperationInvocation(sample, clientVar, operationVariableStatements, isAsync).ToArray(); + + return new Method(signature, new MethodBodyStatement[] + { + clientVariableStatements, + newClientStatement, + EmptyLine, + operationVariableStatements, + operationInvocationStatements + }); + } + + protected virtual MethodBodyStatement BuildGetClientStatement(DpgOperationSample sample, IReadOnlyList methodsToCall, List variableDeclarations, out VariableReference clientVar) + { + var first = methodsToCall[0]; + ValueExpression valueExpression = first switch + { + ConstructorSignature ctor => New.Instance(ctor.Type, sample.GetValueExpressionsForParameters(ctor.Parameters, variableDeclarations).ToArray()), + _ => new InvokeInstanceMethodExpression(null, first.Name, sample.GetValueExpressionsForParameters(first.Parameters, variableDeclarations).ToArray(), null, false) + }; + + foreach (var method in methodsToCall.Skip(1)) + { + valueExpression = valueExpression.Invoke(method.Name, sample.GetValueExpressionsForParameters(method.Parameters, variableDeclarations).ToArray()); + } + + clientVar = new VariableReference(_client.Type, "client"); + + return Declare(clientVar, valueExpression); + } + + protected IEnumerable BuildSampleOperationInvocation(DpgOperationSample sample, ValueExpression clientVar, List variableDeclarations, bool isAsync) + { + var methodSignature = sample.OperationMethodSignature.WithAsync(isAsync); + var parameterExpressions = sample.GetValueExpressionsForParameters(methodSignature.Parameters, variableDeclarations); + ValueExpression invocation = clientVar.Invoke(methodSignature, parameterExpressions.ToArray(), addConfigureAwaitFalse: false); + var returnType = GetReturnType(methodSignature.ReturnType); + VariableReference resultVar = sample.IsLongRunning + ? new VariableReference(returnType, "operation") + : new VariableReference(returnType, "response"); + + if (sample.IsPageable) + { + // if it is pageable, we need to wrap the invocation inside a foreach statement + // but before the foreach, if this operation is long-running, we still need to call it, and pass the operation.Value into the foreach + if (sample.IsLongRunning) + { + /* + * This will generate code like: + * Operation operation = ; + * BinaryData responseData = operation.Value; + */ + yield return Declare(resultVar, invocation); + returnType = GetOperationValueType(returnType); + invocation = resultVar.Property("Value"); + resultVar = new VariableReference(returnType, "responseData"); + yield return Declare(resultVar, invocation); + } + /* + * This will generate code like: + * await foreach (ItemType item in ) + * {} + */ + var itemType = GetPageableItemType(returnType); + var foreachStatement = new ForeachStatement(itemType, "item", invocation, isAsync, out var itemVar) + { + BuildResponseStatements(sample, itemVar).ToArray() + }; + yield return foreachStatement; + } + else + { + // if it is not pageable, we just call the operation, declare a local variable and assign the result to it + /* + * This will generate code like: + * Operation operation = ; // when it is long-running + * Response response = ; // when it is not long-running + */ + yield return Declare(resultVar, invocation); + + // generate an extra line when it is long-running + /* + * This will generate code like: + * Operation operation = ; + * BinaryData responseData = operation.Value; + */ + if (sample.IsLongRunning && returnType.IsOperationOfT) + { + returnType = GetOperationValueType(returnType); + invocation = resultVar.Property("Value"); + resultVar = new VariableReference(returnType, "responseData"); + yield return Declare(resultVar, invocation); + } + + yield return EmptyLine; + + yield return BuildResponseStatements(sample, resultVar).ToArray(); + } + } + + protected abstract IEnumerable BuildResponseStatements(DpgOperationSample sample, VariableReference resultVar); + + protected static CSharpType GetOperationValueType(CSharpType? returnType) + { + if (returnType == null) + throw new InvalidOperationException("The return type of a client operation should never be null"); + + returnType = GetReturnType(returnType); + + Debug.Assert(returnType.IsOperationOfT); + + return returnType.Arguments.Single(); + } + + protected static CSharpType GetReturnType(CSharpType? returnType) + { + if (returnType == null) + throw new InvalidOperationException("The return type of a client operation should never be null"); + + if (returnType.IsFrameworkType && returnType.FrameworkType.Equals(typeof(Task<>))) + { + return returnType.Arguments.Single(); + } + + return returnType; + } + + protected static CSharpType GetPageableItemType(CSharpType? returnType) + { + if (returnType == null) + throw new InvalidOperationException("The return type of a client operation should never be null"); + + Debug.Assert(returnType.IsPageable || returnType.IsAsyncPageable); + + return returnType.Arguments.Single(); + } + } +} diff --git a/logger/autorest.csharp/level/Output/DpgOutputLibraryBuilder.cs b/logger/autorest.csharp/level/Output/DpgOutputLibraryBuilder.cs new file mode 100644 index 0000000..1e72cbf --- /dev/null +++ b/logger/autorest.csharp/level/Output/DpgOutputLibraryBuilder.cs @@ -0,0 +1,505 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; +using Configuration = AutoRest.CSharp.Common.Input.Configuration; + +namespace AutoRest.CSharp.Output.Models +{ + internal class DpgOutputLibraryBuilder + { + private const string MaxCountParameterName = "maxCount"; + + private readonly InputNamespace _rootNamespace; + private readonly SourceInputModel? _sourceInputModel; + private readonly string _libraryName; + + public DpgOutputLibraryBuilder(InputNamespace rootNamespace, SourceInputModel? sourceInputModel) + { + _rootNamespace = rootNamespace; + _sourceInputModel = sourceInputModel; + _libraryName = Configuration.LibraryName; + } + + public DpgOutputLibrary Build(bool isTspInput) + { + var inputClients = UpdateOperations().ToList(); + + var clientInfosByName = inputClients + .Select(og => CreateClientInfo(og, _sourceInputModel, _rootNamespace.Name)) + .ToDictionary(ci => ci.Name); + AssignParentClients(inputClients, clientInfosByName); + var topLevelClientInfos = SetHierarchy(clientInfosByName); + var parametersInClientOptions = new List(); + var clientOptions = CreateClientOptions(topLevelClientInfos, parametersInClientOptions); + + SetRequestsToClients(clientInfosByName.Values); + + var enums = new Dictionary(); + var models = new Dictionary(); + var clients = new List(); + + var library = new DpgOutputLibrary(_libraryName, enums, models, clients, clientOptions, isTspInput, _sourceInputModel); + + if (isTspInput) + { + CreateModels(_rootNamespace.Models, models, library.TypeFactory, _sourceInputModel); + CreateEnums(_rootNamespace.Enums, enums, models, library.TypeFactory, _sourceInputModel); + } + CreateClients(clients, topLevelClientInfos, library.TypeFactory, clientOptions, parametersInClientOptions); + + return library; + } + + public static void CreateEnums(IReadOnlyList inputEnums, IDictionary enums, IDictionary models, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + { + foreach (var inputEnum in inputEnums) + { + // [TODO]: Consolidate default namespace choice between HLC and DPG + var ns = ExtractNamespace(inputEnum.CrossLanguageDefinitionId); + enums.Add(inputEnum, new EnumType(inputEnum, TypeProvider.GetDefaultModelNamespace(ns, Configuration.Namespace), "public", typeFactory, sourceInputModel)); + } + } + + private static string? ExtractNamespace(string crossLanguageDefinitionId) + { + if (Configuration.Generation1ConvenienceClient) + { + var index = crossLanguageDefinitionId.LastIndexOf("."); + return index >= 0 ? crossLanguageDefinitionId[..index] : null; + } + + return null; + } + + public static void CreateModels(IReadOnlyList inputModels, IDictionary models, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + { + var defaultDerivedTypes = new Dictionary(); + foreach (var model in inputModels) + { + ModelTypeProvider? defaultDerivedType = GetDefaultDerivedType(models, typeFactory, model, defaultDerivedTypes, sourceInputModel); + // [TODO]: Consolidate default namespace choice between HLC and DPG + var ns = ExtractNamespace(model.CrossLanguageDefinitionId); + var typeProvider = new ModelTypeProvider(model, TypeProvider.GetDefaultModelNamespace(ns, Configuration.Namespace), sourceInputModel, typeFactory, defaultDerivedType); + models.Add(model, typeProvider); + } + } + + private static ModelTypeProvider? GetDefaultDerivedType(IDictionary models, TypeFactory typeFactory, InputModelType model, Dictionary defaultDerivedTypes, SourceInputModel? sourceInputModel) + { + //only want to create one instance of the default derived per polymorphic set + bool isBasePolyType = model.DiscriminatorProperty is not null; + bool isChildPolyType = model.DiscriminatorValue is not null; + if (!isBasePolyType && !isChildPolyType) + { + return null; + } + + var actualBase = model; + while (actualBase.BaseModel?.DiscriminatorProperty is not null) + { + actualBase = actualBase.BaseModel; + } + + //Since the unknown type is used for deserialization only we don't need to create if it is an input only model + // TODO -- remove this condition completely when remove the UseModelReaderWriter flag + if (!Configuration.UseModelReaderWriter && !actualBase.Usage.HasFlag(InputModelTypeUsage.Output)) + { + return null; + } + + string defaultDerivedName = $"Unknown{actualBase.Name}"; + if (!defaultDerivedTypes.TryGetValue(defaultDerivedName, out ModelTypeProvider? defaultDerivedType)) + { + //create the "Unknown" version + var unknownDerivedType = new InputModelType( + defaultDerivedName, + string.Empty, + "internal", + null, + $"Unknown version of {actualBase.Name}", + model.Usage.HasFlag(InputModelTypeUsage.Input) ? InputModelTypeUsage.Input | InputModelTypeUsage.Output : InputModelTypeUsage.Output, + Array.Empty(), + actualBase, + Array.Empty(), + "Unknown", //TODO: do we need to support extensible enum / int values? + null, + new Dictionary(), + null) + { + IsUnknownDiscriminatorModel = true + }; + defaultDerivedType = new ModelTypeProvider(unknownDerivedType, TypeProvider.GetDefaultModelNamespace(null, Configuration.Namespace), sourceInputModel, typeFactory, null); + defaultDerivedTypes.Add(defaultDerivedName, defaultDerivedType); + models.Add(unknownDerivedType, defaultDerivedType); + } + + return defaultDerivedType; + } + + private IEnumerable UpdateOperations() + { + var defaultName = _rootNamespace.Name.ReplaceLast("Client", ""); + // this map of old/new InputOperation is to update the lazy initialization of `Paging.NextLinkOperation` + var operationsMap = new Dictionary>(); + foreach (var client in _rootNamespace.Clients) + { + var clientName = client.Name.IsNullOrEmpty() ? defaultName : client.Name; + foreach (var operation in client.Operations) + { + operationsMap.CreateAndCacheResult(operation, () => UpdateOperation(operation, clientName, operationsMap)); + } + + yield return client with { Operations = client.Operations.Select(op => operationsMap[op]()).ToList() }; + } + } + + private static InputOperation UpdateOperation(InputOperation operation, string clientName, IReadOnlyDictionary> operationsMap) + { + var name = UpdateOperationName(operation, operation.ResourceName ?? clientName); + if (operation.Paging != null && !Configuration.DisablePaginationTopRenaming && !operation.Parameters.Any(p => p.Name.Equals(MaxCountParameterName, StringComparison.OrdinalIgnoreCase))) + { + operation = operation with + { + Parameters = UpdatePageableOperationParameters(operation.Parameters), + Paging = UpdateOperationPaging(operation.Paging, operationsMap), + }; + } + + return operation with + { + Name = name, + }; + } + + private static string UpdateOperationName(InputOperation operation, string clientName) + => operation.CleanName.RenameGetMethod(clientName).RenameListToGet(clientName); + + private static IReadOnlyList UpdatePageableOperationParameters(IReadOnlyList operationParameters) + { + var parameters = new List(operationParameters.Count); + foreach (var parameter in operationParameters) + { + if (parameter.Name.Equals("top", StringComparison.OrdinalIgnoreCase)) + { + parameters.Add(parameter with { Name = MaxCountParameterName }); + } + else + { + parameters.Add(parameter); + } + } + + return parameters; + } + + private static OperationPaging? UpdateOperationPaging(OperationPaging? sourcePaging, IReadOnlyDictionary> operationsMap) + { + if (sourcePaging == null) + { + return null; + } + + if (sourcePaging.NextLinkOperation != null && operationsMap.TryGetValue(sourcePaging.NextLinkOperation, out var nextLinkOperationRef)) + { + return sourcePaging with { NextLinkOperation = nextLinkOperationRef() }; + } + + return sourcePaging; + } + + private ClientOptionsTypeProvider CreateClientOptions(IReadOnlyList topLevelClientInfos, List parametersInClientOptions) + { + var clientName = topLevelClientInfos.Count == 1 + ? topLevelClientInfos[0].Name + : topLevelClientInfos.SingleOrDefault(c => string.IsNullOrEmpty(c.OperationGroupKey))?.Name; + + var clientOptionsName = $"{ClientBuilder.GetClientPrefix(clientName ?? _libraryName, _rootNamespace.Name)}ClientOptions"; + var description = clientName != null + ? (FormattableString)$"Client options for {clientName}." + : $"Client options for {_libraryName} library clients."; + + IReadOnlyList? apiVersions = _sourceInputModel?.GetServiceVersionOverrides() ?? _rootNamespace.ApiVersions; + return new ClientOptionsTypeProvider(apiVersions, clientOptionsName, Configuration.Namespace, description, _sourceInputModel) + { + AdditionalParameters = parametersInClientOptions + }; + } + + private static ClientInfo CreateClientInfo(InputClient ns, SourceInputModel? sourceInputModel, string rootNamespaceName) + { + var clientNamePrefix = ClientBuilder.GetClientPrefix(ns.Name, rootNamespaceName); + var clientNamespace = Configuration.Namespace; + var clientDescription = ns.Description; + var operations = ns.Operations; + var clientParameters = RestClientBuilder.GetParametersFromClient(ns).ToList(); + var resourceParameters = clientParameters.Where(cp => cp.IsResourceParameter).ToHashSet(); + var isSubClient = Configuration.SingleTopLevelClient && !string.IsNullOrEmpty(ns.Name) || resourceParameters.Any() || !string.IsNullOrEmpty(ns.Parent); + var clientName = isSubClient ? clientNamePrefix : clientNamePrefix + ClientBuilder.GetClientSuffix(); + + INamedTypeSymbol? existingType; + if (sourceInputModel == null || (existingType = sourceInputModel.FindForType(clientNamespace, clientName)) == null) + { + return new ClientInfo(ns.Name, clientName, clientNamespace, clientDescription, operations, ns.Parameters, resourceParameters); + } + + clientName = existingType.Name; + clientNamespace = existingType.ContainingNamespace.ToDisplayString(); + return new ClientInfo(ns.Name, clientName, clientNamespace, clientDescription, existingType, operations, ns.Parameters, resourceParameters); + } + + private IReadOnlyList SetHierarchy(IReadOnlyDictionary clientInfosByName) + { + if (_sourceInputModel != null) + { + foreach (var clientInfo in clientInfosByName.Values) + { + AssignParents(clientInfo, clientInfosByName, _sourceInputModel); + } + } + + var topLevelClients = clientInfosByName.Values.Where(c => c.Parent == null).ToList(); + if (!Configuration.SingleTopLevelClient || topLevelClients.Count == 1) + { + return topLevelClients; + } + + var topLevelClientInfo = topLevelClients.FirstOrDefault(c => string.IsNullOrEmpty(c.OperationGroupKey)); + if (topLevelClientInfo == null) + { + var clientName = ClientBuilder.GetClientPrefix(_libraryName, _rootNamespace.Name) + ClientBuilder.GetClientSuffix(); + var clientNamespace = Configuration.Namespace; + var clientParameters = topLevelClients.SelectMany(c => c.ClientParameters.Concat(c.Operations.SelectMany(op => op.Parameters))) + .Where(p => (p.Kind == InputOperationParameterKind.Client) && (!p.IsRequired || p.IsApiVersion || p.IsEndpoint)) + .DistinctBy(p => p.Name) + .ToArray(); + + topLevelClientInfo = new ClientInfo(clientName, clientNamespace, clientParameters); + } + + foreach (var clientInfo in topLevelClients) + { + if (clientInfo != topLevelClientInfo) + { + clientInfo.Parent = topLevelClientInfo; + topLevelClientInfo.Children.Add(clientInfo); + } + } + + return new[] { topLevelClientInfo }; + } + + // assign parent according to the information in the input Model + private static void AssignParentClients(in IEnumerable clients, IReadOnlyDictionary clientInfosByName) + { + foreach (var client in clients) + { + if (!String.IsNullOrEmpty(client.Parent)) + { + ClientInfo? targetClient = null; + ClientInfo? targetParent = null; + foreach (var info in clientInfosByName.Values) + { + if (info.OperationGroupKey == client.Name) + targetClient = info; + if (info.OperationGroupKey == client.Parent) + targetParent = info; + } + if (targetClient != null && targetParent != null) + { + targetClient.Parent = targetParent; + targetParent.Children.Add(targetClient); + } + } + } + } + + //Assign parent according to the customized inputModel + private static void AssignParents(in ClientInfo clientInfo, IReadOnlyDictionary clientInfosByName, SourceInputModel sourceInputModel) + { + var child = clientInfo; + while (child.Parent == null && child.ExistingType != null && sourceInputModel.TryGetClientSourceInput(child.ExistingType, out var clientSourceInput) && clientSourceInput.ParentClientType != null) + { + var parent = clientInfosByName[clientSourceInput.ParentClientType.Name]; + if (clientInfo == parent) + { + throw new InvalidOperationException("loop"); + } + + child.Parent = parent; + parent.Children.Add(child); + child = parent; + } + } + + private static void SetRequestsToClients(IEnumerable clientInfos) + { + foreach (var clientInfo in clientInfos) + { + foreach (var operation in clientInfo.Operations) + { + SetRequestToClient(clientInfo, operation); + } + } + } + + private static void SetRequestToClient(ClientInfo clientInfo, InputOperation operation) + { + switch (clientInfo.ResourceParameters.Count) + { + case 1: + var requestParameter = clientInfo.ResourceParameters.Single(); + if (operation.Parameters.Contains(requestParameter)) + { + break; + } + + while (clientInfo.Parent != null && clientInfo.ResourceParameters.Count != 0) + { + clientInfo = clientInfo.Parent; + } + break; + case > 1: + var requestParameters = operation.Parameters.ToHashSet(); + while (clientInfo.Parent != null && !clientInfo.ResourceParameters.IsSubsetOf(requestParameters)) + { + clientInfo = clientInfo.Parent; + } + + break; + } + + clientInfo.Requests.Add(operation); + } + + private void CreateClients(List allClients, IEnumerable topLevelClientInfos, TypeFactory typeFactory, ClientOptionsTypeProvider clientOptions, List parametersInClientOptions) + { + var topLevelClients = CreateClients(topLevelClientInfos, typeFactory, clientOptions, null, parametersInClientOptions); + + // Simple implementation of breadth first traversal + allClients.AddRange(topLevelClients); + for (int i = 0; i < allClients.Count; i++) + { + allClients.AddRange(allClients[i].SubClients); + } + } + + private IEnumerable CreateClients(IEnumerable clientInfos, TypeFactory typeFactory, ClientOptionsTypeProvider clientOptions, LowLevelClient? parentClient, List parametersInClientOptions) + { + foreach (var clientInfo in clientInfos) + { + var description = string.IsNullOrWhiteSpace(clientInfo.Description) + ? $"The {ClientBuilder.GetClientPrefix(clientInfo.Name, _rootNamespace.Name)} {(parentClient == null ? "service client" : "sub-client")}." + : BuilderHelpers.EscapeXmlDocDescription(clientInfo.Description); + + var subClients = new List(); + + if (!Configuration.IsBranded) + { + for (int i = 0; i < clientInfo.Requests.Count; i++) + { + clientInfo.Requests[i] = InputOperation.RemoveApiVersionParam(clientInfo.Requests[i]); + } + } + + var client = new LowLevelClient( + clientInfo.Name, + clientInfo.Namespace, + description, + _libraryName, + parentClient, + clientInfo.Requests, + clientInfo.ClientParameters, + _rootNamespace.Auth, + _sourceInputModel, + clientOptions, + typeFactory) + { + SubClients = subClients + }; + + subClients.AddRange(CreateClients(clientInfo.Children, typeFactory, clientOptions, client, parametersInClientOptions)); + // parametersInClientOptions is assigned to ClientOptionsTypeProvider.AdditionalProperties before, which makes sure AdditionalProperties is readonly and won't change after ClientOptionsTypeProvider is built. + parametersInClientOptions.AddRange(client.GetOptionalParametersInOptions()); + + yield return client; + } + } + + private class ClientInfo + { + public string OperationGroupKey { get; } + public string Name { get; } + public string Namespace { get; } + public string Description { get; } + public INamedTypeSymbol? ExistingType { get; } + public IReadOnlyList Operations { get; } + + private IReadOnlyList? _clientParameters; + private IReadOnlyList _initClientParameters; + public IReadOnlyList ClientParameters => _clientParameters ??= EnsureClientParameters(); + + private IReadOnlyList EnsureClientParameters() + { + if (_initClientParameters.Count == 0) + { + var endpointParameter = this.Children.SelectMany(c => c.ClientParameters).FirstOrDefault(p => p.IsEndpoint); + return endpointParameter != null ? new[] { endpointParameter } : Array.Empty(); + } + return _initClientParameters; + } + + public ISet ResourceParameters { get; } + + public ClientInfo? Parent { get; set; } + public IList Children { get; } + public IList Requests { get; } + + public ClientInfo(string operationGroupKey, string clientName, string clientNamespace, string clientDescription, IReadOnlyList operations, IReadOnlyList clientParameters, ISet resourceParameters) + : this(operationGroupKey, clientName, clientNamespace, clientDescription, null, operations, clientParameters, resourceParameters) + { + } + + public ClientInfo(string operationGroupKey, string clientName, string clientNamespace, string clientDescription, INamedTypeSymbol? existingType, IReadOnlyList operations, IReadOnlyList clientParameters, ISet resourceParameters) + { + OperationGroupKey = operationGroupKey; + Name = clientName; + Namespace = clientNamespace; + Description = clientDescription; + ExistingType = existingType; + Operations = operations; + _initClientParameters = clientParameters; + ResourceParameters = resourceParameters; + Children = new List(); + Requests = new List(); + } + + public ClientInfo(string clientName, string clientNamespace, IReadOnlyList clientParameters) + { + OperationGroupKey = string.Empty; + Name = clientName; + Namespace = clientNamespace; + Description = string.Empty; + ExistingType = null; + Operations = Array.Empty(); + _initClientParameters = clientParameters; + ResourceParameters = new HashSet(); + Children = new List(); + Requests = new List(); + } + } + } +} diff --git a/logger/autorest.csharp/level/Output/LowLevelClient.cs b/logger/autorest.csharp/level/Output/LowLevelClient.cs new file mode 100644 index 0000000..8099e43 --- /dev/null +++ b/logger/autorest.csharp/level/Output/LowLevelClient.cs @@ -0,0 +1,480 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models.Responses; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Output.Models +{ + internal class LowLevelClient : TypeProvider + { + private readonly string _libraryName; + private readonly TypeFactory _typeFactory; + private readonly IEnumerable _clientParameters; + private readonly InputAuth _authorization; + private readonly IEnumerable _operations; + + protected override string DefaultName { get; } + protected override string DefaultAccessibility => "public"; + + private ConstructorSignature? _subClientInternalConstructor; + + public string Description { get; } + public ConstructorSignature SubClientInternalConstructor => _subClientInternalConstructor ??= BuildSubClientInternalConstructor(); + + public IReadOnlyList SubClients { get; init; } + public LowLevelClient? ParentClient; + + public ClientOptionsTypeProvider ClientOptions { get; } + + public bool IsSubClient { get; } + + private bool? _isResourceClient; + public bool IsResourceClient => _isResourceClient ??= Parameters.Any(p => p.IsResourceIdentifier); + + private LowLevelClient? _topLevelClient; + public LowLevelClient TopLevelClient => _topLevelClient ??= GetTopLevelClient(this); + + private LowLevelClient GetTopLevelClient(LowLevelClient client) + { + if (client.ParentClient is null) + return client; + + return GetTopLevelClient(client.ParentClient); + } + + public LowLevelClient(string name, string ns, string description, string libraryName, LowLevelClient? parentClient, IEnumerable operations, IEnumerable clientParameters, InputAuth authorization, SourceInputModel? sourceInputModel, ClientOptionsTypeProvider clientOptions, TypeFactory typeFactory) + : base(ns, sourceInputModel) + { + _libraryName = libraryName; + _typeFactory = typeFactory; + DefaultName = name; + Description = description; + IsSubClient = parentClient != null; + ParentClient = parentClient; + + ClientOptions = clientOptions; + + _clientParameters = clientParameters; + _authorization = authorization; + _operations = operations; + + SubClients = Array.Empty(); + } + + private IReadOnlyList? _parameters; + public IReadOnlyList Parameters => _parameters ??= new RestClientBuilder(_clientParameters, _operations, _typeFactory).GetOrderedParametersByRequired(); + + private ClientFields? _fields; + public ClientFields Fields => _fields ??= ClientFields.CreateForClient(Parameters, _authorization); + + private (ConstructorSignature[] PrimaryConstructors, ConstructorSignature[] SecondaryConstructors)? _constructors; + private (ConstructorSignature[] PrimaryConstructors, ConstructorSignature[] SecondaryConstructors) Constructors => _constructors ??= BuildPublicConstructors(Parameters); + public ConstructorSignature[] PrimaryConstructors => Constructors.PrimaryConstructors; + public ConstructorSignature[] SecondaryConstructors => Constructors.SecondaryConstructors; + + private IReadOnlyList? _allClientMethods; + private IReadOnlyList AllClientMethods => _allClientMethods ??= BuildMethods(this, _typeFactory, _operations, _clientParameters, Fields, Declaration.Namespace, Declaration.Name, _sourceInputModel).ToArray(); + + private IReadOnlyList? _clientMethods; + public IReadOnlyList ClientMethods => _clientMethods ??= AllClientMethods + .OrderBy(m => m.LongRunning != null ? 2 : m.PagingInfo != null ? 1 : 0) // Temporary sorting to minimize amount of changed files. Will be removed when new LRO is implemented + .ToArray(); + + private IReadOnlyList? _requestMethods; + public IReadOnlyList RequestMethods => _requestMethods ??= AllClientMethods.Select(m => m.RequestMethod) + .Concat(ClientMethods.Select(m => m.PagingInfo?.NextPageMethod).WhereNotNull()) + .Distinct() + .ToArray(); + + private IReadOnlyList? _responseClassifierTypes; + public IReadOnlyList ResponseClassifierTypes => _responseClassifierTypes ??= RequestMethods.Select(m => m.ResponseClassifierType).ToArray(); + + private LowLevelSubClientFactoryMethod? _factoryMethod; + public LowLevelSubClientFactoryMethod? FactoryMethod => _factoryMethod ??= ParentClient != null ? BuildFactoryMethod(ParentClient.Fields, _libraryName) : null; + + public IEnumerable CustomMethods() + { + //TODO: get all custom methods using similar method to GetEffectiveCtor + yield break; + } + + public IReadOnlyList GetConstructorParameters() + { + return _clientParameters.Concat(_operations.SelectMany(op => op.Parameters).Where(p => p.Kind == InputOperationParameterKind.Client)).DistinctBy(p => p.Name).ToList(); + } + public static IEnumerable BuildMethods(LowLevelClient? client, TypeFactory typeFactory, IEnumerable operations, IEnumerable clientParameters, ClientFields fields, string namespaceName, string clientName, SourceInputModel? sourceInputModel) + { + var builders = operations.ToDictionary(o => o, o => new OperationMethodChainBuilder(client, o, namespaceName, clientName, clientParameters, fields, typeFactory, sourceInputModel)); + foreach (var (_, builder) in builders) + { + builder.BuildNextPageMethod(builders); + } + + foreach (var (_, builder) in builders) + { + yield return builder.BuildOperationMethodChain(); + } + } + + private (ConstructorSignature[] PrimaryConstructors, ConstructorSignature[] SecondaryConstructors) BuildPublicConstructors(IReadOnlyList orderedParameters) + { + if (!IsSubClient) + { + var requiredParameters = RestClientBuilder.GetRequiredParameters(orderedParameters).ToArray(); + var optionalParameters = GetOptionalParametersInConstructor(RestClientBuilder.GetOptionalParameters(orderedParameters).Append(CreateOptionsParameter())).ToArray(); + + return ( + BuildPrimaryConstructors(requiredParameters, optionalParameters).ToArray(), + BuildSecondaryConstructors(requiredParameters, optionalParameters).ToArray() + ); + } + else + { + return (Array.Empty(), new[] { CreateMockingConstructor() }); + } + } + + private IEnumerable GetOptionalParametersInConstructor(IEnumerable optionalParameters) + { + return optionalParameters.Where( + p => ClientOptions.Type.EqualsIgnoreNullable(p.Type) || p.IsEndpoint); // Endpoint is an exception, even it is optional, still need to be the parameter of constructor + } + + public IEnumerable GetOptionalParametersInOptions() + { + return RestClientBuilder.GetOptionalParameters(Parameters).Where(p => !p.IsEndpoint); + } + + private IEnumerable BuildPrimaryConstructors(IReadOnlyList requiredParameters, IReadOnlyList optionalParameters) + { + var optionalToRequired = optionalParameters + .Select(parameter => ClientOptions.Type.EqualsIgnoreNullable(parameter.Type) + ? parameter with { DefaultValue = null, Validation = ValidationType.None } + : parameter with + { + DefaultValue = null, + Validation = parameter.Initializer != null ? Parameter.GetValidation(parameter.Type, parameter.RequestLocation, parameter.SkipUrlEncoding) : parameter.Validation, + Initializer = null + }).ToArray(); + + if (Fields.CredentialFields.Count == 0) + { + /* order the constructor paramters as (endpoint, requiredParameters, OptionalParamters). + * use the OrderBy to put endpoint as the first parameter. + * */ + yield return CreatePrimaryConstructor(requiredParameters.Concat(optionalToRequired).OrderBy(parameter => !parameter.Name.Equals("endpoint", StringComparison.OrdinalIgnoreCase)).ToArray()); + } + else + { + foreach (var credentialField in Fields.CredentialFields) + { + /* order the constructor paramters as (endpoint, requiredParameters, CredentialParamter, OptionalParamters). + * use the OrderBy to put endpoint as the first parameter. + * */ + yield return CreatePrimaryConstructor(requiredParameters.Append(CreateCredentialParameter(credentialField!.Type)).Concat(optionalToRequired).OrderBy(parameter => !parameter.Name.Equals("endpoint", StringComparison.OrdinalIgnoreCase)).ToArray()); + } + } + } + + private IEnumerable BuildSecondaryConstructors(IReadOnlyList requiredParameters, IReadOnlyList optionalParameters) + { + if (requiredParameters.Any() || Fields.CredentialFields.Any()) + { + yield return CreateMockingConstructor(); + } + + /* Construct the parameter arguments to call primitive constructor. + * In primitive constructor, the endpoint is the first parameter, + * so put the endpoint as the first parameter argument if the endpoint is optional paramter. + * */ + var optionalParametersArguments = optionalParameters + .Where(p => !p.Name.Equals("endpoint", StringComparison.OrdinalIgnoreCase)) + .Select(p => new FormattableStringToExpression(p.Initializer ?? p.Type.GetParameterInitializer(p.DefaultValue!.Value)!)) + .ToArray(); + var optionalEndpoint = optionalParameters.Where(p => p.Name.Equals("endpoint", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + var arguments = new List(); + if (optionalEndpoint != null) + { + arguments.Add(new FormattableStringToExpression(optionalEndpoint.Initializer ?? optionalEndpoint.Type.GetParameterInitializer(optionalEndpoint.DefaultValue!.Value)!)); + } + + arguments.AddRange(requiredParameters.Select(p => (ValueExpression)p)); + + if (Fields.CredentialFields.Count == 0) + { + var allArguments = arguments.Concat(optionalParametersArguments); + yield return CreateSecondaryConstructor(requiredParameters, allArguments.ToArray()); + } + else + { + foreach (var credentialField in Fields.CredentialFields) + { + var credentialParameter = CreateCredentialParameter(credentialField!.Type); + var allArguments = arguments.Append(credentialParameter).Concat(optionalParametersArguments); + yield return CreateSecondaryConstructor(requiredParameters.Append(credentialParameter).ToArray(), allArguments.ToArray()); + } + } + } + + private ConstructorSignature CreatePrimaryConstructor(IReadOnlyList parameters) + => new(Type, $"Initializes a new instance of {Declaration.Name}", null, Public, parameters); + + private ConstructorSignature CreateSecondaryConstructor(IReadOnlyList parameters, IReadOnlyList arguments) + { + return new(Type, $"Initializes a new instance of {Declaration.Name}", null, Public, parameters, Initializer: new ConstructorInitializer(false, arguments)); + } + + private Parameter CreateCredentialParameter(CSharpType type) + { + return new Parameter( + "credential", + Configuration.ApiTypes.CredentialDescription, + type, + null, + ValidationType.AssertNotNull, + null); + } + + private ConstructorSignature CreateMockingConstructor() + => new(Type, $"Initializes a new instance of {Declaration.Name} for mocking.", null, Protected, Array.Empty()); + + private Parameter CreateOptionsParameter() + { + var clientOptionsType = ClientOptions.Type.WithNullable(true); + return new Parameter("options", $"The options for configuring the client.", clientOptionsType, Constant.Default(clientOptionsType), ValidationType.None, Constant.NewInstanceOf(clientOptionsType).GetConstantFormattable()); + } + + private ConstructorSignature BuildSubClientInternalConstructor() + { + var constructorParameters = GetSubClientFactoryMethodParameters() + .Select(p => p with { DefaultValue = null, Validation = ValidationType.None, Initializer = null }) + .ToArray(); + + return new ConstructorSignature(Type, $"Initializes a new instance of {Declaration.Name}", null, Internal, constructorParameters); + } + + public LowLevelSubClientFactoryMethod BuildFactoryMethod(ClientFields parentFields, string libraryName) + { + var constructorCallParameters = GetSubClientFactoryMethodParameters().ToArray(); + var methodParameters = constructorCallParameters.Where(p => parentFields.GetFieldByParameter(p) == null).ToArray(); + + var subClientName = Type.Name; + var methodName = subClientName.StartsWith(libraryName) + ? subClientName[libraryName.Length..] + : subClientName; + + if (!IsResourceClient && !methodName.EndsWith(ClientBuilder.GetClientSuffix())) + { + methodName += ClientBuilder.GetClientSuffix(); + } + + var methodSignature = new MethodSignature($"Get{methodName}", + $"Initializes a new instance of {Type.Name}", null, Public | Virtual, Type, null, + methodParameters.ToArray()); + FieldDeclaration? cachingField = methodParameters.Any() + ? null + : new FieldDeclaration(FieldModifiers.Private, this.Type, $"_cached{Type.Name}"); + + return new LowLevelSubClientFactoryMethod(methodSignature, cachingField, constructorCallParameters); + } + + private IEnumerable GetSubClientFactoryMethodParameters() + { + var knownParameters = Configuration.IsBranded + ? new[] { KnownParameters.ClientDiagnostics, KnownParameters.Pipeline, KnownParameters.KeyAuth, KnownParameters.TokenAuth } + : new[] { KnownParameters.Pipeline, KnownParameters.KeyAuth, KnownParameters.TokenAuth }; + return knownParameters + .Concat(RestClientBuilder.GetConstructorParameters(Parameters, null, includeAPIVersion: true).OrderBy(parameter => !parameter.Name.Equals("endpoint", StringComparison.OrdinalIgnoreCase))) + .Where(p => Fields.GetFieldByParameter(p) != null); + } + + internal MethodSignatureBase? GetEffectiveCtor(bool includeClientOptions = false) + { + //TODO: This method is needed because we allow roslyn code gen attributes to be run AFTER the writers do their job but before + // the code is emitted. This is a hack to allow the writers to know what the effective ctor is after the roslyn code gen attributes + var constructors = includeClientOptions ? PrimaryConstructors : SecondaryConstructors; + + List candidates = new(constructors.Where(c => c.Modifiers.HasFlag(Public))); + + if (ExistingType is not null) + { + // [CodeGenSuppress("ConfidentialLedgerCertificateClient", typeof(Uri), typeof(TokenCredential), typeof(ConfidentialLedgerClientOptions))] + // remove suppressed ctors from the candidates + foreach (var attribute in ExistingType.GetAttributes().Where(a => a.AttributeClass is not null && a.AttributeClass.Name == CodeGenAttributes.CodeGenSuppressAttributeName)) + { + if (attribute.ConstructorArguments.Length != 2) + continue; + var classTarget = attribute.ConstructorArguments[0].Value; + if (classTarget is null || !classTarget.Equals(DefaultName)) + continue; + + candidates.RemoveAll(ctor => IsParamMatch(ctor.Parameters, attribute.ConstructorArguments[1].Values.Select(tc => (INamedTypeSymbol)(tc.Value!)).ToArray())); + } + + // add custom ctors into the candidates + foreach (var existingCtor in ExistingType.Constructors) + { + var parameters = existingCtor.Parameters; + var modifiers = GetModifiers(existingCtor); + bool isPublic = modifiers.HasFlag(Public); + //TODO: Currently skipping ctors which use models from the library due to constructing with all empty lists. + if (!isPublic || parameters.Length == 0 || parameters.Any(p => ((INamedTypeSymbol)p.Type).GetCSharpType(_typeFactory) == null)) + { + if (!isPublic) + candidates.RemoveAll(ctor => IsParamMatch(ctor.Parameters, existingCtor.Parameters.Select(p => (INamedTypeSymbol)p.Type).ToArray())); + continue; + } + var ctor = new ConstructorSignature( + Type, + GetSummaryPortion(existingCtor.GetDocumentationCommentXml()), + null, + modifiers, + parameters.Select(p => new Parameter( + p.Name, + $"{p.GetDocumentationCommentXml()}", + ((INamedTypeSymbol)p.Type).GetCSharpType(_typeFactory)!, + null, + ValidationType.None, + null)).ToArray(), + null); + candidates.Add(ctor); + } + } + + var results = candidates.OrderBy(c => c.Parameters.Count); + return includeClientOptions + ? results.FirstOrDefault(c => c.Parameters.Last().Type.EqualsIgnoreNullable(ClientOptions.Type)) + : results.FirstOrDefault(); + } + + private FormattableString? GetSummaryPortion(string? xmlComment) + { + if (xmlComment is null) + return null; + + ReadOnlySpan span = xmlComment.AsSpan(); + int start = span.IndexOf(""); + if (start == -1) + return null; + start += 9; + int end = span.IndexOf(""); + if (end == -1) + return null; + return $"{span.Slice(start, end - start).Trim().ToString()}"; + } + + private bool IsParamMatch(IReadOnlyList methodParameters, INamedTypeSymbol[] suppressionParameters) + { + if (methodParameters.Count != suppressionParameters.Length) + return false; + + for (int i = 0; i < methodParameters.Count; i++) + { + if (!suppressionParameters[i].IsSameType(methodParameters[i].Type)) + return false; + } + + return true; + } + + private MethodSignatureModifiers GetModifiers(IMethodSymbol existingCtor) + { + MethodSignatureModifiers result = GetAccessModifier(existingCtor.DeclaredAccessibility); + if (existingCtor.IsStatic) + { + result |= MethodSignatureModifiers.Static; + } + if (existingCtor.IsVirtual) + { + result |= MethodSignatureModifiers.Virtual; + } + return result; + } + + private MethodSignatureModifiers GetAccessModifier(Accessibility declaredAccessibility) => declaredAccessibility switch + { + Accessibility.Public => MethodSignatureModifiers.Public, + Accessibility.Protected => MethodSignatureModifiers.Protected, + Accessibility.Internal => MethodSignatureModifiers.Internal, + _ => MethodSignatureModifiers.Private + }; + + internal bool IsMethodSuppressed(MethodSignature signature) + { + if (ExistingType is null) + return false; + + //[CodeGenSuppress("GetTests", typeof(string), typeof(string), typeof(DateTimeOffset?), typeof(DateTimeOffset?), typeof(int?), typeof(RequestContext))] + //remove suppressed ctors from the candidates + foreach (var attribute in ExistingType.GetAttributes().Where(a => a.AttributeClass is not null && a.AttributeClass.Name == "CodeGenSuppressAttribute")) + { + if (attribute.ConstructorArguments.Length != 2) + continue; + var methodTarget = attribute.ConstructorArguments[0].Value; + if (methodTarget is null || !methodTarget.Equals(signature.Name)) + continue; + + if (IsParamMatch(signature.Parameters, attribute.ConstructorArguments[1].Values.Select(tc => (INamedTypeSymbol)(tc.Value!)).ToArray())) + return true; + } + + return false; + } + + internal bool HasMatchingCustomMethod(LowLevelClientMethod method) + { + if (ExistingType is not null) + { + // add custom ctors into the candidates + foreach (var member in ExistingType.GetMembers()) + { + if (member is not IMethodSymbol methodSymbol) + continue; + + if (methodSymbol.Name != method.RequestMethod.Name) + continue; + + if (methodSymbol.Parameters.Length != method.ProtocolMethodSignature.Parameters.Count - 1) + continue; + + if (methodSymbol.Parameters.Last().Type.Name == "CancellationToken") + continue; + + bool allEqual = true; + for (int i = 0; i < methodSymbol.Parameters.Length; i++) + { + if (!((INamedTypeSymbol)methodSymbol.Parameters[i].Type).IsSameType(method.ProtocolMethodSignature.Parameters[i].Type)) + { + allEqual = false; + break; + } + } + if (allEqual) + return true; + } + } + + return false; + } + + private bool? _hasConvenienceMethods; + internal bool HasConvenienceMethods => _hasConvenienceMethods ??= AllClientMethods.Any(m => m.ConvenienceMethod is not null && m.ConvenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + } +} diff --git a/logger/autorest.csharp/level/Output/LowLevelClientMethod.cs b/logger/autorest.csharp/level/Output/LowLevelClientMethod.cs new file mode 100644 index 0000000..1c16d9a --- /dev/null +++ b/logger/autorest.csharp/level/Output/LowLevelClientMethod.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Samples.Models; + +namespace AutoRest.CSharp.Output.Models +{ + internal record LowLevelClientMethod( + MethodSignature ProtocolMethodSignature, + ConvenienceMethod? ConvenienceMethod, + RestClientMethod RequestMethod, + InputType? RequestBodyType, + InputType? ResponseBodyType, + Diagnostic ProtocolMethodDiagnostic, + ProtocolMethodPaging? PagingInfo, + OperationLongRunning? LongRunning, + RequestConditionHeaders ConditionHeaderFlag, + IEnumerable Samples, + ConvenienceMethodOmittingMessage? ConvenienceMethodOmittingMessage, + LongRunningResultRetrievalMethod? LongRunningResultRetrievalMethod) // TODO: move `LongRunningResultRetrievalMethod` under output model of long running, currently we're using + // input model of long running in DPG + { + public bool ShouldGenerateConvenienceMethodRef() + { + // we only generate the convenience method ref when convenience method and protocol method have the same accessibility + var convenienceMethod = ConvenienceMethod?.Signature.Modifiers; + var protocolMethod = ProtocolMethodSignature.Modifiers; + + if (convenienceMethod?.HasFlag(MethodSignatureModifiers.Public) is true && protocolMethod.HasFlag(MethodSignatureModifiers.Public) is true) + return true; + + if (convenienceMethod?.HasFlag(MethodSignatureModifiers.Internal) is true && protocolMethod.HasFlag(MethodSignatureModifiers.Internal) is true) + return true; + + return false; + } + } +} diff --git a/logger/autorest.csharp/level/Output/LowLevelSubClientFactoryMethod.cs b/logger/autorest.csharp/level/Output/LowLevelSubClientFactoryMethod.cs new file mode 100644 index 0000000..b77a703 --- /dev/null +++ b/logger/autorest.csharp/level/Output/LowLevelSubClientFactoryMethod.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Models +{ + internal record LowLevelSubClientFactoryMethod(MethodSignature Signature, FieldDeclaration? CachingField, ICollection ConstructorCallParameters); +} diff --git a/logger/autorest.csharp/level/Output/OperationMethodChainBuilder.cs b/logger/autorest.csharp/level/Output/OperationMethodChainBuilder.cs new file mode 100644 index 0000000..ba4c2e7 --- /dev/null +++ b/logger/autorest.csharp/level/Output/OperationMethodChainBuilder.cs @@ -0,0 +1,715 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Serialization; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Output.Samples.Models; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; +using Configuration = AutoRest.CSharp.Common.Input.Configuration; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using AutoRest.CSharp.Common.Input.InputTypes; + +namespace AutoRest.CSharp.Output.Models +{ + internal class OperationMethodChainBuilder + { + private static readonly Dictionary ConditionRequestHeader = new(StringComparer.OrdinalIgnoreCase) + { + ["If-Match"] = RequestConditionHeaders.IfMatch, + ["If-None-Match"] = RequestConditionHeaders.IfNoneMatch, + ["If-Modified-Since"] = RequestConditionHeaders.IfModifiedSince, + ["If-Unmodified-Since"] = RequestConditionHeaders.IfUnmodifiedSince + }; + + private readonly string _namespaceName; + private readonly string _clientName; + private readonly LowLevelClient? _client; + private readonly ClientFields _fields; + private readonly TypeFactory _typeFactory; + private readonly SourceInputModel? _sourceInputModel; + private readonly List _orderedParameters; + private readonly ReturnTypeChain _returnType; + private readonly List _requestParts; + private readonly RestClientMethod _restClientMethod; + + private Parameter? _protocolBodyParameter; + private ProtocolMethodPaging? _protocolMethodPaging; + private RequestConditionHeaders _conditionHeaderFlag = RequestConditionHeaders.None; + + private InputOperation Operation { get; } + private IEnumerable ClientParameters { get; } + + public OperationMethodChainBuilder(LowLevelClient? client, InputOperation operation, string namespaceName, string clientName, IEnumerable clientParameters, ClientFields fields, TypeFactory typeFactory, SourceInputModel? sourceInputModel) + { + _client = client; + _namespaceName = namespaceName; + _clientName = clientName; + _fields = fields; + _typeFactory = typeFactory; + _sourceInputModel = sourceInputModel; + _orderedParameters = new List(); + _requestParts = new List(); + + Operation = operation; + ClientParameters = clientParameters; + _returnType = BuildReturnTypes(); + BuildParameters(); + _restClientMethod = RestClientBuilder.BuildRequestMethod(Operation, _orderedParameters.Select(p => p.CreateMessage).WhereNotNull().ToArray(), _requestParts, _protocolBodyParameter, _typeFactory); + } + + public void BuildNextPageMethod(IReadOnlyDictionary builders) + { + var paging = Operation.Paging; + if (paging == null) + { + return; + } + + RestClientMethod? nextPageMethod = paging switch + { + { SelfNextLink: true } => _restClientMethod, + { NextLinkOperation: { } nextLinkOperation } => builders[nextLinkOperation]._restClientMethod, + { NextLinkName: { } } => RestClientBuilder.BuildNextPageMethod(_restClientMethod), + _ => null + }; + + _protocolMethodPaging = new ProtocolMethodPaging(nextPageMethod, paging.NextLinkName, paging.ItemName ?? "value"); + } + + public LowLevelClientMethod BuildOperationMethodChain() + { + var protocolMethodAttributes = Operation.Deprecated is { } deprecated + ? new[] { new CSharpAttribute(typeof(ObsoleteAttribute), Literal(deprecated)) } + : Array.Empty(); + + var isRequestContextOptional = _orderedParameters.Any(p => p.Protocol == KnownParameters.RequestContext); + // because request context is always the last parameter, when request context is changed to required by some reason, all parameters before it (which means all parameters) should be required + var protocolMethodParameters = isRequestContextOptional + ? _orderedParameters.Select(p => p.Protocol).WhereNotNull().ToArray() + : _orderedParameters.Select(p => p.Protocol?.ToRequired()).WhereNotNull().ToArray(); + var protocolMethodModifiers = (Operation.GenerateProtocolMethod ? _restClientMethod.Accessibility : Internal) | Virtual; + var protocolMethodSignature = new MethodSignature(_restClientMethod.Name, FormattableStringHelpers.FromString(_restClientMethod.Summary), FormattableStringHelpers.FromString(_restClientMethod.Description), protocolMethodModifiers, _returnType.Protocol, null, protocolMethodParameters, protocolMethodAttributes); + var convenienceMethodInfo = ShouldGenerateConvenienceMethod(); + var convenienceMethod = BuildConvenienceMethod(isRequestContextOptional, convenienceMethodInfo); + + var diagnostic = new Diagnostic($"{_clientName}.{_restClientMethod.Name}"); + + var requestBodyType = Operation.Parameters.FirstOrDefault(p => p.Location == RequestLocation.Body)?.Type; + var responseBodyType = GetReturnedResponseInputType(); + + // samples will build below + var samples = new List(); + + var method = new LowLevelClientMethod(protocolMethodSignature, convenienceMethod, _restClientMethod, requestBodyType, responseBodyType, diagnostic, _protocolMethodPaging, Operation.LongRunning, _conditionHeaderFlag, samples, convenienceMethodInfo.Message, GetLongRunningResultRetrievalMethod(Operation.LongRunning)); + + BuildSamples(method, samples); + + return method; + } + + private void BuildSamples(LowLevelClientMethod method, List samples) + { + // we do not generate any sample if these variables are null + // they are only null when HLC calling methods in this class + if (_client == null) + return; + var shouldGenerateSample = DpgOperationSample.ShouldGenerateSample(_client, method.ProtocolMethodSignature); + + if (!shouldGenerateSample) + return; + + // short version samples + var shouldGenerateShortVersion = DpgOperationSample.ShouldGenerateShortVersion(_client, method); + + foreach (var example in Operation.Examples) + { + if (!shouldGenerateShortVersion && example.Name == ExampleMockValueBuilder.ShortVersionMockExampleKey) + continue; + + // add protocol method sample + samples.Add(new( + _client, + _typeFactory, + method, + Operation, + example, + false, + example.Name)); + + // add convenience method sample + if (method.ConvenienceMethod != null && method.ConvenienceMethod.Signature.Modifiers.HasFlag(Public)) + { + samples.Add(new( + _client, + _typeFactory, + method, + Operation, + example, + true, + example.Name)); + } + } + } + + private LongRunningResultRetrievalMethod? GetLongRunningResultRetrievalMethod(OperationLongRunning? longRunning) + { + if (longRunning is { ResultPath: not null }) + return new(_typeFactory.CreateType(longRunning.ReturnType!), longRunning.FinalResponse.BodyType!.Name, longRunning.ResultPath); + + return null; + } + + private ConvenienceMethodGenerationInfo ShouldGenerateConvenienceMethod() + { + // we do not generate convenience method if the emitter does not allow it. + if (!Operation.GenerateConvenienceMethod) + return new() + { + IsConvenienceMethodGenerated = false + }; + + // if the protocol method is generated and the parameters are the same in protocol and convenience, we do not generate the convenience. + if (Operation.GenerateProtocolMethod && !IsConvenienceMethodMeaningful()) + { + return new() + { + Message = ConvenienceMethodOmittingMessage.NotMeaningful, + IsConvenienceMethodGenerated = false + }; + } + + return new() + { + IsConvenienceMethodGenerated = true, + IsConvenienceMethodInternal = false + }; + } + + // If all the corresponding parameters and return types of convenience method and protocol method have the same type, it does not make sense to generate the convenience method. + private bool IsConvenienceMethodMeaningful() + { + return _orderedParameters.Where(parameter => parameter.Protocol != KnownParameters.RequestContext && parameter.Protocol != KnownParameters.RequestContextRequired).Any(parameter => !IsParameterTypeSame(parameter.Convenience, parameter.Protocol)) + || !_returnType.Convenience.Equals(_returnType.Protocol); + } + + private bool HasAmbiguityBetweenProtocolAndConvenience() + { + var userDefinedParameters = _orderedParameters.Where(parameter => parameter.Convenience != KnownParameters.CancellationTokenParameter); + int protocolRequired = userDefinedParameters.Select(p => p.Protocol).WhereNotNull().Where(p => !p.IsOptionalInSignature).Count(); + int convenienceRequired = userDefinedParameters.Select(p => p.Convenience).WhereNotNull().Where(p => !p.IsOptionalInSignature).Count(); + if (protocolRequired != convenienceRequired) + { + return false; + } + return userDefinedParameters.Where(p => p.Protocol != null && !p.Protocol.IsOptionalInSignature).All(parameter => IsParameterTypeSame(parameter.Convenience, parameter.Protocol)); + } + + private bool ShouldRequestContextOptional() + { + if (Configuration.KeepNonOverloadableProtocolSignature || !IsConvenienceMethodMeaningful()) + { + return true; + } + + if (_sourceInputModel != null) + { + var existingMethod = _sourceInputModel.FindMethod(_namespaceName, _clientName, Operation.CleanName, _orderedParameters.Select(p => p.Protocol).WhereNotNull().Select(p => p.Type)); + if (existingMethod != null && existingMethod.Parameters.Length > 0 && existingMethod.Parameters.Last().IsOptional) + { + return true; + } + } + + if (HasAmbiguityBetweenProtocolAndConvenience()) + { + return false; + } + + return true; + } + + private bool IsParameterTypeSame(Parameter? first, Parameter? second) + { + return object.Equals(first?.Type, second?.Type); + } + + private ReturnTypeChain BuildReturnTypes() + { + CSharpType? responseType = GetReturnedResponseCSharpType(); + + if (Operation.Paging != null) + { + if (responseType == null) + { + throw new InvalidOperationException($"Method {Operation.Name} has to have a return value"); + } + + if (responseType is { IsFrameworkType: false, Implementation: ModelTypeProvider modelType }) + { + var property = modelType.GetPropertyBySerializedName(Operation.Paging.ItemName ?? "value", true); + if (!property.ValueType.IsList) + { + throw new InvalidOperationException($"'{modelType.Declaration.Name}.{property.Declaration.Name}' property must be a collection of items"); + } + + responseType = property.ValueType.ElementType; + } + else if (responseType.IsList) + { + responseType = responseType.ElementType; + } + + if (Operation.LongRunning != null) + { + var convenienceMethodReturnType = new CSharpType(typeof(Operation<>), new CSharpType(typeof(Pageable<>), responseType)); + return new ReturnTypeChain(convenienceMethodReturnType, typeof(Operation>), responseType); + } + + return new ReturnTypeChain(new CSharpType(typeof(Pageable<>), responseType), typeof(Pageable), responseType); + } + + if (Operation.LongRunning != null) + { + if (responseType != null) + { + return new ReturnTypeChain(new CSharpType(typeof(Operation<>), responseType), typeof(Operation), responseType); + } + + return new ReturnTypeChain(typeof(Operation), typeof(Operation), null); + } + + var headAsBoolean = Operation.HttpMethod == RequestMethod.Head && Configuration.HeadAsBoolean; + if (headAsBoolean) + { + return new ReturnTypeChain(Configuration.ApiTypes.GetResponseOfT(), Configuration.ApiTypes.GetResponseOfT(), null); + } + + if (responseType != null) + { + return new ReturnTypeChain(new CSharpType(Configuration.ApiTypes.ResponseOfTType, responseType), Configuration.ApiTypes.ResponseType, responseType); + } + + return new ReturnTypeChain(Configuration.ApiTypes.ResponseType, Configuration.ApiTypes.ResponseType, null); + } + + private CSharpType? GetReturnedResponseCSharpType() + { + var inputType = GetReturnedResponseInputType(); + if (inputType != null) + { + return _typeFactory.CreateType(inputType).OutputType; + } + return null; + } + + private InputType? GetReturnedResponseInputType() + { + if (Operation.LongRunning != null) + { + return Operation.LongRunning.ReturnType; + } + + var operationBodyTypes = Operation.Responses.Where(r => !r.IsErrorResponse).Select(r => r.BodyType).Distinct(); + if (operationBodyTypes.Any()) + { + return operationBodyTypes.First(); + } + + return null; + } + + private IReadOnlyList? GetReturnedResponseContentType() + { + var responses = Operation.Responses.Where(r => !r.IsErrorResponse); + return responses.Any() ? responses.First().ContentTypes : null; + } + + private ConvenienceMethod? BuildConvenienceMethod(bool shouldRequestContextOptional, ConvenienceMethodGenerationInfo generationInfo) + { + if (!generationInfo.IsConvenienceMethodGenerated) + return null; + + bool needNameChange = shouldRequestContextOptional && HasAmbiguityBetweenProtocolAndConvenience(); + string name = _restClientMethod.Name; + if (needNameChange) + { + name = _restClientMethod.Name.IsLastWordSingular() ? $"{_restClientMethod.Name}Value" : $"{_restClientMethod.Name.LastWordToSingular()}Values"; + } + var attributes = Operation.Deprecated is { } deprecated + ? new[] { new CSharpAttribute(typeof(ObsoleteAttribute), Literal(deprecated)) } + : null; + + var protocolToConvenience = new List(); + var parameterList = new List(); + foreach (var parameterChain in _orderedParameters) + { + var protocolParameter = parameterChain.Protocol; + var convenienceParameter = parameterChain.Convenience; + if (convenienceParameter != null) + { + if (parameterChain.IsSpreadParameter) + { + if (convenienceParameter.Type is { IsFrameworkType: false, Implementation: ModelTypeProvider model }) + { + var parameters = BuildSpreadParameters(model).OrderBy(p => p.DefaultValue == null ? 0 : 1).ToArray(); + + parameterList.AddRange(parameters); + protocolToConvenience.Add(new ProtocolToConvenienceParameterConverter(protocolParameter!, convenienceParameter, new ConvenienceParameterSpread(model, parameters))); + } + else + { + throw new InvalidOperationException($"The parameter {convenienceParameter.Name} in method {_client?.Declaration.FullName}.{name} is marked as Spread but its type is not ModelTypeProvider (got {convenienceParameter.Type})"); + } + } + else + { + parameterList.Add(convenienceParameter); + if (protocolParameter != null) + protocolToConvenience.Add(new ProtocolToConvenienceParameterConverter(protocolParameter, convenienceParameter, null)); + } + } + else if (protocolParameter != null) + { + // convenience parameter is null - such as CancellationToken in non-azure library + protocolToConvenience.Add(new ProtocolToConvenienceParameterConverter(protocolParameter, convenienceParameter, null)); + } + } + var accessibility = _restClientMethod.Accessibility | Virtual; + if (generationInfo.IsConvenienceMethodInternal) + { + accessibility &= ~Public; // removes public if any + accessibility |= Internal; // add internal + } + var convenienceSignature = new MethodSignature(name, FormattableStringHelpers.FromString(_restClientMethod.Summary), FormattableStringHelpers.FromString(_restClientMethod.Description), accessibility, _returnType.Convenience, null, parameterList, attributes); + var diagnostic = Configuration.IsBranded + ? name != _restClientMethod.Name ? new Diagnostic($"{_clientName}.{convenienceSignature.Name}") : null + : null; + return new ConvenienceMethod(convenienceSignature, protocolToConvenience, _returnType.ConvenienceResponseType, Operation.RequestMediaTypes, GetReturnedResponseContentType(), diagnostic, _protocolMethodPaging is not null, Operation.LongRunning is not null, Operation.Deprecated); + } + + private IEnumerable BuildSpreadParameters(ModelTypeProvider model) + { + var fields = model.Fields; + var addedParameters = new HashSet(); + foreach (var parameter in fields.PublicConstructorParameters) + { + addedParameters.Add(parameter.Name); + yield return parameter; + } + + foreach (var parameter in fields.SerializationParameters) + { + if (addedParameters.Contains(parameter.Name)) + continue; + + var field = fields.GetFieldByParameterName(parameter.Name); + var inputProperty = fields.GetInputByField(field); + if (inputProperty is null) + continue; // this means this is an additional properties property, which should never happen. + + if (inputProperty.IsRequired && inputProperty.Type is InputLiteralType) + continue; + var inputType = parameter.Type.InputType.WithNullable(!inputProperty.IsRequired); + Constant? defaultValue = null; + if (!inputProperty.IsRequired) + defaultValue = Constant.Default(inputType); + + yield return parameter with + { + Type = inputType, + DefaultValue = defaultValue, + }; + } + } + + private void BuildParameters() + { + var operationParameters = RestClientBuilder.FilterOperationAllParameters(Operation.Parameters) + .Concat(ClientParameters) + .DistinctBy(p => p.NameInRequest); + + var requiredPathParameters = new Dictionary(); + var optionalPathParameters = new Dictionary(); + var requiredRequestParameters = new List(); + var optionalRequestParameters = new List(); + + var requestConditionHeaders = RequestConditionHeaders.None; + var requestConditionSerializationFormat = SerializationFormat.Default; + InputParameter? bodyParameter = null; + InputParameter? contentTypeRequestParameter = null; + InputParameter? requestConditionRequestParameter = null; + + foreach (var operationParameter in operationParameters) + { + switch (operationParameter) + { + case { Location: RequestLocation.Body }: + bodyParameter = operationParameter; + break; + case { Location: RequestLocation.Header, IsContentType: true } when contentTypeRequestParameter == null: + contentTypeRequestParameter = operationParameter; + break; + case { Location: RequestLocation.Header } when ConditionRequestHeader.TryGetValue(operationParameter.NameInRequest, out var header): + if (operationParameter.IsRequired) + { + throw new NotSupportedException("Required conditional request headers are not supported."); + } + + requestConditionHeaders |= header; + requestConditionRequestParameter ??= operationParameter; + requestConditionSerializationFormat = requestConditionSerializationFormat == SerializationFormat.Default + ? SerializationBuilder.GetSerializationFormat(operationParameter.Type) + : requestConditionSerializationFormat; + + break; + case { Location: RequestLocation.Uri or RequestLocation.Path }: + requiredPathParameters.Add(operationParameter.NameInRequest, operationParameter); + break; + case { IsApiVersion: true, Type: InputLiteralType { Value: not null } }: + case { IsApiVersion: true, DefaultValue: not null }: + optionalRequestParameters.Add(operationParameter); + break; + case { IsRequired: true }: + if (Operation.KeepClientDefaultValue && operationParameter.DefaultValue != null) + { + optionalRequestParameters.Add(operationParameter); + } + else + { + requiredRequestParameters.Add(operationParameter); + } + break; + default: + optionalRequestParameters.Add(operationParameter); + break; + } + } + + AddWaitForCompletion(); + AddUriOrPathParameters(Operation.Uri, requiredPathParameters); + AddUriOrPathParameters(Operation.Path, requiredPathParameters); + AddQueryOrHeaderParameters(requiredRequestParameters); + AddBody(bodyParameter, contentTypeRequestParameter); + AddUriOrPathParameters(Operation.Uri, optionalPathParameters); + AddUriOrPathParameters(Operation.Path, optionalPathParameters); + AddQueryOrHeaderParameters(optionalRequestParameters); + AddRequestConditionHeaders(requestConditionHeaders, requestConditionRequestParameter, requestConditionSerializationFormat); + AddRequestContext(); + } + + private void AddWaitForCompletion() + { + if (Operation.LongRunning != null) + { + _orderedParameters.Add(new ParameterChain(KnownParameters.WaitForCompletion, KnownParameters.WaitForCompletion, null, false)); + } + } + + private void AddUriOrPathParameters(string uriPart, IReadOnlyDictionary requestParameters) + { + foreach ((ReadOnlySpan span, bool isLiteral) in StringExtensions.GetPathParts(uriPart)) + { + if (isLiteral) + { + continue; + } + + var text = span.ToString(); + if (requestParameters.TryGetValue(text, out var requestParameter)) + { + AddParameter(text, requestParameter); + } + } + } + + private void AddQueryOrHeaderParameters(IEnumerable inputParameters) + { + foreach (var inputParameter in inputParameters) + { + AddParameter(inputParameter.NameInRequest, inputParameter); + } + } + + private void AddBody(InputParameter? bodyParameter, InputParameter? contentTypeRequestParameter) + { + if (bodyParameter == null) + { + return; + } + _protocolBodyParameter = bodyParameter.IsRequired + ? KnownParameters.RequestContent + : KnownParameters.RequestContentNullable; + _orderedParameters.Add(new ParameterChain(BuildParameter(bodyParameter), _protocolBodyParameter, _protocolBodyParameter, bodyParameter.Kind == InputOperationParameterKind.Spread)); + + if (contentTypeRequestParameter != null) + { + if (Operation.RequestMediaTypes != null && (Operation.RequestMediaTypes.Count > 1 || Operation.RequestMediaTypes.Contains("multipart/form-data"))) + { + AddContentTypeRequestParameter(contentTypeRequestParameter, Operation.RequestMediaTypes); + } + else + { + AddParameter(contentTypeRequestParameter, typeof(ContentType)); + } + } + } + + public void AddRequestConditionHeaders(RequestConditionHeaders conditionHeaderFlag, InputParameter? requestConditionRequestParameter, SerializationFormat serializationFormat) + { + if (conditionHeaderFlag == RequestConditionHeaders.None || requestConditionRequestParameter == null) + { + return; + } + + _conditionHeaderFlag = conditionHeaderFlag; + + switch (conditionHeaderFlag) + { + case RequestConditionHeaders.IfMatch | RequestConditionHeaders.IfNoneMatch: + _orderedParameters.Add(new ParameterChain(KnownParameters.MatchConditionsParameter, KnownParameters.MatchConditionsParameter, KnownParameters.MatchConditionsParameter)); + AddReference(KnownParameters.MatchConditionsParameter.Name, null, KnownParameters.MatchConditionsParameter, serializationFormat); + break; + case RequestConditionHeaders.IfMatch: + case RequestConditionHeaders.IfNoneMatch: + AddParameter(requestConditionRequestParameter, typeof(ETag)); + break; + default: + _orderedParameters.Add(new ParameterChain(KnownParameters.RequestConditionsParameter, KnownParameters.RequestConditionsParameter, KnownParameters.RequestConditionsParameter)); + AddReference(KnownParameters.RequestConditionsParameter.Name, null, KnownParameters.RequestConditionsParameter, serializationFormat); + break; + } + } + + public void AddRequestContext() + { + _orderedParameters.Add(new ParameterChain( + Configuration.IsBranded ? KnownParameters.CancellationTokenParameter : null, // in non-azure libraries, convenience method no longer takes a `CancellationToken` parameter. + ShouldRequestContextOptional() ? KnownParameters.RequestContext : KnownParameters.RequestContextRequired, + KnownParameters.RequestContext)); + } + + private void AddContentTypeRequestParameter(InputParameter operationParameter, IReadOnlyList requestMediaTypes) + { + var name = operationParameter.Name.ToVariableName(); + if (requestMediaTypes.Count == 1 && requestMediaTypes.Contains("multipart/form-data")) + { + var description = Parameter.CreateDescription(operationParameter, typeof(string), requestMediaTypes); + var parameter = new Parameter(name, description, typeof(string), null, ValidationType.None, null, RequestLocation: RequestLocation.Header); + _orderedParameters.Add(new ParameterChain(null, parameter, parameter)); + AddReference(operationParameter.NameInRequest, operationParameter, parameter, SerializationFormat.Default); + return; + } + else + { + var description = Parameter.CreateDescription(operationParameter, typeof(ContentType), requestMediaTypes); + var parameter = new Parameter(name, description, typeof(ContentType), null, ValidationType.None, null, RequestLocation: RequestLocation.Header); + _orderedParameters.Add(new ParameterChain(parameter, parameter, parameter)); + + AddReference(operationParameter.NameInRequest, operationParameter, parameter, SerializationFormat.Default); + } + } + + private void AddParameter(InputParameter operationParameter, CSharpType? frameworkParameterType = null) + => AddParameter(operationParameter.NameInRequest, operationParameter, frameworkParameterType); + + private void AddParameter(string name, InputParameter inputParameter, CSharpType? frameworkParameterType = null) + { + var protocolMethodParameter = BuildParameter(inputParameter, frameworkParameterType ?? ChangeTypeForProtocolMethod(inputParameter.Type)); + + AddReference(name, inputParameter, protocolMethodParameter, SerializationBuilder.GetSerializationFormat(inputParameter.Type)); + if (inputParameter.Kind is InputOperationParameterKind.Client or InputOperationParameterKind.Constant) + { + return; + } + + if (inputParameter.Kind == InputOperationParameterKind.Grouped) + { + _orderedParameters.Add(new ParameterChain(null, protocolMethodParameter, protocolMethodParameter)); + return; + } + + var convenienceMethodParameter = BuildParameter(inputParameter, frameworkParameterType); + var parameterChain = inputParameter.Location == RequestLocation.None + ? new ParameterChain(convenienceMethodParameter, null, null) + : new ParameterChain(convenienceMethodParameter, protocolMethodParameter, protocolMethodParameter); + + _orderedParameters.Add(parameterChain); + } + + private Parameter BuildParameter(in InputParameter operationParameter, CSharpType? typeOverride = null) + { + var type = typeOverride != null + ? typeOverride.WithNullable(operationParameter.Type is InputNullableType) + : _typeFactory.CreateType(operationParameter.Type); + + return Parameter.FromInputParameter(operationParameter, type, _typeFactory, Operation.KeepClientDefaultValue); + } + + private void AddReference(string nameInRequest, InputParameter? operationParameter, Parameter parameter, SerializationFormat serializationFormat) + { + var reference = operationParameter != null ? CreateReference(operationParameter, parameter) : parameter; + _requestParts.Add(new RequestPartSource(nameInRequest, operationParameter, reference, serializationFormat)); + } + + private ReferenceOrConstant CreateReference(InputParameter operationParameter, Parameter parameter) + { + if (operationParameter.Kind == InputOperationParameterKind.Client) + { + var field = operationParameter.IsEndpoint ? _fields.EndpointField : _fields.GetFieldByName(parameter.Name); + if (field == null) + { + throw new InvalidOperationException($"Parameter '{parameter.Name}' should have matching field"); + } + + return new Reference(field.Name, field.Type); + } + + if (operationParameter.Kind is InputOperationParameterKind.Constant && parameter.DefaultValue is not null) + { + return (ReferenceOrConstant)parameter.DefaultValue; + } + + return parameter; + } + + private CSharpType? ChangeTypeForProtocolMethod(InputType type) => type switch + { + InputEnumType enumType => _typeFactory.CreateType(enumType.ValueType), + InputModelType modelType => new CSharpType(typeof(object)), + InputNullableType nullableType => ChangeTypeForProtocolMethod(nullableType.Type) switch + { + null => null, + { } protocolType => protocolType.WithNullable(true), + }, + _ => null + }; + + private record ReturnTypeChain(CSharpType Convenience, CSharpType Protocol, CSharpType? ConvenienceResponseType); + + private record ParameterChain(Parameter? Convenience, Parameter? Protocol, Parameter? CreateMessage, bool IsSpreadParameter = false); + + internal record ConvenienceMethodGenerationInfo() + { + public bool IsConvenienceMethodGenerated { get; init; } = false; + + public bool IsConvenienceMethodInternal { get; init; } = false; + + public ConvenienceMethodOmittingMessage? Message { get; init; } + } + } +} diff --git a/logger/autorest.csharp/level/Output/ProtocolMethodPaging.cs b/logger/autorest.csharp/level/Output/ProtocolMethodPaging.cs new file mode 100644 index 0000000..24cbcb6 --- /dev/null +++ b/logger/autorest.csharp/level/Output/ProtocolMethodPaging.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Responses; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Output.Models +{ + internal record ProtocolMethodPaging(RestClientMethod? NextPageMethod, string? NextLinkName, string ItemName); +} diff --git a/logger/autorest.csharp/level/Output/Samples/DpgClientSampleProvider.cs b/logger/autorest.csharp/level/Output/Samples/DpgClientSampleProvider.cs new file mode 100644 index 0000000..68e3234 --- /dev/null +++ b/logger/autorest.csharp/level/Output/Samples/DpgClientSampleProvider.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Samples.Models; +using AutoRest.CSharp.Utilities; +using NUnit.Framework; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.LowLevel.Output.Samples +{ + internal class DpgClientSampleProvider : DpgClientTestProvider + { + public DpgClientSampleProvider(string defaultNamespace, LowLevelClient client, SourceInputModel? sourceInputModel) : base($"{defaultNamespace}.Samples", $"Samples_{client.Declaration.Name}", client, sourceInputModel) + { + _samples = client.ClientMethods.SelectMany(m => m.Samples); + } + + protected override IEnumerable BuildUsings() + { + if (Configuration.IsBranded) + yield return "Azure.Identity"; // we need this using because we might need to call `new DefaultAzureCredential` from `Azure.Identity` package, but Azure.Identity package is not a dependency of the generator project. + } + + private readonly IEnumerable _samples; + private Dictionary>? methodToSampleDict; + private Dictionary> MethodToSampleDict => methodToSampleDict ??= BuildMethodToSampleCache(); + + private Dictionary> BuildMethodToSampleCache() + { + var result = new Dictionary>(); + foreach (var sample in _samples) + { + result.AddInList(sample.OperationMethodSignature.WithAsync(false), sample); + result.AddInList(sample.OperationMethodSignature.WithAsync(true), sample); + } + + return result; + } + + public IEnumerable<(string ExampleInformation, string TestMethodName)> GetSampleInformation(MethodSignature signature, bool isAsync) + { + if (MethodToSampleDict.TryGetValue(signature, out var result)) + { + foreach (var sample in result) + { + yield return (sample.GetSampleInformation(isAsync), GetMethodName(sample, isAsync)); + } + } + } + + protected override IEnumerable BuildMethods() + { + foreach (var sample in _client.ClientMethods.SelectMany(m => m.Samples)) + { + yield return BuildSampleMethod(sample, false); + yield return BuildSampleMethod(sample, true); + } + } + + protected override string GetMethodName(DpgOperationSample sample, bool isAsync) + { + var builder = new StringBuilder("Example_"); + + if (sample.ResourceName is not null) + builder.Append(sample.ResourceName).Append("_"); + + builder.Append(sample.InputOperationName) + .Append('_').Append(sample.ExampleKey); + + if (sample.IsConvenienceSample) + { + builder.Append("_Convenience"); + } + if (isAsync) + { + builder.Append("_Async"); + } + return builder.ToString(); + } + + protected override CSharpAttribute[] GetMethodAttributes() => _attributes; + + private readonly CSharpAttribute[] _attributes = new[] { new CSharpAttribute(typeof(TestAttribute)), new CSharpAttribute(typeof(IgnoreAttribute), Literal("Only validating compilation of examples")) }; + + protected override IEnumerable BuildResponseStatements(DpgOperationSample sample, VariableReference resultVar) + { + if (sample.IsResponseStream) + { + return BuildResponseForStream(resultVar); + } + else + { + return BuildNormalResponse(sample, resultVar); + } + } + + private IEnumerable BuildResponseForStream(VariableReference resultVar) + { + var contentStreamExpression = new StreamExpression(resultVar.Property(Configuration.ApiTypes.ContentStreamName)); + yield return new IfStatement(NotEqual(contentStreamExpression, Null)) + { + UsingDeclare("outFileStream", InvokeFileOpenWrite(""), out var streamVariable), + contentStreamExpression.CopyTo(streamVariable) + }; + } + + private IEnumerable BuildNormalResponse(DpgOperationSample sample, VariableReference responseVar) + { + // we do not write response handling for convenience method samples + if (sample.IsConvenienceSample) + yield break; + + ValueExpression streamVar; + var responseType = responseVar.Type; + if (responseType.EqualsIgnoreNullable(typeof(BinaryData))) + streamVar = responseVar.Invoke(nameof(BinaryData.ToStream)); + else if (responseType.EqualsIgnoreNullable(Configuration.ApiTypes.ResponseType)) + streamVar = responseVar.Property(Configuration.ApiTypes.ContentStreamName); + else if (responseType.IsResponseOfT) + streamVar = responseVar.Invoke(Configuration.ApiTypes.GetRawResponseName); + else + yield break; + + if (sample.ResultType != null) + { + var resultVar = new VariableReference(typeof(JsonElement), Configuration.ApiTypes.JsonElementVariableName); + yield return Declare(resultVar, JsonDocumentExpression.Parse(new StreamExpression(streamVar)).RootElement); + + var responseParsingStatements = new List(); + BuildResponseParseStatements(sample.IsAllParametersUsed, sample.ResultType, resultVar, responseParsingStatements, new HashSet()); + + yield return responseParsingStatements; + } + else + { + // if there is not a schema for us to show, just print status code + ValueExpression statusVar = responseVar; + if (responseType.IsResponseOfT) + statusVar = responseVar.Invoke(Configuration.ApiTypes.GetRawResponseName); + if (responseType.IsResponseOfT || responseType.IsResponse) + yield return InvokeConsoleWriteLine(statusVar.Property(Configuration.ApiTypes.StatusName)); + } + } + + private static void BuildResponseParseStatements(bool useAllProperties, InputType type, ValueExpression invocation, List statements, HashSet visitedTypes) + { + switch (type) + { + case InputListType listType: + if (visitedTypes.Contains(listType.ValueType)) + return; + // [0] + invocation = new IndexerExpression(invocation, Literal(0)); + BuildResponseParseStatements(useAllProperties, listType.ValueType, invocation, statements, visitedTypes); + return; + case InputDictionaryType dictionaryType: + if (visitedTypes.Contains(dictionaryType.ValueType)) + return; + // .GetProperty("") + invocation = invocation.Invoke("GetProperty", Literal("")); + BuildResponseParseStatements(useAllProperties, dictionaryType.ValueType, invocation, statements, visitedTypes); + return; + case InputNullableType nullableType: + if (visitedTypes.Contains(nullableType.Type)) + return; + BuildResponseParseStatements(useAllProperties, nullableType.Type, invocation, statements, visitedTypes); + return; + case InputModelType modelType: + BuildResponseParseStatementsForModelType(useAllProperties, modelType, invocation, statements, visitedTypes); + return; + } + // we get primitive types, return the statement + var statement = InvokeConsoleWriteLine(invocation.InvokeToString()); + statements.Add(statement); + } + + private static void BuildResponseParseStatementsForModelType(bool useAllProperties, InputModelType model, ValueExpression invocation, List statements, HashSet visitedTypes) + { + var allProperties = model.GetSelfAndBaseModels().SelectMany(m => m.Properties); + var propertiesToExplore = useAllProperties + ? allProperties + : allProperties.Where(p => p.IsRequired); + + if (!propertiesToExplore.Any()) // if you have a required property, but its child properties are all optional + { + // return the object + statements.Add(InvokeConsoleWriteLine(invocation.InvokeToString())); + return; + } + + foreach (var property in propertiesToExplore) + { + if (!visitedTypes.Contains(property.Type)) + { + // .GetProperty(""); + InputType type = property.Type.GetImplementType(); + visitedTypes.Add(type); + var next = invocation.Invoke("GetProperty", Literal(property.SerializedName)); + BuildResponseParseStatements(useAllProperties, type, next, statements, visitedTypes); + visitedTypes.Remove(type); + } + } + } + } +} diff --git a/logger/autorest.csharp/level/Output/Samples/DpgOperationSample.cs b/logger/autorest.csharp/level/Output/Samples/DpgOperationSample.cs new file mode 100644 index 0000000..af19619 --- /dev/null +++ b/logger/autorest.csharp/level/Output/Samples/DpgOperationSample.cs @@ -0,0 +1,481 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.LowLevel.Extensions; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Output.Samples.Models +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + internal class DpgOperationSample + { + public DpgOperationSample(LowLevelClient client, TypeFactory typeFactory, LowLevelClientMethod method, InputOperation inputOperation, InputOperationExample operationExample, bool isConvenienceSample, string exampleKey) + { + _client = client; + _typeFactory = typeFactory; + _method = method; + _inputOperation = inputOperation; + _inputOperationExample = operationExample; + IsConvenienceSample = isConvenienceSample; + ExampleKey = exampleKey.ToCleanName(); + IsAllParametersUsed = exampleKey == ExampleMockValueBuilder.MockExampleAllParameterKey; // TODO -- only work around for the response usage building. + _operationMethodSignature = isConvenienceSample ? method.ConvenienceMethod!.Signature : method.ProtocolMethodSignature; + } + + protected internal readonly InputOperation _inputOperation; + protected internal readonly InputOperationExample _inputOperationExample; + protected internal readonly LowLevelClient _client; + protected internal readonly LowLevelClientMethod _method; + private readonly TypeFactory _typeFactory; + private readonly MethodSignature _operationMethodSignature; + public bool IsAllParametersUsed { get; } + public string ExampleKey { get; } + public bool IsConvenienceSample { get; } + public string? ResourceName => _inputOperation.ResourceName; + public string InputOperationName => _inputOperation.Name; + + public MethodSignature OperationMethodSignature => _operationMethodSignature; + + public bool IsLongRunning => IsConvenienceSample ? _method.ConvenienceMethod!.IsLongRunning : _method.LongRunning != null; + + public bool IsPageable => IsConvenienceSample ? _method.ConvenienceMethod!.IsPageable : _method.PagingInfo != null; + + private IReadOnlyList? _clientInvocationChain; + public IReadOnlyList ClientInvocationChain => _clientInvocationChain ??= GetClientInvocationChain(); + + /// + /// Get the methods to be called to get the client, it should be like `Client(...).GetXXClient(..).GetYYClient(..)`. + /// It's composed of a constructor of non-subclient and a optional list of subclient factory methods. + /// + /// + private IReadOnlyList GetClientInvocationChain() + { + var client = _client; + var callChain = new Stack(); + while (client.FactoryMethod != null) + { + callChain.Push(client.FactoryMethod.Signature); + if (client.ParentClient == null) + { + break; + } + + client = client.ParentClient; + } + callChain.Push(client.GetEffectiveCtor()!); + + return callChain.ToArray(); + } + + public IEnumerable GetValueExpressionsForParameters(IEnumerable parameters, List variableDeclarationStatements) + { + foreach (var parameter in parameters) + { + ValueExpression parameterExpression; + if (ParameterValueMapping.TryGetValue(parameter.Name, out var exampleValue)) + { + // if we could get an example value out of the map, we just use it. + parameterExpression = ExampleValueSnippets.GetExpression(exampleValue, parameter.SerializationFormat); + } + else + { + // if we cannot get an example value out of the map, we should skip it, unless it is required + // in the required case, we should return the default value of the type. + // but we should not abuse `default` because it might cause ambiguous calls which leads to compilation errors + if (parameter.IsOptionalInSignature) + continue; + + parameterExpression = parameter.Type.IsValueType && !parameter.Type.IsNullable ? Default.CastTo(parameter.Type) : Null.CastTo(parameter.Type); + } + if (IsInlineParameter(parameter)) + { + yield return parameter.IsOptionalInSignature ? new PositionalParameterReference(parameter.Name, parameterExpression) : parameterExpression; + } + else + { + // when it is not inline parameter, we add the declaration of the parameter into the statements, and returns the parameter name reference + var parameterReference = new VariableReference(parameter.Type, parameter.Name); + var declaration = NeedsDispose(parameter) ? UsingDeclare(parameterReference, parameterExpression) : Declare(parameterReference, parameterExpression); + variableDeclarationStatements.Add(declaration); + yield return parameter.IsOptionalInSignature ? new PositionalParameterReference(parameter.Name, parameterReference) : parameterReference; // returns the parameter name reference + } + } + } + + private Dictionary? _parameterValueMapping; + public Dictionary ParameterValueMapping => _parameterValueMapping ??= EnsureParameterValueMapping(); + + private Dictionary EnsureParameterValueMapping() + { + var result = new Dictionary(); + var parameters = GetAllParameters(); + var parameterExamples = GetAllParameterExamples(); + + foreach (var parameter in parameters) + { + if (ProcessKnownParameters(result, parameter)) + continue; + // find the corresponding input parameter + var exampleValue = FindExampleValueBySerializedName(parameterExamples, parameter.Name); + + if (exampleValue == null) + { + // if this is a required parameter and we did not find the corresponding parameter in the examples, we put the null + if (!parameter.IsOptionalInSignature) + { + ValueExpression parameterExpression = parameter.Type.IsValueType && !parameter.Type.IsNullable ? Default.CastTo(parameter.Type) : Null.CastTo(parameter.Type); + result.Add(parameter.Name, new InputExampleParameterValue(parameter, parameterExpression)); + } + // if it is optional, we just do not put it in the map indicates that in the invocation we could omit it + } + else + { + // add it into the mapping + result.Add(parameter.Name, new InputExampleParameterValue(parameter, exampleValue)); + } + } + + return result; + } + + /// + /// Returns all the parameters that should be used in this sample + /// Only required parameters on this operation will be included if useAllParameters is false + /// Includes all parameters if useAllParameters is true + /// + /// + private IEnumerable GetAllParameters() + { + // here we should gather all the parameters from my client, and my parent client, and the parent client of my parent client, etc + foreach (var method in ClientInvocationChain) + { + foreach (var parameter in method.Parameters) + yield return parameter; + } + // then we return all the parameters on this operation + var parameters = IsAllParametersUsed ? + _operationMethodSignature.Parameters : + _operationMethodSignature.Parameters.Where(p => !p.IsOptionalInSignature); + foreach (var parameter in parameters) + yield return parameter; + } + + /// + /// This method returns all the related parameter examples on this particular method. For examples whose parameters + /// are of type InputModelType, we will try to find the corresponding type and use the CSharpType name instead of the parameter name. + /// + /// + /// A list of parameter examples. + /// + private IEnumerable GetAllParameterExamples() + { + foreach (var parameter in _inputOperationExample.Parameters) + { + var inputParameter = parameter.Parameter; + var inputParameterName = inputParameter.Name; + + if (inputParameter.Type is InputModelType model && inputParameterName.Equals(inputParameter.Type.Name)) + { + var type = _typeFactory.CreateType(model); + if (type != null) + { + yield return parameter with { Parameter = inputParameter with { Name = type.Name } }; + } + } + + yield return parameter; + } + } + + private bool ProcessKnownParameters(Dictionary result, Parameter parameter) + { + if (parameter == KnownParameters.WaitForCompletion) + { + result.Add(parameter.Name, new InputExampleParameterValue(parameter, new TypeReference(typeof(WaitUntil)).Property(nameof(WaitUntil.Completed)))); + return true; + } + + if (parameter == KnownParameters.CancellationTokenParameter) + { + // we usually do not set this parameter in generated test cases + return true; + } + + if (parameter == KnownParameters.RequestContextRequired) + { + // we need the RequestContext to disambiguiate from the convenience method - but passing in a null value is allowed. + result.Add(parameter.Name, new InputExampleParameterValue(parameter, Null.CastTo(parameter.Type))); + return true; + } + + // endpoint we kind of will change its description therefore here we only find it for name and type + if (IsSameParameter(parameter, KnownParameters.Endpoint)) + { + result.Add(parameter.Name, new InputExampleParameterValue(parameter, GetEndpointValue(parameter.Name))); + return true; + } + + // request content is also special + if (IsSameParameter(parameter, KnownParameters.RequestContent) || IsSameParameter(parameter, KnownParameters.RequestContentNullable)) + { + result.Add(parameter.Name, new InputExampleParameterValue(parameter, GetBodyParameterValue())); + return true; + } + + if (IsSameParameter(parameter, KnownParameters.RequestConditionsParameter) || IsSameParameter(parameter, KnownParameters.MatchConditionsParameter)) + { + // temporarily just return null value + result.Add(parameter.Name, new InputExampleParameterValue(parameter, Null.CastTo(parameter.Type))); + return true; + } + + // handle credentials + if (parameter.Type.EqualsIgnoreNullable(KnownParameters.KeyAuth.Type)) + { + result.Add(parameter.Name, new InputExampleParameterValue(parameter, New.Instance(Configuration.ApiTypes.KeyCredentialType, Configuration.ApiTypes.GetKeySampleExpression(_client.TopLevelClient.Type.Name)))); + return true; + } + + if (parameter.Type.EqualsIgnoreNullable(KnownParameters.TokenAuth.Type)) + { + result.Add(parameter.Name, new InputExampleParameterValue(parameter, new FormattableStringToExpression($"new DefaultAzureCredential()"))); // TODO -- we have to workaround here because we do not have the Azure.Identity dependency here + return true; + } + + return false; + } + + protected InputExampleValue? FindExampleValueBySerializedName(IEnumerable parameterExamples, string name) + { + foreach (var parameterExample in parameterExamples) + { + var parameter = parameterExample.Parameter; + // TODO -- we might need to refactor this when we finally separate protocol method and convenience method from the LowLevelClientMethod class + if (parameter.Kind == InputOperationParameterKind.Spread) + { + // when it is a spread parameter, it should always be InputModelType + var modelType = parameter.Type as InputModelType; + var objectExampleValue = parameterExample.ExampleValue as InputExampleObjectValue; + Debug.Assert(modelType != null); + var values = objectExampleValue?.Values ?? new Dictionary(); + + foreach (var modelOrBase in modelType.GetSelfAndBaseModels()) + { + foreach (var property in modelOrBase.Properties) + { + if (property.Name.ToVariableName() == name) + { + return values.TryGetValue(property.SerializedName, out var exampleValue) ? exampleValue : null; + } + } + } + } + else + { + if (parameter.Name.ToVariableName() == name) + { + return parameterExample.ExampleValue; + } + } + } + return null; + } + + public InputExampleValue GetEndpointValue(string parameterName) + { + var operationParameterValue = _inputOperationExample.Parameters.FirstOrDefault(e => e.Parameter.IsEndpoint)?.ExampleValue; + if (operationParameterValue != null) + return operationParameterValue; + + // sometimes, especially in swagger projects, the parameter used as endpoint in our client, does not have the `IsEndpoint` flag, we have to fallback here so that we could at least have a value for it. + return InputExampleValue.Value(InputPrimitiveType.String, $"<{parameterName}>"); + } + + private bool NeedsDispose(Parameter parameter) + { + if (IsSameParameter(parameter, KnownParameters.RequestContent) || IsSameParameter(parameter, KnownParameters.RequestContentNullable)) + return true; + + return false; + } + + private bool IsInlineParameter(Parameter parameter) + { + if (IsSameParameter(parameter, KnownParameters.RequestContent) || IsSameParameter(parameter, KnownParameters.RequestContentNullable)) + return false; + + if (IsSameParameter(parameter, KnownParameters.Endpoint)) + return false; + + if (parameter.Type.EqualsIgnoreNullable(KnownParameters.KeyAuth.Type)) + return false; + + if (parameter.Type.EqualsIgnoreNullable(KnownParameters.TokenAuth.Type)) + return false; + + if (parameter.Type is { IsFrameworkType: false, Implementation: ObjectType }) + return false; + + return true; + } + + private InputExampleValue GetBodyParameterValue() + { + // we have a request body type + if (_method.RequestBodyType == null) + return InputExampleValue.Null(InputPrimitiveType.Unknown); + + //if (Method.RequestBodyType is InputPrimitiveType { Kind: InputTypeKind.Stream }) + // return InputExampleValue.Stream(Method.RequestBodyType, ""); + + // find the example value for this type + // if there is only one parameter is body parameter, we return it. + var bodyParameters = _inputOperationExample.Parameters.Where(e => e.Parameter is { Location: RequestLocation.Body }).ToArray(); + if (bodyParameters.Length == 1) + { + return bodyParameters.Single().ExampleValue; + } + // there could be multiple body parameters especially when we have a multiform content type operation + // if we have more than one body parameters which should happen very rarely, we just search the type in all parameters we have and get the first one that matches. + var bodyParameterExample = _inputOperationExample.Parameters.FirstOrDefault(e => e.Parameter.Type == _method.RequestBodyType); + if (bodyParameterExample != null) + { + return bodyParameterExample.ExampleValue; + } + + return InputExampleValue.Null(_method.RequestBodyType); + } + + private static bool IsSameParameter(Parameter parameter, Parameter knownParameter) + => parameter.Name == knownParameter.Name && parameter.Type.EqualsIgnoreNullable(knownParameter.Type); + + public bool HasResponseBody => _method.ResponseBodyType != null; + public bool IsResponseStream => _method.ResponseBodyType is InputPrimitiveType { Kind: InputPrimitiveTypeKind.Stream }; + + private InputType? _resultType; + public InputType? ResultType => _resultType ??= GetEffectiveResponseType(); + + /// + /// This method returns the Type we would like to deal with in the sample code. + /// For normal operation and long running operation, it is just the InputType of the response + /// For pageable operation, it is the InputType of the item + /// + /// + private InputType? GetEffectiveResponseType() + { + var responseType = _method.ResponseBodyType; + if (_method.PagingInfo == null) + return responseType; + + var pagingItemName = _method.PagingInfo.ItemName; + var listResultType = responseType as InputModelType; + var itemsArrayProperty = listResultType?.Properties.FirstOrDefault(p => p.SerializedName == pagingItemName && p.Type is InputListType); + return (itemsArrayProperty?.Type as InputListType)?.ValueType; + } + + // TODO -- this needs a refactor when we consolidate things around customization code https://github.com/Azure/autorest.csharp/issues/3370 + public static bool ShouldGenerateShortVersion(LowLevelClient client, LowLevelClientMethod method) + { + if (method.ConvenienceMethod is not null) + { + if (method.ConvenienceMethod.Signature.Parameters.Count == method.ProtocolMethodSignature.Parameters.Count - 1 && + method.ConvenienceMethod.Signature.Parameters.Count > 0 && + !method.ConvenienceMethod.Signature.Parameters.Last().Type.Equals(typeof(CancellationToken))) + { + bool allEqual = true; + for (int i = 0; i < method.ConvenienceMethod.Signature.Parameters.Count; i++) + { + if (!method.ConvenienceMethod.Signature.Parameters[i].Type.Equals(method.ProtocolMethodSignature.Parameters[i].Type)) + { + allEqual = false; + break; + } + } + if (allEqual) + { + return false; + } + } + } + else + { + if (client.HasMatchingCustomMethod(method)) + return false; + } + + return true; + } + + public static bool ShouldGenerateSample(LowLevelClient client, MethodSignature protocolMethodSignature) + { + return protocolMethodSignature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && + !protocolMethodSignature.Attributes.Any(a => a.Type.Equals(typeof(ObsoleteAttribute))) && + !client.IsMethodSuppressed(protocolMethodSignature) && + (client.IsSubClient ? true : client.GetEffectiveCtor() is not null); + } + + public string GetSampleInformation(bool isAsync) => IsConvenienceSample + ? GetSampleInformationForConvenience(_method.ConvenienceMethod!.Signature.WithAsync(isAsync)) + : GetSampleInformationForProtocol(_method.ProtocolMethodSignature.WithAsync(isAsync)); + + private string GetSampleInformationForConvenience(MethodSignature methodSignature) + { + var methodName = methodSignature.Name; + if (IsAllParametersUsed) + { + return $"This sample shows how to call {methodName} with all parameters."; + } + + return $"This sample shows how to call {methodName}."; + } + + private string GetSampleInformationForProtocol(MethodSignature methodSignature) + { + var methodName = methodSignature.Name; + if (IsAllParametersUsed) + { + return $"This sample shows how to call {methodName} with all {GenerateParameterAndRequestContentDescription(methodSignature.Parameters)}{(HasResponseBody ? " and parse the result" : "")}."; + } + + return $"This sample shows how to call {methodName}{(HasResponseBody ? " and parse the result" : string.Empty)}."; + } + + // RequestContext is excluded + private static bool HasNonBodyCustomParameter(IReadOnlyList parameters) + => parameters.Any(p => p.RequestLocation != RequestLocation.Body && !p.Equals(KnownParameters.RequestContext)); + + private string GenerateParameterAndRequestContentDescription(IReadOnlyList parameters) + { + var hasNonBodyParameter = HasNonBodyCustomParameter(parameters); + var hasBodyParameter = parameters.Any(p => p.RequestLocation == RequestLocation.Body); + + if (hasNonBodyParameter) + { + if (hasBodyParameter) + { + return "parameters and request content"; + } + return "parameters"; + } + return "request content"; + } + + private string GetDebuggerDisplay() + => $"Sample (Client: {_client.Type}, Method: {_operationMethodSignature.GetCRef()})"; + } +} diff --git a/logger/autorest.csharp/level/Output/Samples/InputExampleParameterValue.cs b/logger/autorest.csharp/level/Output/Samples/InputExampleParameterValue.cs new file mode 100644 index 0000000..a39fe8e --- /dev/null +++ b/logger/autorest.csharp/level/Output/Samples/InputExampleParameterValue.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using AutoRest.CSharp.Common.Input.Examples; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Output.Samples.Models +{ + /// + /// A represents a value for a parameter, which could either be a , or a as a literal + /// + internal record InputExampleParameterValue + { + public string Name { get; } + + public CSharpType Type { get; } + + public InputExampleValue? Value { get; } + + public ValueExpression? Expression { get; } + + private InputExampleParameterValue(string name, CSharpType type) + { + Name = name; + Type = type; + } + + public InputExampleParameterValue(Reference reference, InputExampleValue value) : this(reference.Name, reference.Type) + { + Value = value; + } + + public InputExampleParameterValue(Reference reference, ValueExpression expression) : this(reference.Name, reference.Type) + { + Expression = expression; + } + + public InputExampleParameterValue(Parameter parameter, InputExampleValue value) : this(parameter.Name, parameter.Type) + { + Value = value; + } + + public InputExampleParameterValue(Parameter parameter, ValueExpression expression) : this(parameter.Name, parameter.Type) + { + Expression = expression; + } + } +} diff --git a/logger/autorest.csharp/level/Output/Tests/DpgClientRecordedTestProvider.cs b/logger/autorest.csharp/level/Output/Tests/DpgClientRecordedTestProvider.cs new file mode 100644 index 0000000..0bc0ef7 --- /dev/null +++ b/logger/autorest.csharp/level/Output/Tests/DpgClientRecordedTestProvider.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Samples.Models; +using NUnit.Framework; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.LowLevel.Output.Tests +{ + internal class DpgClientRecordedTestProvider : DpgClientTestProvider + { + private static readonly Parameter IsAsyncParameter = new("isAsync", null, typeof(bool), null, ValidationType.None, null); + + private readonly DpgTestBaseProvider _testBaseProvider; + + public DpgClientRecordedTestProvider(string defaultNamespace, LowLevelClient client, DpgTestBaseProvider testBase, SourceInputModel? sourceInputModel) : base($"{defaultNamespace}.Tests", $"{client.Declaration.Name}Tests", client, sourceInputModel) + { + _testBaseProvider = testBase; + Inherits = _testBaseProvider.Type; + } + + protected override IEnumerable BuildUsings() + { + if (Configuration.IsBranded) + yield return "Azure.Identity"; // we need this using because we might need to call `new DefaultAzureCredential` from `Azure.Identity` package, but Azure.Identity package is not a dependency of the generator project. + } + + protected override IEnumerable BuildConstructors() + { + yield return new(new ConstructorSignature( + Type, + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] { IsAsyncParameter }, + Initializer: new ConstructorInitializer( + IsBase: true, + Arguments: new ValueExpression[] { IsAsyncParameter }) + ), + EmptyStatement); + } + + protected override IEnumerable BuildMethods() + { + foreach (var sample in _client.ClientMethods.SelectMany(m => m.Samples)) + { + yield return BuildSampleMethod(sample, true); + } + } + + protected override string GetMethodName(DpgOperationSample sample, bool isAsync) + { + var builder = new StringBuilder(); + + if (sample.ResourceName is not null) + builder.Append(sample.ResourceName).Append('_'); + + builder.Append(sample.InputOperationName) + .Append('_').Append(sample.ExampleKey); + + if (sample.IsConvenienceSample) + { + builder.Append("_Convenience"); + } + + // do not append Async here because the test method is always using the async version of the operation + + return builder.ToString(); + } + + protected override CSharpAttribute[] GetMethodAttributes() => _attributes; + + private static readonly CSharpAttribute[] _attributes = new[] { new CSharpAttribute(typeof(TestAttribute)), new CSharpAttribute(typeof(IgnoreAttribute), Literal("Please remove the Ignore attribute to let the test method run")) }; + + protected override MethodBodyStatement BuildGetClientStatement(DpgOperationSample sample, IReadOnlyList methodsToCall, List variableDeclarations, out VariableReference clientVar) + { + // change the first method in methodToCall to the factory method of that client + var firstMethod = methodsToCall[0]; + if (firstMethod is ConstructorSignature ctor) + { + firstMethod = _testBaseProvider.CreateClientMethods[ctor.Type].Signature; + } + var newMethodsToCall = methodsToCall.ToArray(); + newMethodsToCall[0] = firstMethod; + return base.BuildGetClientStatement(sample, newMethodsToCall, variableDeclarations, out clientVar); + } + + protected override IEnumerable BuildResponseStatements(DpgOperationSample sample, VariableReference resultVar) + { + // TODO -- for test methods, we need the response values to generate response validations + yield break; + } + } +} diff --git a/logger/autorest.csharp/level/Output/Tests/DpgTestBaseProvider.cs b/logger/autorest.csharp/level/Output/Tests/DpgTestBaseProvider.cs new file mode 100644 index 0000000..ef31502 --- /dev/null +++ b/logger/autorest.csharp/level/Output/Tests/DpgTestBaseProvider.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core.TestFramework; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.LowLevel.Output.Tests +{ + internal class DpgTestBaseProvider : ExpressionTypeProvider + { + private static readonly Parameter IsAsyncParameter = new("isAsync", null, typeof(bool), null, ValidationType.None, null); + + private readonly IEnumerable _clients; + + public DpgTestBaseProvider(string defaultNamespace, IEnumerable clients, DpgTestEnvironmentProvider dpgTestEnvironment, SourceInputModel? sourceInputModel) : base(defaultNamespace, sourceInputModel) + { + TestEnvironment = dpgTestEnvironment; + DefaultNamespace = $"{defaultNamespace}.Tests"; + DefaultName = $"{ClientBuilder.GetRPName(defaultNamespace)}TestBase"; + _clients = clients; + Inherits = new CSharpType(typeof(RecordedTestBase<>), TestEnvironment.Type); + DeclarationModifiers = TypeSignatureModifiers.Public | TypeSignatureModifiers.Partial; + } + + public DpgTestEnvironmentProvider TestEnvironment { get; } + + protected override string DefaultNamespace { get; } + + protected override string DefaultName { get; } + + protected override IEnumerable BuildConstructors() + { + yield return new(new ConstructorSignature( + Type: Type, + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public, + Parameters: new[] { IsAsyncParameter }, + Initializer: new ConstructorInitializer( + IsBase: true, + Arguments: new ValueExpression[] { IsAsyncParameter }) + ), + EmptyStatement); + } + + protected override IEnumerable BuildMethods() + { + foreach (var method in CreateClientMethods.Values) + { + yield return method; + } + } + + private Dictionary? _createClientMethods; + + public Dictionary CreateClientMethods => _createClientMethods ??= EnsureCreateClientMethods(); + + private Dictionary EnsureCreateClientMethods() + { + var result = new Dictionary(); + foreach (var client in _clients) + { + if (client.IsSubClient) + continue; + + var ctor = client.GetEffectiveCtor(includeClientOptions: true); + + if (ctor == null) + continue; + + var signature = new MethodSignature( + Name: $"Create{client.Type.Name}", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Protected, + ReturnType: client.Type, + ReturnDescription: null, + Parameters: ctor.Parameters.Where(p => !p.Type.EqualsIgnoreNullable(client.ClientOptions.Type)).ToArray() + ); + + var method = new Method(signature, BuildClientFactoryMethodBody(client, ctor).ToArray()); + + result.Add(client.Type, method); + } + + return result; + } + + private IEnumerable BuildClientFactoryMethodBody(LowLevelClient client, MethodSignatureBase signature) + { + var clientOptionType = client.ClientOptions.Type; + var optionsVar = new VariableReference(clientOptionType, "options"); + var newOptionsExpression = new InvokeStaticMethodExpression(null, "InstrumentClientOptions", new[] { New.Instance(clientOptionType) }); + + yield return Declare(optionsVar, newOptionsExpression); + + var clientVar = new VariableReference(client.Type, "client"); + var newClientArguments = signature.Parameters.Where(p => !p.Type.EqualsIgnoreNullable(clientOptionType)).Select(p => (ParameterReference)p).Append(optionsVar); + var newClientExpression = New.Instance(client.Type, newClientArguments.ToArray()); + + yield return Declare(clientVar, newClientExpression); + + yield return Return(new InvokeStaticMethodExpression(null, "InstrumentClient", new[] { clientVar })); + } + } +} diff --git a/logger/autorest.csharp/level/Output/Tests/DpgTestEnvironmentProvider.cs b/logger/autorest.csharp/level/Output/Tests/DpgTestEnvironmentProvider.cs new file mode 100644 index 0000000..dd46b48 --- /dev/null +++ b/logger/autorest.csharp/level/Output/Tests/DpgTestEnvironmentProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core.TestFramework; + +namespace AutoRest.CSharp.LowLevel.Output.Tests +{ + internal class DpgTestEnvironmentProvider : ExpressionTypeProvider + { + public DpgTestEnvironmentProvider(string defaultNamespace, SourceInputModel? sourceInputModel) : base(defaultNamespace, sourceInputModel) + { + DefaultNamespace = $"{defaultNamespace}.Tests"; + DefaultName = $"{ClientBuilder.GetRPName(defaultNamespace)}TestEnvironment"; + Inherits = typeof(TestEnvironment); + DeclarationModifiers = TypeSignatureModifiers.Public | TypeSignatureModifiers.Partial; + } + + protected override string DefaultNamespace { get; } + + protected override string DefaultName { get; } + } +} diff --git a/logger/autorest.csharp/level/Output/Tests/SmokeTestProvider.cs b/logger/autorest.csharp/level/Output/Tests/SmokeTestProvider.cs new file mode 100644 index 0000000..11381d0 --- /dev/null +++ b/logger/autorest.csharp/level/Output/Tests/SmokeTestProvider.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Input.Source; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Samples.Models; +using NUnit.Framework; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.LowLevel.Output.Tests +{ + internal class SmokeTestProvider : DpgClientTestProvider + { + public SmokeTestProvider(string defaultNamespace, LowLevelClient client, SourceInputModel? sourceInputModel) : base($"{defaultNamespace}.Tests", $"{client.Declaration.Name}Tests", client, sourceInputModel) + { + } + + protected override IEnumerable BuildUsings() + { + if (Configuration.IsBranded) + yield return "Azure.Identity"; // we need this using because we might need to call `new DefaultAzureCredential` from `Azure.Identity` package, but Azure.Identity package is not a dependency of the generator project. + } + + protected override IEnumerable BuildMethods() + { + // smoke test only have one method, here we only takes the first or nothing + var firstSample = _client.ClientMethods.SelectMany(m => m.Samples).FirstOrDefault(); + if (firstSample is not null) + { + yield return BuildSmokeTestMethod(firstSample); + } + } + + private Method BuildSmokeTestMethod(DpgOperationSample sample) + { + var signature = new MethodSignature( + Name: "SmokeTest", + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public, + ReturnType: null, + ReturnDescription: null, + Parameters: Array.Empty(), + Attributes: GetMethodAttributes()); + + return new Method(signature, BuildSomkeTestMethodBody(sample).ToArray()); + } + + private IEnumerable BuildSomkeTestMethodBody(DpgOperationSample sample) + { + var clientVariableStatements = new List(); + var newClientStatement = BuildGetClientStatement(sample, sample.ClientInvocationChain, clientVariableStatements, out var clientVar); + + yield return clientVariableStatements; + yield return newClientStatement; + + yield return Assertions.IsNotNull(clientVar); + } + + protected override string GetMethodName(DpgOperationSample sample, bool isAsync) + { + var builder = new StringBuilder(); + + if (sample.ResourceName is not null) + builder.Append(sample.ResourceName).Append('_'); + + builder.Append(sample.InputOperationName) + .Append('_').Append(sample.ExampleKey); + + if (sample.IsConvenienceSample) + { + builder.Append("_Convenience"); + } + + // do not append Async here because the test method is always using the async version of the operation + + return builder.ToString(); + } + + protected override CSharpAttribute[] GetMethodAttributes() => _attributes; + + private static readonly CSharpAttribute[] _attributes = new[] { new CSharpAttribute(typeof(TestAttribute)), new CSharpAttribute(typeof(IgnoreAttribute), Literal("Compilation test only")) }; + + protected override IEnumerable BuildResponseStatements(DpgOperationSample sample, VariableReference resultVar) + { + yield break; + } + } +} diff --git a/logger/autorest.csharp/mgmt/AutoRest/MgmtConfiguration.cs b/logger/autorest.csharp/mgmt/AutoRest/MgmtConfiguration.cs new file mode 100644 index 0000000..e4f8a08 --- /dev/null +++ b/logger/autorest.csharp/mgmt/AutoRest/MgmtConfiguration.cs @@ -0,0 +1,543 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; +using AutoRest.CSharp.AutoRest.Communication; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Report; + +namespace AutoRest.CSharp.Input +{ + public class MgmtConfiguration + { + public class MgmtDebugConfiguration + { + private const string MgmtDebugOptionsFormat = "mgmt-debug.{0}"; + + public bool SuppressListException { get; } + + public bool ShowSerializedNames { get; } + + public bool GenerateReport { get; } + + public bool ReportOnly { get; } + + public string ReportFormat { get; } + + public MgmtDebugConfiguration( + JsonElement? suppressListException = default, + JsonElement? showSerializedNames = default, + JsonElement? skipCodeGen = default, + JsonElement? generateReport = default, + JsonElement? reportOnly = default, + JsonElement? reportFormat = default + ) + { + SuppressListException = Configuration.DeserializeBoolean(suppressListException, false); + ShowSerializedNames = Configuration.DeserializeBoolean(showSerializedNames, false); + GenerateReport = Configuration.DeserializeBoolean(generateReport, true); + ReportOnly = Configuration.DeserializeBoolean(reportOnly, false); + ReportFormat = Configuration.IsValidJsonElement(reportFormat) ? reportFormat?.GetString() ?? "yaml" : "yaml"; + } + + internal static MgmtDebugConfiguration LoadConfiguration(JsonElement root) + { + if (root.ValueKind != JsonValueKind.Object) + return new MgmtDebugConfiguration(); + + root.TryGetProperty(nameof(SuppressListException), out var suppressListException); + root.TryGetProperty(nameof(ShowSerializedNames), out var showSerializedNames); + root.TryGetProperty(nameof(GenerateReport), out var generateReport); + root.TryGetProperty(nameof(ReportOnly), out var reportOnly); + root.TryGetProperty(nameof(ReportFormat), out var reportFormat); + + return new MgmtDebugConfiguration( + suppressListException: suppressListException, + showSerializedNames: showSerializedNames, + generateReport: generateReport, + reportOnly: reportOnly, + reportFormat: reportFormat + ); + } + + internal static MgmtDebugConfiguration GetConfiguration(IPluginCommunication autoRest) + { + return new MgmtDebugConfiguration( + suppressListException: autoRest.GetValue(string.Format(MgmtDebugOptionsFormat, "suppress-list-exception")).GetAwaiter().GetResult(), + showSerializedNames: autoRest.GetValue(string.Format(MgmtDebugOptionsFormat, "show-serialized-names")).GetAwaiter().GetResult(), + generateReport: autoRest.GetValue(string.Format(MgmtDebugOptionsFormat, "generate-report")).GetAwaiter().GetResult(), + reportOnly: autoRest.GetValue(string.Format(MgmtDebugOptionsFormat, "report-only")).GetAwaiter().GetResult(), + reportFormat: autoRest.GetValue(string.Format(MgmtDebugOptionsFormat, "report-format")).GetAwaiter().GetResult() + ); + } + + public void Write(Utf8JsonWriter writer, string settingName) + { + if (!SuppressListException && !ShowSerializedNames) + return; + + writer.WriteStartObject(settingName); + + if (SuppressListException) + writer.WriteBoolean(nameof(SuppressListException), SuppressListException); + + if (ShowSerializedNames) + writer.WriteBoolean(nameof(ShowSerializedNames), ShowSerializedNames); + + writer.WriteEndObject(); + } + } + + public record AcronymMappingTarget(string Value, string? ParameterValue = null, string? RawValue = null) + { + internal static AcronymMappingTarget Parse(string rawValue) + { + var spans = rawValue.AsSpan(); + var index = spans.IndexOf('|'); + if (index < 0) + return new AcronymMappingTarget(rawValue, null, rawValue); + + return new AcronymMappingTarget(spans.Slice(0, index).ToString(), spans.Slice(index, rawValue.Length - index).ToString(), rawValue); + } + } + + private static IReadOnlyDictionary ParseAcronymMapping(IReadOnlyDictionary acronymMapping) + => acronymMapping.ToDictionary(kv => kv.Key, kv => AcronymMappingTarget.Parse(kv.Value)); + + public MgmtConfiguration( + IReadOnlyList operationGroupsToOmit, + IReadOnlyList requestPathIsNonResource, + IReadOnlyList noPropertyTypeReplacement, + IReadOnlyList listException, + IReadOnlyList promptedEnumValues, + IReadOnlyList keepOrphanedModels, + IReadOnlyList keepPluralEnums, + IReadOnlyList keepPluralResourceData, + IReadOnlyList noResourceSuffix, + IReadOnlyList schemasToPrependRPPrefix, + IReadOnlyList generateArmResourceExtensions, + IReadOnlyList parameterizedScopes, + IReadOnlyList operationsToSkipLroApiVersionOverride, + MgmtDebugConfiguration mgmtDebug, + JsonElement? requestPathToParent = default, + JsonElement? requestPathToResourceName = default, + JsonElement? requestPathToResourceData = default, + JsonElement? requestPathToResourceType = default, + JsonElement? requestPathToScopeResourceTypes = default, + JsonElement? requestPathToSingletonResource = default, + JsonElement? overrideOperationName = default, + JsonElement? operationPositions = default, + JsonElement? acronymMapping = default, + JsonElement? renamePropertyBag = default, + JsonElement? formatByNameRules = default, + JsonElement? renameMapping = default, + JsonElement? parameterRenameMapping = default, + JsonElement? irregularPluralWords = default, + JsonElement? mergeOperations = default, + JsonElement? armCore = default, + JsonElement? resourceModelRequiresType = default, + JsonElement? resourceModelRequiresName = default, + JsonElement? singletonRequiresKeyword = default, + JsonElement? operationIdMappings = default, + JsonElement? updateRequiredCopy = default, + JsonElement? patchInitializerCustomization = default, + JsonElement? partialResources = default, + JsonElement? privilegedOperations = default, + JsonElement? operationsToLroApiVersionOverride = default) + { + MgmtReport.Instance.Reset(); + RequestPathToParent = DeserializeDictionary(requestPathToParent); + RequestPathToResourceName = DeserializeDictionary(requestPathToResourceName); + RequestPathToResourceData = DeserializeDictionary(requestPathToResourceData); + RequestPathToResourceType = DeserializeDictionary(requestPathToResourceType); + RequestPathToScopeResourceTypes = DeserializeDictionary(requestPathToScopeResourceTypes); + RequestPathToSingletonResource = DeserializeDictionary(requestPathToSingletonResource); + OverrideOperationName = DeserializeDictionary(overrideOperationName).AddToTransformerStore(TransformTypeName.OverrideOperationName, fromConfig: true); + RawAcronymMapping = DeserializeDictionary(acronymMapping).AddToTransformerStore(TransformTypeName.AcronymMapping, fromConfig: true); + RenamePropertyBag = DeserializeDictionary(renamePropertyBag); + FormatByNameRules = DeserializeDictionary(formatByNameRules).AddToTransformerStore(TransformTypeName.FormatByNameRules, fromConfig: true); + RenameMapping = DeserializeDictionary(renameMapping).AddToTransformerStore(TransformTypeName.RenameMapping, fromConfig: true); + ParameterRenameMapping = DeserializeDictionary>(parameterRenameMapping).AddToTransformerStore(TransformTypeName.ParameterRenameMapping,(arg) => arg.Value.Select(valueKV => new TransformItem(arg.Type, arg.Key, true /*fromConfig*/, valueKV.Key, valueKV.Value))); + IrregularPluralWords = DeserializeDictionary(irregularPluralWords); + PartialResources = DeserializeDictionary(partialResources); + try + { + OperationPositions = DeserializeDictionary(operationPositions); + } + catch (JsonException) + { + var operationPositionsStrDict = DeserializeDictionary(operationPositions); + OperationPositions = operationPositionsStrDict.ToDictionary(kv => kv.Key, kv => kv.Value.Split(";")); + } + MgmtDebug = mgmtDebug; + // TODO: A unified way to load from both readme and configuration.json + try + { + MergeOperations = DeserializeDictionary(mergeOperations); + } + catch (JsonException) + { + var mergeOperationsStrDict = DeserializeDictionary(mergeOperations); + MergeOperations = mergeOperationsStrDict.ToDictionary(kv => kv.Key, kv => kv.Value.Split(";")); + } + OperationGroupsToOmit = operationGroupsToOmit.AddToTransformerStore(Mgmt.Report.TransformTypeName.OperationGroupsToOmit, fromConfig: true); + RequestPathIsNonResource = requestPathIsNonResource.AddToTransformerStore(TransformTypeName.RequestPathIsNonResource, fromConfig: true); + NoPropertyTypeReplacement = noPropertyTypeReplacement.AddToTransformerStore(TransformTypeName.NoPropertyTypeReplacement, fromConfig: true); + ListException = listException; + PromptedEnumValues = promptedEnumValues; + KeepOrphanedModels = keepOrphanedModels; + KeepPluralEnums = keepPluralEnums.AddToTransformerStore(TransformTypeName.KeepPluralEnums, fromConfig: true); + KeepPluralResourceData = keepPluralResourceData.AddToTransformerStore(TransformTypeName.KeepPluralResourceData, fromConfig: true); + NoResourceSuffix = noResourceSuffix.AddToTransformerStore(TransformTypeName.NoResourceSuffix, fromConfig: true); + PrependRPPrefix = schemasToPrependRPPrefix.AddToTransformerStore(TransformTypeName.PrependRpPrefix, fromConfig: true); + GenerateArmResourceExtensions = generateArmResourceExtensions; + RawParameterizedScopes = parameterizedScopes; + OperationsToSkipLroApiVersionOverride = operationsToSkipLroApiVersionOverride; + IsArmCore = Configuration.DeserializeBoolean(armCore, false); + DoesResourceModelRequireType = Configuration.DeserializeBoolean(resourceModelRequiresType, true); + DoesResourceModelRequireName = Configuration.DeserializeBoolean(resourceModelRequiresName, true); + DoesSingletonRequiresKeyword = Configuration.DeserializeBoolean(singletonRequiresKeyword, false); + OperationIdMappings = DeserializeDictionary>(operationIdMappings); + UpdateRequiredCopy = DeserializeDictionary(updateRequiredCopy); + PatchInitializerCustomization = DeserializeDictionary>(patchInitializerCustomization); + PrivilegedOperations = DeserializeDictionary(privilegedOperations).AddToTransformerStore(Mgmt.Report.TransformTypeName.PrivilegedOperations, fromConfig: true); + OperationsToLroApiVersionOverride = DeserializeDictionary(operationsToLroApiVersionOverride); + } + + private static Dictionary DeserializeDictionary(JsonElement? jsonElement) where TKey : notnull + => !Configuration.IsValidJsonElement(jsonElement) ? new Dictionary() : JsonSerializer.Deserialize>(jsonElement.ToString()!)!; + + public MgmtDebugConfiguration MgmtDebug { get; } + /// + /// Will the resource model detection require type property? Defaults to true + /// + public bool DoesResourceModelRequireType { get; } + /// + /// Will the resource model detection require name property? Defaults to true + /// + public bool DoesResourceModelRequireName { get; } + /// + /// Will we only see the resource name to be in the dictionary to make a resource singleton? Defaults to false + /// + public bool DoesSingletonRequiresKeyword { get; } + public IReadOnlyDictionary RequestPathToParent { get; } + public IReadOnlyDictionary RequestPathToResourceName { get; } + public IReadOnlyDictionary RequestPathToResourceData { get; } + public IReadOnlyDictionary RequestPathToResourceType { get; } + public IReadOnlyDictionary RequestPathToSingletonResource { get; } + public IReadOnlyDictionary OverrideOperationName { get; } + public IReadOnlyDictionary RenamePropertyBag { get; } + private IReadOnlyDictionary RawAcronymMapping { get; } + private IReadOnlyDictionary? _acronymMapping; + public IReadOnlyDictionary AcronymMapping => _acronymMapping ??= ParseAcronymMapping(RawAcronymMapping); + public IReadOnlyDictionary FormatByNameRules { get; } + public IReadOnlyDictionary RenameMapping { get; } + public IReadOnlyDictionary> ParameterRenameMapping { get; } + public IReadOnlyDictionary IrregularPluralWords { get; } + public IReadOnlyDictionary RequestPathToScopeResourceTypes { get; } + public IReadOnlyDictionary OperationPositions { get; } + public IReadOnlyDictionary MergeOperations { get; } + public IReadOnlyDictionary PartialResources { get; } + public IReadOnlyList RawParameterizedScopes { get; } + private ImmutableHashSet? _parameterizedScopes; + internal ImmutableHashSet ParameterizedScopes + => _parameterizedScopes ??= RawParameterizedScopes.Select(scope => RequestPath.FromString(scope)).ToImmutableHashSet(); + public IReadOnlyList OperationGroupsToOmit { get; } + public IReadOnlyList RequestPathIsNonResource { get; } + public IReadOnlyList NoPropertyTypeReplacement { get; } + public IReadOnlyList ListException { get; } + public IReadOnlyList PromptedEnumValues { get; } + public IReadOnlyList KeepOrphanedModels { get; } + public IReadOnlyList KeepPluralEnums { get; } + public IReadOnlyList KeepPluralResourceData { get; } + public IReadOnlyList PrependRPPrefix { get; } + public IReadOnlyList OperationsToSkipLroApiVersionOverride { get; } + public IReadOnlyDictionary OperationsToLroApiVersionOverride { get; } + public IReadOnlyDictionary PrivilegedOperations { get; } + public IReadOnlyDictionary> OperationIdMappings { get; } + public IReadOnlyDictionary UpdateRequiredCopy { get; } + public IReadOnlyDictionary> PatchInitializerCustomization { get; } + + public IReadOnlyList NoResourceSuffix { get; } + public IReadOnlyList GenerateArmResourceExtensions { get; } + + public bool IsArmCore { get; } + + internal static MgmtConfiguration GetConfiguration(IPluginCommunication autoRest) + { + return new MgmtConfiguration( + operationGroupsToOmit: autoRest.GetValue(TransformTypeName.OperationGroupsToOmit).GetAwaiter().GetResult() ?? Array.Empty(), + requestPathIsNonResource: autoRest.GetValue(TransformTypeName.RequestPathIsNonResource).GetAwaiter().GetResult() ?? Array.Empty(), + noPropertyTypeReplacement: autoRest.GetValue(TransformTypeName.NoPropertyTypeReplacement).GetAwaiter().GetResult() ?? Array.Empty(), + listException: autoRest.GetValue("list-exception").GetAwaiter().GetResult() ?? Array.Empty(), + promptedEnumValues: autoRest.GetValue("prompted-enum-values").GetAwaiter().GetResult() ?? Array.Empty(), + keepOrphanedModels: autoRest.GetValue("keep-orphaned-models").GetAwaiter().GetResult() ?? Array.Empty(), + keepPluralEnums: autoRest.GetValue(TransformTypeName.KeepPluralEnums).GetAwaiter().GetResult() ?? Array.Empty(), + keepPluralResourceData: autoRest.GetValue(TransformTypeName.KeepPluralResourceData).GetAwaiter().GetResult() ?? Array.Empty(), + noResourceSuffix: autoRest.GetValue(TransformTypeName.NoResourceSuffix).GetAwaiter().GetResult() ?? Array.Empty(), + schemasToPrependRPPrefix: autoRest.GetValue(TransformTypeName.PrependRpPrefix).GetAwaiter().GetResult() ?? Array.Empty(), + generateArmResourceExtensions: autoRest.GetValue("generate-arm-resource-extensions").GetAwaiter().GetResult() ?? Array.Empty(), + parameterizedScopes: autoRest.GetValue("parameterized-scopes").GetAwaiter().GetResult() ?? Array.Empty(), + operationsToSkipLroApiVersionOverride: autoRest.GetValue("operations-to-skip-lro-api-version-override").GetAwaiter().GetResult() ?? Array.Empty(), + mgmtDebug: MgmtDebugConfiguration.GetConfiguration(autoRest), + requestPathToParent: autoRest.GetValue("request-path-to-parent").GetAwaiter().GetResult(), + requestPathToResourceName: autoRest.GetValue("request-path-to-resource-name").GetAwaiter().GetResult(), + requestPathToResourceData: autoRest.GetValue("request-path-to-resource-data").GetAwaiter().GetResult(), + requestPathToResourceType: autoRest.GetValue("request-path-to-resource-type").GetAwaiter().GetResult(), + requestPathToScopeResourceTypes: autoRest.GetValue("request-path-to-scope-resource-types").GetAwaiter().GetResult(), + operationPositions: autoRest.GetValue("operation-positions").GetAwaiter().GetResult(), + requestPathToSingletonResource: autoRest.GetValue("request-path-to-singleton-resource").GetAwaiter().GetResult(), + overrideOperationName: autoRest.GetValue(TransformTypeName.OverrideOperationName).GetAwaiter().GetResult(), + acronymMapping: GetAcronymMappingConfig(autoRest), + renamePropertyBag: autoRest.GetValue("rename-property-bag").GetAwaiter().GetResult(), + formatByNameRules: autoRest.GetValue(TransformTypeName.FormatByNameRules).GetAwaiter().GetResult(), + renameMapping: autoRest.GetValue(TransformTypeName.RenameMapping).GetAwaiter().GetResult(), + parameterRenameMapping: autoRest.GetValue(TransformTypeName.ParameterRenameMapping).GetAwaiter().GetResult(), + irregularPluralWords: autoRest.GetValue("irregular-plural-words").GetAwaiter().GetResult(), + mergeOperations: autoRest.GetValue("merge-operations").GetAwaiter().GetResult(), + armCore: autoRest.GetValue("arm-core").GetAwaiter().GetResult(), + resourceModelRequiresType: autoRest.GetValue("resource-model-requires-type").GetAwaiter().GetResult(), + resourceModelRequiresName: autoRest.GetValue("resource-model-requires-name").GetAwaiter().GetResult(), + singletonRequiresKeyword: autoRest.GetValue("singleton-resource-requires-keyword").GetAwaiter().GetResult(), + operationIdMappings: autoRest.GetValue("operation-id-mappings").GetAwaiter().GetResult(), + updateRequiredCopy: autoRest.GetValue("update-required-copy").GetAwaiter().GetResult(), + patchInitializerCustomization: autoRest.GetValue("patch-initializer-customization").GetAwaiter().GetResult(), + partialResources: autoRest.GetValue("partial-resources").GetAwaiter().GetResult(), + privilegedOperations: autoRest.GetValue("privileged-operations").GetAwaiter().GetResult(), + operationsToLroApiVersionOverride: autoRest.GetValue("operations-to-lro-api-version-override").GetAwaiter().GetResult()); + } + + private static JsonElement? GetAcronymMappingConfig(IPluginCommunication autoRest) + { + var newValue = autoRest.GetValue(TransformTypeName.AcronymMapping).GetAwaiter().GetResult(); + // acronym-mapping was renamed from rename-rules, so fallback to check rename-rules if acronym-mapping is not available + if (newValue == null || !newValue.HasValue || newValue.Value.ValueKind == JsonValueKind.Null) + return autoRest.GetValue("rename-rules").GetAwaiter().GetResult(); + else + return newValue; + } + + internal void SaveConfiguration(Utf8JsonWriter writer) + { + WriteNonEmptySettings(writer, nameof(MergeOperations), MergeOperations); + WriteNonEmptySettings(writer, nameof(RequestPathIsNonResource), RequestPathIsNonResource); + WriteNonEmptySettings(writer, nameof(NoPropertyTypeReplacement), NoPropertyTypeReplacement); + WriteNonEmptySettings(writer, nameof(ListException), ListException); + WriteNonEmptySettings(writer, nameof(KeepOrphanedModels), KeepOrphanedModels); + WriteNonEmptySettings(writer, nameof(KeepPluralEnums), KeepPluralEnums); + WriteNonEmptySettings(writer, nameof(NoResourceSuffix), NoResourceSuffix); + WriteNonEmptySettings(writer, nameof(PrependRPPrefix), PrependRPPrefix); + WriteNonEmptySettings(writer, nameof(GenerateArmResourceExtensions), GenerateArmResourceExtensions); + WriteNonEmptySettings(writer, nameof(OperationGroupsToOmit), OperationGroupsToOmit); + WriteNonEmptySettings(writer, nameof(RequestPathToParent), RequestPathToParent); + WriteNonEmptySettings(writer, nameof(OperationPositions), OperationPositions); + WriteNonEmptySettings(writer, nameof(RequestPathToResourceName), RequestPathToResourceName); + WriteNonEmptySettings(writer, nameof(RequestPathToResourceData), RequestPathToResourceData); + WriteNonEmptySettings(writer, nameof(RequestPathToResourceType), RequestPathToResourceType); + WriteNonEmptySettings(writer, nameof(RequestPathToScopeResourceTypes), RequestPathToScopeResourceTypes); + WriteNonEmptySettings(writer, nameof(RequestPathToSingletonResource), RequestPathToSingletonResource); + WriteNonEmptySettings(writer, nameof(RawAcronymMapping), RawAcronymMapping); + WriteNonEmptySettings(writer, nameof(RenamePropertyBag), RenamePropertyBag); + WriteNonEmptySettings(writer, nameof(FormatByNameRules), FormatByNameRules); + WriteNonEmptySettings(writer, nameof(RenameMapping), RenameMapping); + WriteNonEmptySettings(writer, nameof(ParameterRenameMapping), ParameterRenameMapping); + WriteNonEmptySettings(writer, nameof(IrregularPluralWords), IrregularPluralWords); + WriteNonEmptySettings(writer, nameof(PrivilegedOperations), PrivilegedOperations); + WriteNonEmptySettings(writer, nameof(OverrideOperationName), OverrideOperationName); + WriteNonEmptySettings(writer, nameof(PartialResources), PartialResources); + MgmtDebug.Write(writer, nameof(MgmtDebug)); + if (IsArmCore) + writer.WriteBoolean("ArmCore", IsArmCore); + if (!DoesResourceModelRequireType) + writer.WriteBoolean(nameof(DoesResourceModelRequireType), DoesResourceModelRequireType); + if (!DoesResourceModelRequireName) + writer.WriteBoolean(nameof(DoesResourceModelRequireName), DoesResourceModelRequireName); + if (DoesSingletonRequiresKeyword) + writer.WriteBoolean(nameof(DoesSingletonRequiresKeyword), DoesSingletonRequiresKeyword); + WriteNonEmptySettings(writer, nameof(OperationIdMappings), OperationIdMappings); + WriteNonEmptySettings(writer, nameof(PromptedEnumValues), PromptedEnumValues); + WriteNonEmptySettings(writer, nameof(UpdateRequiredCopy), UpdateRequiredCopy); + WriteNonEmptySettings(writer, nameof(PatchInitializerCustomization), PatchInitializerCustomization); + WriteNonEmptySettings(writer, nameof(OperationsToLroApiVersionOverride), OperationsToLroApiVersionOverride); + } + + internal static MgmtConfiguration LoadConfiguration(JsonElement root) + { + root.TryGetProperty(nameof(OperationGroupsToOmit), out var operationGroupsToOmitElement); + root.TryGetProperty(nameof(RequestPathIsNonResource), out var requestPathIsNonResourceElement); + root.TryGetProperty(nameof(NoPropertyTypeReplacement), out var noPropertyTypeReplacementElement); + root.TryGetProperty(nameof(ListException), out var listExceptionElement); + root.TryGetProperty(nameof(KeepOrphanedModels), out var keepOrphanedModelsElement); + root.TryGetProperty(nameof(KeepPluralEnums), out var keepPluralEnumsElement); + root.TryGetProperty(nameof(KeepPluralResourceData), out var keepPluralResourceDataElement); + root.TryGetProperty(nameof(NoResourceSuffix), out var noResourceSuffixElement); + root.TryGetProperty(nameof(PrependRPPrefix), out var prependRPPrefixElement); + root.TryGetProperty(nameof(GenerateArmResourceExtensions), out var generateArmResourceExtensionsElement); + root.TryGetProperty(nameof(RequestPathToParent), out var requestPathToParent); + root.TryGetProperty(nameof(RequestPathToResourceName), out var requestPathToResourceName); + root.TryGetProperty(nameof(RequestPathToResourceData), out var requestPathToResourceData); + root.TryGetProperty(nameof(RequestPathToResourceType), out var requestPathToResourceType); + root.TryGetProperty(nameof(RequestPathToScopeResourceTypes), out var requestPathToScopeResourceTypes); + root.TryGetProperty(nameof(OperationPositions), out var operationPositions); + root.TryGetProperty(nameof(RequestPathToSingletonResource), out var requestPathToSingletonResource); + root.TryGetProperty(nameof(RawAcronymMapping), out var acronymMapping); + root.TryGetProperty(nameof(RenamePropertyBag), out var renamePropertyBag); + root.TryGetProperty(nameof(FormatByNameRules), out var formatByNameRules); + root.TryGetProperty(nameof(RenameMapping), out var renameMapping); + root.TryGetProperty(nameof(ParameterRenameMapping), out var parameterRenameMapping); + root.TryGetProperty(nameof(IrregularPluralWords), out var irregularPluralWords); + root.TryGetProperty(nameof(OverrideOperationName), out var operationIdToName); + root.TryGetProperty(nameof(MergeOperations), out var mergeOperations); + root.TryGetProperty(nameof(PromptedEnumValues), out var promptedEnumValuesElement); + root.TryGetProperty(nameof(PartialResources), out var virtualResources); + root.TryGetProperty(nameof(RawParameterizedScopes), out var parameterizedScopesElement); + root.TryGetProperty(nameof(PrivilegedOperations), out var privilegedOperationsElement); + root.TryGetProperty(nameof(OperationsToSkipLroApiVersionOverride), out var operationsToSkipLroApiVersionOverrideElement); + + var operationGroupToOmit = Configuration.DeserializeArray(operationGroupsToOmitElement); + var requestPathIsNonResource = Configuration.DeserializeArray(requestPathIsNonResourceElement); + var noPropertyTypeReplacement = Configuration.DeserializeArray(noPropertyTypeReplacementElement); + var listException = Configuration.DeserializeArray(listExceptionElement); + var promptedEnumValues = Configuration.DeserializeArray(promptedEnumValuesElement); + var keepOrphanedModels = Configuration.DeserializeArray(keepOrphanedModelsElement); + var keepPluralEnums = Configuration.DeserializeArray(keepPluralEnumsElement); + var keepPluralResourceData = Configuration.DeserializeArray(keepPluralResourceDataElement); + var noResourceSuffix = Configuration.DeserializeArray(noResourceSuffixElement); + var prependRPPrefix = Configuration.DeserializeArray(prependRPPrefixElement); + var generateArmResourceExtensions = Configuration.DeserializeArray(generateArmResourceExtensionsElement); + var parameterizedScopes = Configuration.DeserializeArray(parameterizedScopesElement); + var operationsToSkipLroApiVersionOverride = Configuration.DeserializeArray(operationsToSkipLroApiVersionOverrideElement); + + root.TryGetProperty("ArmCore", out var isArmCore); + root.TryGetProperty(nameof(MgmtDebug), out var mgmtDebugRoot); + root.TryGetProperty(nameof(DoesResourceModelRequireType), out var resourceModelRequiresType); + root.TryGetProperty(nameof(DoesResourceModelRequireName), out var resourceModelRequiresName); + root.TryGetProperty(nameof(DoesSingletonRequiresKeyword), out var singletonRequiresKeyword); + root.TryGetProperty(nameof(OperationIdMappings), out var operationIdMappings); + root.TryGetProperty(nameof(UpdateRequiredCopy), out var updateRequiredCopy); + root.TryGetProperty(nameof(PatchInitializerCustomization), out var patchInitializerCustomization); + root.TryGetProperty(nameof(PrivilegedOperations), out var privilegedOperations); + root.TryGetProperty(nameof(OperationsToLroApiVersionOverride), out var operationsToLroApiVersionOverride); + + return new MgmtConfiguration( + operationGroupsToOmit: operationGroupToOmit, + requestPathIsNonResource: requestPathIsNonResource, + noPropertyTypeReplacement: noPropertyTypeReplacement, + listException: listException, + promptedEnumValues: promptedEnumValues, + keepOrphanedModels: keepOrphanedModels, + keepPluralEnums: keepPluralEnums, + keepPluralResourceData: keepPluralResourceData, + noResourceSuffix: noResourceSuffix, + schemasToPrependRPPrefix: prependRPPrefix, + generateArmResourceExtensions: generateArmResourceExtensions, + parameterizedScopes: parameterizedScopes, + operationsToSkipLroApiVersionOverride: operationsToSkipLroApiVersionOverride, + mgmtDebug: MgmtDebugConfiguration.LoadConfiguration(mgmtDebugRoot), + requestPathToParent: requestPathToParent, + requestPathToResourceName: requestPathToResourceName, + requestPathToResourceData: requestPathToResourceData, + requestPathToResourceType: requestPathToResourceType, + requestPathToScopeResourceTypes: requestPathToScopeResourceTypes, + operationPositions: operationPositions, + requestPathToSingletonResource: requestPathToSingletonResource, + overrideOperationName: operationIdToName, + acronymMapping: acronymMapping, + renamePropertyBag: renamePropertyBag, + formatByNameRules: formatByNameRules, + renameMapping: renameMapping, + parameterRenameMapping: parameterRenameMapping, + irregularPluralWords: irregularPluralWords, + mergeOperations: mergeOperations, + armCore: isArmCore, + resourceModelRequiresType: resourceModelRequiresType, + resourceModelRequiresName: resourceModelRequiresName, + singletonRequiresKeyword: singletonRequiresKeyword, + operationIdMappings: operationIdMappings, + updateRequiredCopy: updateRequiredCopy, + patchInitializerCustomization: patchInitializerCustomization, + partialResources: virtualResources, + privilegedOperations: privilegedOperations, + operationsToLroApiVersionOverride: operationsToLroApiVersionOverride); + } + + private static void WriteNonEmptySettings( + Utf8JsonWriter writer, + string settingName, + IReadOnlyDictionary settings) + { + if (settings.Count > 0) + { + writer.WriteStartObject(settingName); + foreach (var keyval in settings) + { + writer.WriteString(keyval.Key, keyval.Value); + } + + writer.WriteEndObject(); + } + } + + private static void WriteNonEmptySettings( + Utf8JsonWriter writer, + string settingName, + IReadOnlyDictionary settings) + { + if (settings.Count > 0) + { + writer.WriteStartObject(settingName); + foreach (var keyval in settings) + { + writer.WriteStartArray(keyval.Key); + foreach (var s in keyval.Value) + { + writer.WriteStringValue(s); + } + + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + } + } + + private static void WriteNonEmptySettings( + Utf8JsonWriter writer, + string settingName, + IReadOnlyList settings) + { + if (settings.Count() > 0) + { + writer.WriteStartArray(settingName); + foreach (var s in settings) + { + writer.WriteStringValue(s); + } + + writer.WriteEndArray(); + } + } + + private static void WriteNonEmptySettings( + Utf8JsonWriter writer, + string settingName, + IReadOnlyDictionary> settings) + { + if (settings.Count() > 0) + { + writer.WriteStartObject(settingName); + foreach (var keyval in settings) + { + WriteNonEmptySettings(writer, keyval.Key, keyval.Value); + } + + writer.WriteEndObject(); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/AutoRest/MgmtContext.cs b/logger/autorest.csharp/mgmt/AutoRest/MgmtContext.cs new file mode 100644 index 0000000..e59ef75 --- /dev/null +++ b/logger/autorest.csharp/mgmt/AutoRest/MgmtContext.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.AutoRest +{ + internal static class MgmtContext + { + private static BuildContext? _context; + public static BuildContext Context => _context ?? throw new InvalidOperationException("MgmtContext was not initialized."); + + public static MgmtOutputLibrary Library => Context.Library; + + public static TypeFactory TypeFactory => Context.TypeFactory; + + public static InputNamespace InputNamespace => Context.InputNamespace; + + public static string DefaultNamespace => Configuration.Namespace; + + public static string RPName => ClientBuilder.GetRPName(DefaultNamespace); + + public static bool IsInitialized => _context is not null; + + public static void Initialize(BuildContext context) + { + _context = context; + } + } +} diff --git a/logger/autorest.csharp/mgmt/AutoRest/MgmtOutputLibrary.cs b/logger/autorest.csharp/mgmt/AutoRest/MgmtOutputLibrary.cs new file mode 100644 index 0000000..d692cf0 --- /dev/null +++ b/logger/autorest.csharp/mgmt/AutoRest/MgmtOutputLibrary.cs @@ -0,0 +1,964 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using OutputResourceData = AutoRest.CSharp.Mgmt.Output.ResourceData; +using Azure.Core; +using System.Runtime.CompilerServices; +using AutoRest.CSharp.Common.Input.InputTypes; + +namespace AutoRest.CSharp.Mgmt.AutoRest +{ + internal class MgmtOutputLibrary : OutputLibrary + { + /// + /// This is a map from resource name to a list of + /// considering of the extension resources, one resource name might correspond to multiple operation sets + /// This must be initialized before other maps + /// + private Dictionary> ResourceDataSchemaNameToOperationSets { get; } + + /// + /// This is a map from raw request path to their corresponding , + /// which is a collection of the operations with the same raw request path + /// + internal Dictionary RawRequestPathToOperationSets { get; } + + /// + /// This is a map from operation to its request path + /// + private Lazy> OperationsToRequestPaths { get; } + + /// + /// This is a map from raw request path to the corresponding + /// The type of values is a HashSet of , because we might get the case that multiple operation groups might share the same request path + /// + private Lazy>> RawRequestPathToRestClient { get; } + + /// + /// This is a map from raw request path to the corresponding + /// This must be initialized before other maps + /// + private Lazy> RawRequestPathToResourceData { get; } + + /// + /// This is a map from request path to the which consists from , , and + /// + private Lazy> RequestPathToResources { get; } + + public Lazy> PagingMethods { get; } + + private Lazy> RestClientMethods { get; } + + private Lazy> AllSchemaMap { get; } + + public Lazy> ResourceSchemaMap { get; } + + internal Lazy> SchemaMap { get; } + + private Lazy> AllEnumMap { get; } + + private Lazy>> ChildOperations { get; } + + private readonly InputNamespace _input; + private readonly IEnumerable _inputClients; + private Dictionary _schemaToModels = new(ReferenceEqualityComparer.Instance); + private Lazy> _schemaNameToModels; + + /// + /// This is a collection that contains all the models from property bag, we use HashSet here to avoid potential duplicates + /// + public HashSet PropertyBagModels { get; } + + /// + /// This is a map from to the list of raw request path of its operations + /// + private readonly Dictionary> _operationGroupToRequestPaths = new(); + + public MgmtOutputLibrary(InputNamespace inputNamespace) + { + _input = inputNamespace; + + // For TypeSpec input, we need to filter out the client that has no operations + _inputClients = _input.Clients.Where(c => c.Operations.Count > 0); + + // these dictionaries are initialized right now and they would not change later + RawRequestPathToOperationSets = CategorizeOperationGroups(); + ResourceDataSchemaNameToOperationSets = DecorateOperationSets(); + AllEnumMap = new Lazy>(EnsureAllEnumMap); + AllSchemaMap = new Lazy>(InitializeModels); + + // others are populated later + OperationsToRequestPaths = new Lazy>(PopulateOperationsToRequestPaths); + RawRequestPathToRestClient = new Lazy>>(EnsureRestClients); + RawRequestPathToResourceData = new Lazy>(EnsureRequestPathToResourceData); + RequestPathToResources = new Lazy>(EnsureRequestPathToResourcesMap); + PagingMethods = new Lazy>(EnsurePagingMethods); + RestClientMethods = new Lazy>(EnsureRestClientMethods); + ResourceSchemaMap = new Lazy>(EnsureResourceSchemaMap); + SchemaMap = new Lazy>(EnsureSchemaMap); + ChildOperations = new Lazy>>(EnsureResourceChildOperations); + _schemaNameToModels = new Lazy>(EnsureSchemaNameToModels); + + // initialize the property bag collection + // TODO -- considering provide a customized comparer + PropertyBagModels = new HashSet(); + } + + private Dictionary EnsureSchemaNameToModels() => _schemaToModels.ToDictionary(kv => kv.Key.Name, kv => kv.Value); + + public Dictionary CSharpTypeToOperationSource { get; } = new Dictionary(); + public IEnumerable OperationSources => CSharpTypeToOperationSource.Values; + + private IEnumerable UpdateBodyParameters() + { + var updatedTypes = new List(); + Dictionary usageCounts = new Dictionary(); + + // run one pass to get the schema usage count + foreach (var client in _inputClients) + { + foreach (var operation in client.Operations) + { + foreach (var parameter in operation.Parameters) + { + if (parameter.Location != RequestLocation.Body) + continue; + + IncrementCount(usageCounts, parameter.Type.GetImplementType().Name); + } + foreach (var response in operation.Responses) + { + var responseSchema = response.BodyType; + if (responseSchema is null) + continue; + + IncrementCount(usageCounts, responseSchema.GetImplementType().Name); + } + } + } + + // run second pass to rename the ones based on the schema usage count + foreach (var client in _inputClients) + { + foreach (var operation in client.Operations) + { + if (operation.HttpMethod != RequestMethod.Patch && operation.HttpMethod != RequestMethod.Put && operation.HttpMethod != RequestMethod.Post) + continue; + + var bodyParam = operation.Parameters.FirstOrDefault(p => p.Location == RequestLocation.Body); + if (bodyParam is null) + continue; + + if (!usageCounts.TryGetValue(bodyParam.Type.GetImplementType().Name, out var count)) + continue; + + // get the request path and operation set + RequestPath requestPath = RequestPath.FromOperation(operation, client, MgmtContext.TypeFactory); + var operationSet = RawRequestPathToOperationSets[requestPath]; + if (operationSet.TryGetResourceDataSchema(out _, out var resourceDataSchema, _input)) + { + // if this is a resource, we need to make sure its body parameter is required when the verb is put or patch + BodyParameterNormalizer.MakeRequired(bodyParam, operation.HttpMethod); + } + + if (count != 1) + { + //even if it has multiple uses for a model type we should normalize the param name just not change the type + BodyParameterNormalizer.UpdateParameterNameOnly(bodyParam, ResourceDataSchemaNameToOperationSets, operation); + continue; + } + if (resourceDataSchema is not null) + { + //TODO handle expandable request paths. We assume that this is fine since if all of the expanded + //types use the same model they should have a common name, but since this case doesn't exist yet + //we don't know for sure + if (requestPath.IsExpandable) + throw new InvalidOperationException($"Found expandable path in UpdatePatchParameterNames for {client.Key}.{operation.CSharpName()} : {requestPath}"); + + var name = GetResourceName(resourceDataSchema.Name, operationSet, requestPath); + BodyParameterNormalizer.Update(operation.HttpMethod, bodyParam, name, operation, updatedTypes); + } + else + { + BodyParameterNormalizer.UpdateUsingReplacement(bodyParam, ResourceDataSchemaNameToOperationSets, operation, updatedTypes); + } + } + } + + // run third pass to rename the corresponding parameters + foreach (var client in _inputClients) + { + foreach (var operation in client.Operations) + { + foreach (var param in operation.Parameters) + { + if (param.Location != RequestLocation.Body) + continue; + + if (param.Type.GetImplementType() is not InputModelType inputModel) + continue; + + string originalName = param.Name; + param.Name = NormalizeParamNames.GetNewName(originalName, inputModel.Name, ResourceDataSchemaNameToOperationSets); + + string fullSerializedName = operation.GetFullSerializedName(param); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.UpdateBodyParameter, fullSerializedName), + fullSerializedName, "SetBodyParameterNameOnThirdPass", originalName, param.Name); + + } + } + } + return updatedTypes; + } + + private static void IncrementCount(Dictionary usageCounts, string schemaName) + { + if (usageCounts.ContainsKey(schemaName)) + { + usageCounts[schemaName]++; + } + else + { + usageCounts.Add(schemaName, 1); + } + } + + // Initialize ResourceData, Models and resource manager common types + private Dictionary InitializeModels() + { + var defaultDerivedTypes = new Dictionary(); + // first, construct resource data models + foreach (var inputModel in _input.Models) + { + var defaultDerivedType = GetDefaultDerivedType(inputModel, defaultDerivedTypes); + var model = ResourceDataSchemaNameToOperationSets.ContainsKey(inputModel.Name) ? BuildResourceData(inputModel, defaultDerivedType) : BuildModel(inputModel, defaultDerivedType); + _schemaToModels.Add(inputModel, model); + } + + foreach (var inputEnum in _input.Enums) + { + var model = BuildModel(inputEnum); + _schemaToModels.Add(inputEnum, model); + } + + //this is where we update + var updatedTypes = UpdateBodyParameters(); + foreach (var type in updatedTypes) + { + if (type is InputModelType inputModel) + { + _schemaToModels[type] = BuildModel(type, GetDefaultDerivedType(inputModel, defaultDerivedTypes)); + } + else + { + _schemaToModels[type] = BuildModel(type); + } + } + + // second, collect any model which can be replaced as whole (not as a property or as a base class) + var replacedTypes = new Dictionary(); + foreach (var schema in _input.Models) + { + if (_schemaToModels.TryGetValue(schema, out var type)) + { + if (type is MgmtObjectType mgmtObjectType) + { + var csharpType = TypeReferenceTypeChooser.GetExactMatch(mgmtObjectType); + if (csharpType != null) + { + // re-construct the model with replaced csharp type (e.g. the type in Resource Manager) + switch (mgmtObjectType) + { + case MgmtReferenceType: + // when we get a reference type, we should still wrap it into a reference type + replacedTypes.Add(schema, new MgmtReferenceType(schema)); + break; + default: + // other types will go into SystemObjectType + replacedTypes.Add(schema, csharpType.Implementation); + break; + } + } + } + } + } + + // third, update the entries in cache maps with the new model instances + foreach (var (schema, replacedType) in replacedTypes) + { + var originalModel = _schemaToModels[schema]; + _schemaToModels[schema] = replacedType; + _schemaNameToModels.Value[schema.Name] = replacedType; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.ReplaceTypeWhenInitializingModel, schema.GetFullSerializedName()), + schema.GetFullSerializedName(), + "ReplaceType", originalModel.Declaration.FullName, replacedType.Declaration.FullName); + } + return _schemaToModels; + } + + private IEnumerable? _resourceOperationSets; + public IEnumerable ResourceOperationSets => _resourceOperationSets ??= ResourceDataSchemaNameToOperationSets.SelectMany(pair => pair.Value); + + private MgmtObjectType? GetDefaultDerivedType(InputModelType model, Dictionary defaultDerivedTypes) + { + //only want to create one instance of the default derived per polymorphic set + bool isBasePolyType = model.DiscriminatorProperty is not null; + bool isChildPolyType = model.DiscriminatorValue is not null; + if (!isBasePolyType && !isChildPolyType) + { + return null; + } + + var actualBase = model; + while (actualBase.BaseModel?.DiscriminatorProperty is not null) + { + actualBase = actualBase.BaseModel; + } + + // We don't need to create default type if its an input only model + // TODO -- remove this condition completely when remove the UseModelReaderWriter flag + if (!Configuration.UseModelReaderWriter && !actualBase.Usage.HasFlag(InputModelTypeUsage.Output)) + return null; + + //if I have children and parents then I am my own defaultDerivedType + if (model.DerivedModels.Any() && model.BaseModel is { DiscriminatorProperty: not null }) + { + return null; + } + + string defaultDerivedName = GetDefaultDerivedName(actualBase); + if (!defaultDerivedTypes.TryGetValue(defaultDerivedName, out MgmtObjectType? defaultDerivedType)) + { + //create the "Unknown" version + var unknownDerivedType = new InputModelType( + defaultDerivedName, + string.Empty, + "internal", + null, + // [TODO]: Condition is added to minimize change + $"Unknown version of {actualBase.SpecName ?? actualBase.Name}", + actualBase.Usage, + Array.Empty(), + actualBase, + Array.Empty(), + "Unknown", //TODO: do we need to support extensible enum / int values? + null, + new Dictionary(), + null) + { + IsUnknownDiscriminatorModel = true, + }; + + defaultDerivedType = new MgmtObjectType(unknownDerivedType); + defaultDerivedTypes.Add(defaultDerivedName, defaultDerivedType); + _schemaToModels.Add(unknownDerivedType, defaultDerivedType); + } + + return defaultDerivedType; + } + + private static string GetDefaultDerivedName(InputModelType actualBase) + { + if (actualBase.SpecName is not null) + { + return $"Unknown{NameTransformer.Instance.EnsureNameCase(actualBase.SpecName, null).Name}"; + } + return $"Unknown{actualBase.Name}"; + } + + public OperationSet GetOperationSet(string requestPath) => RawRequestPathToOperationSets[requestPath]; + + public RestClientMethod GetRestClientMethod(InputOperation operation) + { + if (RestClientMethods.Value.TryGetValue(operation, out var restClientMethod)) + { + return restClientMethod; + } + throw new Exception($"The {operation.OperationId} method does not exist."); + } + + public RequestPath GetRequestPath(InputOperation operation) => OperationsToRequestPaths.Value[operation]; + + private Dictionary EnsurePagingMethods() + { + var pagingMethods = new Dictionary(); + var placeholder = new TypeDeclarationOptions("Placeholder", "Placeholder", "public", false, true); + foreach (var restClient in RestClients) + { + var methods = ClientBuilder.BuildPagingMethods(restClient.InputClient, restClient, placeholder); + foreach (var method in methods) + { + pagingMethods.Add(method.Method, method); + } + } + + return pagingMethods; + } + + private Dictionary EnsureRestClientMethods() + { + var restClientMethods = new Dictionary(new ObjectReferenceEqualityComparer()); + foreach (var restClient in RestClients) + { + foreach (var (operation, restClientMethod) in restClient.Methods) + { + if (restClientMethod.Accessibility != MethodSignatureModifiers.Public) + continue; + if (!restClientMethods.TryAdd(operation, restClientMethod)) + { + throw new Exception($"An rest method '{operation.OperationId}' has already been added"); + } + } + } + + return restClientMethods; + } + + private ModelFactoryTypeProvider? _modelFactory; + public ModelFactoryTypeProvider? ModelFactory => _modelFactory ??= ModelFactoryTypeProvider.TryCreate(AllSchemaMap.Value.Values.Where(ShouldIncludeModel), MgmtContext.TypeFactory, MgmtContext.Context.SourceInputModel); + + private bool ShouldIncludeModel(TypeProvider model) + { + if (model is MgmtReferenceType) + return false; + + return model.Type.Namespace.StartsWith(MgmtContext.Context.DefaultNamespace); + } + + private MgmtExtensionBuilder? _extensionBuilder; + internal MgmtExtensionBuilder ExtensionBuilder => _extensionBuilder ??= EnsureExtensionBuilder(); + + private MgmtExtensionBuilder EnsureExtensionBuilder() + { + var extensionOperations = new Dictionary>(); + // find the extension operations for the armcore types other than ArmResource + foreach (var (armCoreType, extensionContextualPath) in RequestPath.ExtensionChoices) + { + var operations = ShouldGenerateChildrenForType(armCoreType) ? GetChildOperations(extensionContextualPath) : Enumerable.Empty(); + extensionOperations.Add(armCoreType, operations); + } + + // find the extension operations for ArmResource + var armResourceOperations = new Dictionary>(); + foreach (var (parentRequestPath, operations) in ChildOperations.Value) + { + if (parentRequestPath.IsParameterizedScope()) + { + armResourceOperations.Add(parentRequestPath, operations); + } + } + + return new MgmtExtensionBuilder(extensionOperations, armResourceOperations); + } + + private bool ShouldGenerateChildrenForType(Type armCoreType) + => !Configuration.MgmtConfiguration.IsArmCore || armCoreType.Namespace != MgmtContext.Context.DefaultNamespace; + + public IEnumerable Extensions => ExtensionBuilder.Extensions; + public IEnumerable MockableExtensions => ExtensionBuilder.MockableExtensions; + public MgmtExtensionWrapper ExtensionWrapper => ExtensionBuilder.ExtensionWrapper; + + public MgmtExtension GetExtension(Type armCoreType) => ExtensionBuilder.GetExtension(armCoreType); + + private IEnumerable? _resourceDatas; + public IEnumerable ResourceData => _resourceDatas ??= RawRequestPathToResourceData.Value.Values.Distinct(); + + private IEnumerable? _restClients; + public IEnumerable RestClients => _restClients ??= RawRequestPathToRestClient.Value.Values.SelectMany(v => v).Distinct(); + + private IEnumerable? _armResources; + public IEnumerable ArmResources => _armResources ??= RequestPathToResources.Value.Values.Select(bag => bag.Resource).Distinct(); + + private IEnumerable? _resourceCollections; + public IEnumerable ResourceCollections => _resourceCollections ??= RequestPathToResources.Value.Values.Select(bag => bag.ResourceCollection).WhereNotNull().Distinct(); + + private Dictionary EnsureResourceSchemaMap() + { + return AllSchemaMap.Value.Where(kv => kv.Value is ResourceData).ToDictionary(kv => kv.Key, kv => kv.Value); + } + + private Dictionary EnsureSchemaMap() + { + return AllSchemaMap.Value.Where(kv => !(kv.Value is ResourceData)).ToDictionary(kv => kv.Key, kv => kv.Value); + } + + public Dictionary EnsureAllEnumMap() + => _input.Enums.ToDictionary(e => e, e => new EnumType(e, MgmtContext.Context)); + + private IEnumerable? _models; + public IEnumerable Models => _models ??= SchemaMap.Value.Values.Where(m => m is not SystemObjectType); + + public ResourceData GetResourceData(string requestPath) + { + if (TryGetResourceData(requestPath, out var resourceData)) + return resourceData; + + throw new InvalidOperationException($"Request path {requestPath} does not correspond to a ResourceData"); + } + + public bool TryGetResourceData(string requestPath, [MaybeNullWhen(false)] out ResourceData resourceData) + { + return RawRequestPathToResourceData.Value.TryGetValue(requestPath, out resourceData); + } + + public bool TryGetArmResource(RequestPath requestPath, [MaybeNullWhen(false)] out Resource resource) + { + resource = null; + if (RequestPathToResources.Value.TryGetValue(requestPath, out var bag)) + { + resource = bag.Resource; + return true; + } + + return false; + } + + public MgmtRestClient GetRestClient(InputOperation operation) + { + var requestPath = operation.GetHttpPath(); + if (TryGetRestClients(requestPath, out var restClients)) + { + // return the first client that contains this operation + return restClients.Single(client => client.InputClient.Operations.Contains(operation)); + } + + throw new InvalidOperationException($"Cannot find MgmtRestClient corresponding to {requestPath} with method {operation.HttpMethod}"); + } + + public bool TryGetRestClients(string requestPath, [MaybeNullWhen(false)] out HashSet restClients) + { + return RawRequestPathToRestClient.Value.TryGetValue(requestPath, out restClients); + } + + private Dictionary> EnsureRestClients() + { + var rawRequestPathToRestClient = new Dictionary>(); + foreach (var inputClient in _inputClients) + { + var restClient = new MgmtRestClient(inputClient, new MgmtRestClientBuilder(inputClient)); + foreach (var requestPath in _operationGroupToRequestPaths[inputClient]) + { + if (rawRequestPathToRestClient.TryGetValue(requestPath, out var set)) + set.Add(restClient); + else + rawRequestPathToRestClient.Add(requestPath, new HashSet { restClient }); + } + } + + return rawRequestPathToRestClient; + } + + private Dictionary EnsureRequestPathToResourcesMap() + { + var requestPathToResources = new Dictionary(); + + foreach ((var resourceDataSchemaName, var operationSets) in ResourceDataSchemaNameToOperationSets) + { + foreach (var operationSet in operationSets) + { + // get the corresponding resource data + var originalResourcePath = operationSet.GetRequestPath(); + var operations = GetChildOperations(originalResourcePath); + var resourceData = GetResourceData(originalResourcePath); + if (resourceData == OutputResourceData.Empty) + BuildPartialResource(requestPathToResources, resourceDataSchemaName, operationSet, operations, originalResourcePath); + else + BuildResource(requestPathToResources, resourceDataSchemaName, operationSet, operations, originalResourcePath, resourceData); + } + } + + return requestPathToResources; + } + + private void BuildResource(Dictionary result, string resourceDataSchemaName, OperationSet operationSet, IEnumerable operations, RequestPath originalResourcePath, ResourceData resourceData) + { + var isSingleton = operationSet.IsSingletonResource(); + // we calculate the resource type of the resource + var resourcePaths = originalResourcePath.Expand(); + foreach (var resourcePath in resourcePaths) + { + var resourceType = resourcePath.GetResourceType(); + var resource = new Resource(operationSet, operations, GetResourceName(resourceDataSchemaName, operationSet, resourcePath), resourceType, resourceData); + var collection = isSingleton ? null : new ResourceCollection(operationSet, operations, resource); + resource.ResourceCollection = collection; + + result.Add(resourcePath, new ResourceObjectAssociation(resourceType, resourceData, resource, collection)); + } + } + + private void BuildPartialResource(Dictionary result, string resourceDataSchemaName, OperationSet operationSet, IEnumerable operations, RequestPath originalResourcePath) + { + var resourceType = originalResourcePath.GetResourceType(); + var resource = new PartialResource(operationSet, operations, GetResourceName(resourceDataSchemaName, operationSet, originalResourcePath, isPartial: true), resourceDataSchemaName, resourceType); + result.Add(originalResourcePath, new ResourceObjectAssociation(originalResourcePath.GetResourceType(), OutputResourceData.Empty, resource, null)); + } + + private string? GetDefaultNameFromConfiguration(OperationSet operationSet, ResourceTypeSegment resourceType) + { + if (Configuration.MgmtConfiguration.RequestPathToResourceName.TryGetValue(operationSet.RequestPath, out var name)) + return name; + if (Configuration.MgmtConfiguration.RequestPathToResourceName.TryGetValue($"{operationSet.RequestPath}|{resourceType}", out name)) + return name; + + return null; + } + + private string GetResourceName(string candidateName, OperationSet operationSet, RequestPath requestPath, bool isPartial = false) + { + // read configuration to see if we could get a configuration for this resource + var resourceType = requestPath.GetResourceType(); + var defaultNameFromConfig = GetDefaultNameFromConfiguration(operationSet, resourceType); + if (defaultNameFromConfig != null) + return defaultNameFromConfig; + + var resourceName = CalculateResourceName(candidateName, operationSet, requestPath, resourceType); + + return isPartial ? + $"{resourceName}{MgmtContext.RPName}" : + resourceName; + } + + private string CalculateResourceName(string candidateName, OperationSet operationSet, RequestPath requestPath, ResourceTypeSegment resourceType) + { + // find all the expanded request paths of resources that are assiociated with the same resource data model + var resourcesWithSameResourceData = ResourceDataSchemaNameToOperationSets[candidateName] + .SelectMany(opSet => opSet.GetRequestPath().Expand()).ToList(); + // find all the expanded resource types of resources that have the same resource type as this one + var resourcesWithSameResourceType = ResourceOperationSets + .SelectMany(opSet => opSet.GetRequestPath().Expand()) + .Where(rqPath => rqPath.GetResourceType().Equals(resourceType)).ToList(); + + var isById = requestPath.IsById; + int countOfSameResourceDataName = resourcesWithSameResourceData.Count; + int countOfSameResourceTypeName = resourcesWithSameResourceType.Count; + if (!isById) + { + // this is a regular resource and the name is unique + if (countOfSameResourceDataName == 1) + return candidateName; + + // if countOfSameResourceDataName > 1, we need to have the resource types as the resource type name + // if we have the unique resource type, we just use the resource type to construct our resource type name + var types = resourceType.Types; + var name = string.Join("", types.Select(segment => segment.ConstantValue.LastWordToSingular().FirstCharToUpperCase())); + if (countOfSameResourceTypeName == 1) + return name; + + string parentPrefix = GetParentPrefix(requestPath); + // if countOfSameResourceTypeName > 1, we will have to add the scope as prefix to fully qualify the resource type name + // first we try to add the parent name as prefix + if (!DoMultipleResourcesShareMyPrefixes(requestPath, parentPrefix, resourcesWithSameResourceType)) + return $"{parentPrefix}{name}"; + + // if we get here, parent prefix is not enough, we try the resource name if it is a constant + if (requestPath.Last().IsConstant) + return $"{requestPath.Last().ConstantValue.FirstCharToUpperCase()}{name}"; + + // if we get here, we have tried all approaches to get a solid resource type name, throw an exception + throw new InvalidOperationException($"Cannot determine a resource class name for resource with the request path: {requestPath}, please assign a valid resource name in `request-path-to-resource-name` section"); + } + // if this resource is based on a "ById" operation + // if we only have one resource class with this name - we have no choice but use this "ById" resource + if (countOfSameResourceDataName == 1) + return candidateName; + + // otherwise we need to add a "ById" suffix to make this resource to have a different name + // TODO -- introduce a flag that suppress the exception here to be thrown which notice the user to assign a proper name in config + return $"{candidateName}ById"; + } + + private string GetParentPrefix(RequestPath pathToWalk) + { + while (pathToWalk.Count > 2) + { + pathToWalk = pathToWalk.ParentRequestPath(); + if (RawRequestPathToResourceData.Value.TryGetValue(pathToWalk.ToString()!, out var parentData)) + { + return parentData.Declaration.Name.Substring(0, parentData.Declaration.Name.Length - 4); + } + else + { + var prefix = GetCoreParentName(pathToWalk); + if (prefix is not null) + return prefix; + } + } + return string.Empty; + } + + private string? GetCoreParentName(RequestPath requestPath) + { + var resourceType = requestPath.GetResourceType(); + if (resourceType.Equals(ResourceTypeSegment.ManagementGroup)) + return nameof(ResourceTypeSegment.ManagementGroup); + if (resourceType.Equals(ResourceTypeSegment.ResourceGroup)) + return nameof(ResourceTypeSegment.ResourceGroup); + if (resourceType.Equals(ResourceTypeSegment.Subscription)) + return nameof(ResourceTypeSegment.Subscription); + if (resourceType.Equals(ResourceTypeSegment.Tenant)) + return nameof(ResourceTypeSegment.Tenant); + return null; + } + + private bool DoMultipleResourcesShareMyPrefixes(RequestPath requestPath, string parentPrefix, IEnumerable resourcesWithSameType) + { + foreach (var resourcePath in resourcesWithSameType) + { + if (resourcePath.Equals(requestPath)) + continue; //skip myself + + if (GetParentPrefix(resourcePath).Equals(parentPrefix, StringComparison.Ordinal)) + return true; + } + return false; + } + + private struct RequestPathCollectionEqualityComparer : IEqualityComparer> + { + public bool Equals([AllowNull] IEnumerable x, [AllowNull] IEnumerable y) + { + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; + return x.SequenceEqual(y); + } + + public int GetHashCode([DisallowNull] IEnumerable obj) + { + return obj.GetHashCode(); + } + } + + public IEnumerable GetChildOperations(RequestPath requestPath) + { + if (requestPath == RequestPath.Any) + return Enumerable.Empty(); + + if (ChildOperations.Value.TryGetValue(requestPath, out var operations)) + return operations; + + return Enumerable.Empty(); + } + + private Dictionary> EnsureResourceChildOperations() + { + var childOperations = new Dictionary>(); + foreach (var operationSet in RawRequestPathToOperationSets.Values) + { + if (operationSet.IsResource(_input)) + continue; + foreach (var operation in operationSet) + { + var parentRequestPath = operation.ParentRequestPath(_input); + if (childOperations.TryGetValue(parentRequestPath, out var list)) + list.Add(operation); + else + childOperations.Add(parentRequestPath, new HashSet { operation }); + } + } + + return childOperations; + } + + private Dictionary EnsureRequestPathToResourceData() + { + var rawRequestPathToResourceData = new Dictionary(); + foreach ((var schema, var provider) in ResourceSchemaMap.Value) + { + if (ResourceDataSchemaNameToOperationSets.TryGetValue(schema.GetImplementType().Name, out var operationSets)) + { + // we are iterating over the ResourceSchemaMap, the value can only be [ResourceData]s + var resourceData = (ResourceData)provider; + foreach (var operationSet in operationSets) + { + if (!rawRequestPathToResourceData.ContainsKey(operationSet.RequestPath)) + { + rawRequestPathToResourceData.Add(operationSet.RequestPath, resourceData); + } + } + } + } + + foreach (var path in Configuration.MgmtConfiguration.PartialResources.Keys) + { + rawRequestPathToResourceData.Add(path, OutputResourceData.Empty); + } + + return rawRequestPathToResourceData; + } + + public override CSharpType ResolveEnum(InputEnumType enumType) + { + CSharpType resolvedEnum; + if (AllEnumMap.Value.TryGetValue(enumType, out var value)) + { + resolvedEnum = value.Type; + } + else + { + throw new InvalidOperationException($"Cannot find enum {enumType.Name}"); + } + return resolvedEnum; + } + + public override CSharpType ResolveModel(InputModelType model) + { + CSharpType resolvedModel; + if (_schemaToModels.TryGetValue(model, out var value)) + { + resolvedModel = value.Type; + } + else + { + throw new InvalidOperationException($"Cannot find model {model.Name}"); + } + return resolvedModel; + } + + public override CSharpType? FindTypeByName(string originalName) + { + _schemaNameToModels.Value.TryGetValue(originalName, out TypeProvider? provider); + + // Try to search declaration name too if no key matches. i.e. Resource Data Type will be appended a 'Data' in the name and won't be found through key + provider ??= _schemaToModels.FirstOrDefault(s => s.Value is MgmtObjectType mot && mot.Declaration.Name == originalName).Value; + + return provider?.Type; + } + + public IEnumerable FindResources(ResourceData resourceData) + { + return ArmResources.Where(resource => resource.ResourceData == resourceData); + } + + private TypeProvider BuildModel(InputType inputType, MgmtObjectType? defaultDerivedType = null) => inputType switch + { + InputEnumType enumType => new EnumType(enumType, MgmtContext.Context), + InputModelType inputModel => (MgmtReferenceType.IsPropertyReferenceType(inputModel) || MgmtReferenceType.IsTypeReferenceType(inputModel) || MgmtReferenceType.IsReferenceType(inputModel)) + ? new MgmtReferenceType(inputModel) + : new MgmtObjectType(inputModel, defaultDerivedType: defaultDerivedType), + InputNullableType nullableType => BuildModel(nullableType.Type, defaultDerivedType), + _ => throw new NotImplementedException($"Unhandled schema type {inputType.GetType()} with name {inputType.Name}") + }; + + private TypeProvider BuildResourceData(InputType inputType, MgmtObjectType? defaultDerivedType) + { + if (inputType.GetImplementType() is InputModelType inputModel) + { + return new ResourceData(inputModel, defaultDerivedType: defaultDerivedType); + } + throw new NotImplementedException(); + } + + private Dictionary> DecorateOperationSets() + { + Dictionary> resourceDataSchemaNameToOperationSets = new Dictionary>(); + foreach (var operationSet in RawRequestPathToOperationSets.Values) + { + if (operationSet.TryGetResourceDataSchema(out var resourceSchemaName, out var resourceSchema, _input)) + { + // Skip the renaming for partial resource when resourceSchema is null but resourceSchemaName is not null + if (resourceSchema is not null) + { + // ensure the name of resource data is singular + // skip this step if the configuration is set to keep this plural + if (!Configuration.MgmtConfiguration.KeepPluralResourceData.Contains(resourceSchemaName)) + { + resourceSchemaName = resourceSchemaName.LastWordToSingular(false); + resourceSchema.Name = resourceSchemaName; + } + else + { + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.KeepPluralResourceData, resourceSchemaName), + resourceSchema.GetFullSerializedName(), $"Keep ObjectName as Plural: {resourceSchemaName}"); + } + } + + // if this operation set corresponds to a SDK resource, we add it to the map + if (!resourceDataSchemaNameToOperationSets.TryGetValue(resourceSchemaName!, out HashSet? result)) + { + result = new HashSet(); + resourceDataSchemaNameToOperationSets.Add(resourceSchemaName!, result); + } + result.Add(operationSet); + } + } + + return resourceDataSchemaNameToOperationSets; + } + + private Dictionary CategorizeOperationGroups() + { + var rawRequestPathToOperationSets = new Dictionary(); + foreach (var inputClient in _inputClients) + { + var requestPathList = new HashSet(); + _operationGroupToRequestPaths.Add(inputClient, requestPathList); + foreach (var operation in inputClient.Operations) + { + var path = operation.GetHttpPath(); + requestPathList.Add(path); + if (rawRequestPathToOperationSets.TryGetValue(path, out var operationSet)) + { + operationSet.Add(operation); + } + else + { + operationSet = new OperationSet(path, inputClient) + { + operation + }; + rawRequestPathToOperationSets.Add(path, operationSet); + } + } + } + + // add operation set for the partial resources here + foreach (var path in Configuration.MgmtConfiguration.PartialResources.Keys) + { + rawRequestPathToOperationSets.Add(path, new OperationSet(path, null)); + } + + return rawRequestPathToOperationSets; + } + + private Dictionary PopulateOperationsToRequestPaths() + { + var operationsToRequestPath = new Dictionary(ReferenceEqualityComparer.Instance); + foreach (var operationGroup in _inputClients) + { + foreach (var operation in operationGroup.Operations) + { + operationsToRequestPath[operation] = RequestPath.FromOperation(operation, operationGroup, MgmtContext.TypeFactory); + } + } + return operationsToRequestPath; + } + + private class ObjectReferenceEqualityComparer : EqualityComparer where T : class + { + public override bool Equals(T? x, T? y) => ReferenceEquals(x, y); + + public override int GetHashCode([DisallowNull] T obj) => RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/logger/autorest.csharp/mgmt/AutoRest/MgmtTarget.cs b/logger/autorest.csharp/mgmt/AutoRest/MgmtTarget.cs new file mode 100644 index 0000000..1e533fb --- /dev/null +++ b/logger/autorest.csharp/mgmt/AutoRest/MgmtTarget.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.AutoRest.PostProcess; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Generation; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Models.Types; +using Microsoft.CodeAnalysis; + +namespace AutoRest.CSharp.AutoRest.Plugins +{ + internal class MgmtTarget + { + private static IDictionary> _addedProjectFilenames = new Dictionary>(); + private static IDictionary> _overriddenProjectFilenames = new Dictionary>(); + + private static bool _reportOnlyModeWarningLogged = false; + private static void AddGeneratedFile(GeneratedCodeWorkspace project, string filename, string text) + { + if (Configuration.MgmtConfiguration.MgmtDebug.ReportOnly) + { + if (!_reportOnlyModeWarningLogged) + { + AutoRestLogger.Warning("codegen-report-only is set to true. Skip adding source code file to output.").Wait(); + _reportOnlyModeWarningLogged = true; + } + return; + } + + if (!_addedProjectFilenames.TryGetValue(project, out var addedFileNames)) + { + addedFileNames = new HashSet(); + _addedProjectFilenames.Add(project, addedFileNames); + } + if (addedFileNames.Contains(filename)) + { + if (!_overriddenProjectFilenames.TryGetValue(project, out var overriddenFileNames)) + { + overriddenFileNames = new List(); + _overriddenProjectFilenames.Add(project, overriddenFileNames); + } + overriddenFileNames.Add(filename); + } + else + { + addedFileNames.Add(filename); + } + project.AddGeneratedFile(filename, text); + } + + public static async Task ExecuteAsync(GeneratedCodeWorkspace project) + { + var addedFilenames = new HashSet(); + var serializeWriter = new SerializationWriter(); + var isArmCore = Configuration.MgmtConfiguration.IsArmCore; + + if (!isArmCore) + { + var utilCodeWriter = new CodeWriter(); + var staticUtilWriter = new StaticUtilWriter(utilCodeWriter); + staticUtilWriter.Write(); + AddGeneratedFile(project, $"ProviderConstants.cs", utilCodeWriter.ToString()); + } + + foreach (var helper in ExpressionTypeProvider.GetHelperProviders()) + { + var helperWriter = new CodeWriter(); + new ExpressionTypeProviderWriter(helperWriter, helper).Write(); + project.AddGeneratedFile($"Internal/{helper.Type.Name}.cs", helperWriter.ToString()); + } + + foreach (var model in MgmtContext.Library.Models) + { + var name = model.Type.Name; + + if (model is MgmtObjectType mot) + { + ModelItem mi = new ModelItem(mot.Declaration.Namespace, mot.Declaration.Name, mot.InputModel.Name, MgmtReport.Instance.TransformSection); + mi.Properties = mot.Properties.ToDictionary(p => p.Declaration.Name, p => + { + if (p.InputModelProperty != null) + { + return new PropertyItem(p.Declaration.Name, p.Declaration.Type.GetNameForReport(), mot.GetFullSerializedName(p.InputModelProperty), MgmtReport.Instance.TransformSection); + } + else + { + AutoRestLogger.Warning($"Ignore Property '{mi.FullName}.{p.Declaration.Name}' without schema (i.e. AdditionalProperties)").Wait(); + return new PropertyItem(p.Declaration.Name, p.Declaration.Type.GetNameForReport(), "", MgmtReport.Instance.TransformSection); + } + }); + MgmtReport.Instance.ModelSection.Add(mi.FullName, mi); + } + else if (model is EnumType et) + { + var inputType = MgmtContext.Library.SchemaMap.Value.First(map => map.Value == model).Key; + var choices = inputType switch + { + InputEnumType sc => sc.Values, + _ => throw new InvalidOperationException("Unexpected Schema type for EnumType: " + inputType.GetType()) + }; + + EnumItem mi = new EnumItem(et.Declaration.Namespace, et.Declaration.Name, inputType.Name, MgmtReport.Instance.TransformSection); + mi.Values = et.Values.ToDictionary(v => v.Declaration.Name, v => + { + var found = choices.FirstOrDefault(c => c.Value.ToString() == v.Value.Value?.ToString()); + if (found == null) + { + var allValues = string.Join(",", choices.Select(c => c.Value ?? "")); + AutoRestLogger.Warning($"Can't find matching enumvalue '{v.Value}' in '{allValues}'").Wait(); + return new EnumValueItem(v.Declaration.Name, "", MgmtReport.Instance.TransformSection); + } + return new EnumValueItem(v.Declaration.Name, inputType.GetFullSerializedName(found), MgmtReport.Instance.TransformSection); + }); + MgmtReport.Instance.EnumSection.Add(mi.FullName, mi); + } + else + { + AutoRestLogger.Warning("Model found which is not MgmtObjectType: " + name).Wait(); + } + + WriteArmModel(project, model, serializeWriter, $"Models/{name}.cs", $"Models/{name}.Serialization.cs"); + } + + foreach (var client in MgmtContext.Library.RestClients) + { + var restCodeWriter = new CodeWriter(); + new MgmtRestClientWriter().WriteClient(restCodeWriter, client); + + AddGeneratedFile(project, $"RestOperations/{client.Type.Name}.cs", restCodeWriter.ToString()); + } + + foreach (var resourceCollection in MgmtContext.Library.ResourceCollections) + { + var writer = new ResourceCollectionWriter(resourceCollection); + writer.Write(); + + var ri = new ResourceItem(resourceCollection, MgmtReport.Instance.TransformSection); + MgmtReport.Instance.ResourceCollectionSection.Add(ri.Name, ri); + + AddGeneratedFile(project, $"{resourceCollection.Type.Name}.cs", writer.ToString()); + } + + foreach (var model in MgmtContext.Library.ResourceData) + { + if (model == ResourceData.Empty) + continue; + + var name = model.Type.Name; + + ModelItem mi = new ModelItem(model.Declaration.Namespace, model.Declaration.Name, model.InputModel.Name, MgmtReport.Instance.TransformSection); + mi.Properties = model.Properties.ToDictionary(p => p.Declaration.Name, p => + { + if (p.InputModelProperty != null) + { + return new PropertyItem(p.Declaration.Name, p.Declaration.Type.GetNameForReport(), model.GetFullSerializedName(p.InputModelProperty), MgmtReport.Instance.TransformSection); + } + else + { + AutoRestLogger.Warning($"Ignore Resource Property '{mi.FullName}.{p.Declaration.Name}' without schema (i.e. AdditionalProperties)").Wait(); + return new PropertyItem(p.Declaration.Name, p.Declaration.Type.GetNameForReport(), "", MgmtReport.Instance.TransformSection); + } + }); + MgmtReport.Instance.ModelSection.Add(mi.FullName, mi); + + WriteArmModel(project, model, serializeWriter, $"{name}.cs", $"{name}.Serialization.cs"); + } + + foreach (var resource in MgmtContext.Library.ArmResources) + { + var writer = ResourceWriter.GetWriter(resource); + writer.Write(); + + var name = resource.Type.Name; + + var ri = new ResourceItem(resource, MgmtReport.Instance.TransformSection); + MgmtReport.Instance.ResourceSection.Add(ri.Name, ri); + + AddGeneratedFile(project, $"{name}.cs", writer.ToString()); + + // we do not need this if model reader writer feature is not enabled + if (Configuration.UseModelReaderWriter) + { + WriteSerialization(project, resource, serializeWriter, $"{name}.Serialization.cs"); + } + } + + var wirePathWriter = new WirePathWriter(); + wirePathWriter.Write(); + AddGeneratedFile(project, $"Internal/WirePathAttribute.cs", wirePathWriter.ToString()); + + // write extension class + WriteExtensions(project, isArmCore, MgmtContext.Library.ExtensionWrapper, MgmtContext.Library.Extensions, MgmtContext.Library.MockableExtensions); + + var lroWriter = new MgmtLongRunningOperationWriter(true); + lroWriter.Write(); + AddGeneratedFile(project, lroWriter.Filename, lroWriter.ToString()); + lroWriter = new MgmtLongRunningOperationWriter(false); + lroWriter.Write(); + AddGeneratedFile(project, lroWriter.Filename, lroWriter.ToString()); + + foreach (var operationSource in MgmtContext.Library.OperationSources) + { + var writer = new OperationSourceWriter(operationSource); + writer.Write(); + AddGeneratedFile(project, $"LongRunningOperation/{operationSource.Type.Name}.cs", writer.ToString()); + } + + foreach (var model in MgmtContext.Library.PropertyBagModels) + { + var name = model.Type.Name; + WriteArmModel(project, model, serializeWriter, $"Models/{name}.cs", $"Models/{name}.Serialization.cs"); + } + + var modelFactoryProvider = MgmtContext.Library.ModelFactory; + if (modelFactoryProvider is not null && modelFactoryProvider.Methods.Any()) + { + var modelFactoryWriter = new ModelFactoryWriter(modelFactoryProvider); + modelFactoryWriter.Write(); + AddGeneratedFile(project, $"{modelFactoryProvider.Type.Name}.cs", modelFactoryWriter.ToString()); + } + + // TODO: fix the overriden + //if (_overriddenProjectFilenames.TryGetValue(project, out var overriddenFilenames)) + // throw new InvalidOperationException($"At least one file was overridden during the generation process. Filenames are: {string.Join(", ", overriddenFilenames)}"); + + var modelsToKeep = Configuration.MgmtConfiguration.KeepOrphanedModels.ToImmutableHashSet(); + await project.PostProcessAsync(new MgmtPostProcessor(modelsToKeep, modelFactoryProvider?.FullName)); + } + + private static void WriteExtensions(GeneratedCodeWorkspace project, bool isArmCore, MgmtExtensionWrapper extensionWrapper, IEnumerable extensions, IEnumerable mockableExtensions) + { + if (isArmCore) + { + // for Azure.ResourceManager (ArmCore), we write the individual extension type providers into their individual files + foreach (var extension in extensions) + { + if (!extension.IsEmpty) + { + MgmtReport.Instance.ExtensionSection.Add(extension.ResourceName, new ExtensionItem(extension, MgmtReport.Instance.TransformSection)); + WriteExtensionFile(project, MgmtExtensionWriter.GetWriter(extension)); + } + } + } + else + { + // for other packages (not ArmCore), we write extension wrapper (a big class that contains all the extension methods) and do not write the individual extension classes + if (!extensionWrapper.IsEmpty) + WriteExtensionFile(project, new MgmtExtensionWrapperWriter(extensionWrapper)); + + // and we write ExtensionClients + foreach (var mockableExtension in mockableExtensions) + { + if (!mockableExtension.IsEmpty) + { + MgmtReport.Instance.ExtensionSection.Add(mockableExtension.ResourceName, new ExtensionItem(mockableExtension, MgmtReport.Instance.TransformSection)); + WriteExtensionFile(project, MgmtMockableExtensionWriter.GetWriter(mockableExtension)); + } + } + } + } + + private static void WriteExtensionFile(GeneratedCodeWorkspace project, MgmtClientBaseWriter extensionWriter) + { + extensionWriter.Write(); + AddGeneratedFile(project, $"Extensions/{extensionWriter.FileName}.cs", extensionWriter.ToString()); + } + + private static void WriteArmModel(GeneratedCodeWorkspace project, TypeProvider model, SerializationWriter serializeWriter, string modelFileName, string serializationFileName) + { + var codeWriter = new CodeWriter(); + + var modelWriter = model switch + { + MgmtReferenceType => new ReferenceTypeWriter(), + ResourceData data => new ResourceDataWriter(data), + _ => new ModelWriter() + }; + + modelWriter.WriteModel(codeWriter, model); + + AddGeneratedFile(project, modelFileName, codeWriter.ToString()); + + WriteSerialization(project, model, serializeWriter, serializationFileName); + } + + private static void WriteSerialization(GeneratedCodeWorkspace project, TypeProvider model, SerializationWriter serializeWriter, string serializationFileName) + { + var serializerCodeWriter = new CodeWriter(); + serializeWriter.WriteSerialization(serializerCodeWriter, model); + AddGeneratedFile(project, serializationFileName, serializerCodeWriter.ToString()); + } + } +} diff --git a/logger/autorest.csharp/mgmt/AutoRest/PostProcess/MgmtPostProcessor.cs b/logger/autorest.csharp/mgmt/AutoRest/PostProcess/MgmtPostProcessor.cs new file mode 100644 index 0000000..01a5670 --- /dev/null +++ b/logger/autorest.csharp/mgmt/AutoRest/PostProcess/MgmtPostProcessor.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using AutoRest.CSharp.AutoRest.Plugins; +using AutoRest.CSharp.Common.Output.PostProcessing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AutoRest.CSharp.Mgmt.AutoRest.PostProcess +{ + internal sealed class MgmtPostProcessor : PostProcessor + { + public MgmtPostProcessor(ImmutableHashSet modelsToKeep, string? modelFactoryFullName) : base(modelsToKeep, modelFactoryFullName) { } + + protected override bool IsRootDocument(Document document) + { + var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); + // a document is root when + // 1. the file is under `Generated` or `Generated/Extensions` which is handled by `IsMgmtRootDocument` + // 2. the declaration has a ReferenceType or similar attribute on it which is handled by `IsReferenceType` + // 3. the file is custom code (not generated and not shared) which is handled by `IsCustomDocument` + return IsMgmtRootDocument(document) || IsReferenceType(root) || base.IsRootDocument(document); + } + + private static bool IsMgmtRootDocument(Document document) => GeneratedCodeWorkspace.IsGeneratedDocument(document) && Path.GetDirectoryName(document.Name) is "Extensions" or ""; + + private static HashSet _referenceAttributes = new HashSet { "ReferenceType", "PropertyReferenceType", "TypeReferenceType" }; + + private static bool IsReferenceType(SyntaxNode? root) + { + if (root is null) + return false; + + var childNodes = root.DescendantNodes(); + var typeNode = childNodes.OfType().FirstOrDefault(); + if (typeNode is null) + { + return false; + } + + var attributeLists = GetAttributeLists(typeNode); + if (attributeLists is null || attributeLists.Value.Count == 0) + return false; + + foreach (var attributeList in attributeLists.Value) + { + if (_referenceAttributes.Contains(attributeList.Attributes[0].Name.ToString())) + return true; + } + + return false; + } + + private static SyntaxList? GetAttributeLists(SyntaxNode node) + { + if (node is StructDeclarationSyntax structDeclaration) + return structDeclaration.AttributeLists; + + if (node is ClassDeclarationSyntax classDeclaration) + return classDeclaration.AttributeLists; + + return null; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/BodyParameterNormalizer.cs b/logger/autorest.csharp/mgmt/Decorator/BodyParameterNormalizer.cs new file mode 100644 index 0000000..90d11db --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/BodyParameterNormalizer.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Decorator.Transformer; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Utilities; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class BodyParameterNormalizer + { + private static readonly string Content = "Content"; + + internal static void Update(RequestMethod method, InputParameter bodyParameter, string resourceName, InputOperation operation, List updatedTypes) + { + if (method == RequestMethod.Put) + { + UpdateRequestParameter(bodyParameter, "content", $"{resourceName}CreateOrUpdateContent", operation, updatedTypes); + } + else if (method == RequestMethod.Patch) + { + UpdateRequestParameter(bodyParameter, "patch", $"{resourceName}Patch", operation, updatedTypes); + } + } + + internal static void UpdateUsingReplacement(InputParameter bodyParameter, IDictionary> resourceDataDictionary, InputOperation operation, List updatedTypes) + { + var schemaName = bodyParameter.Type.GetImplementType().Name; + if (schemaName.EndsWith("Parameters", StringComparison.Ordinal)) + schemaName = schemaName.ReplaceLast("Parameters", Content); + if (schemaName.EndsWith("Request", StringComparison.Ordinal)) + schemaName = schemaName.ReplaceLast("Request", Content); + if (schemaName.EndsWith("Options", StringComparison.Ordinal)) + schemaName = schemaName.ReplaceLast("Options", Content); + if (schemaName.EndsWith("Info", StringComparison.Ordinal)) + schemaName = schemaName.ReplaceLast("Info", Content); + if (schemaName.EndsWith("Input", StringComparison.Ordinal)) + schemaName = schemaName.ReplaceLast("Input", Content); + var paramName = NormalizeParamNames.GetNewName(bodyParameter.Name, schemaName, resourceDataDictionary); + // TODO -- we need to add a check here to see if this rename introduces parameter name collisions + UpdateRequestParameter(bodyParameter, paramName, schemaName, operation, updatedTypes); + } + + internal static void UpdateParameterNameOnly(InputParameter bodyParam, IDictionary> resourceDataDictionary, InputOperation operation) + { + string oriName = bodyParam.Name; + bodyParam.Name = NormalizeParamNames.GetNewName(bodyParam.Name, bodyParam.Type.GetImplementType().Name, resourceDataDictionary); + string fullSerializedName = operation.GetFullSerializedName(bodyParam); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.UpdateBodyParameter, fullSerializedName), + fullSerializedName, "UpdateParameterNameOnly", oriName, bodyParam.Name); + } + + private static void UpdateRequestParameter(InputParameter parameter, string parameterName, string schemaName, InputOperation operation, List updatedTypes) + { + string oriParameterName = parameter.Name; + parameter.Name = parameterName; + string fullSerializedName = operation.GetFullSerializedName(parameter); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.UpdateBodyParameter, fullSerializedName), + fullSerializedName, "UpdateParameterName", oriParameterName, parameter.Name); + + InputType parameterType = parameter.Type.GetImplementType(); + string oriSchemaName = parameterType.Name; + if (oriSchemaName != schemaName) + { + // we only need to update the schema name if it is a model or enum type + if (parameterType is InputModelType || parameter.Type is InputEnumType) + { + updatedTypes.Add(parameterType); + } + parameterType.Name = schemaName; + fullSerializedName = parameterType.GetFullSerializedName(); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.UpdateBodyParameter, fullSerializedName), + fullSerializedName, "UpdateParameterSchemaName", oriSchemaName, parameterType.Name); + } + + if (parameter.Type is InputEnumType || parameter.Type is InputModelType) + SchemaNameAndFormatUpdater.UpdateAcronym(parameter.Type); + } + + internal static void MakeRequired(InputParameter bodyParameter, RequestMethod method) + { + if (ShouldMarkRequired(method)) + { + bodyParameter.IsRequired = true; + } + } + + private static bool ShouldMarkRequired(RequestMethod method) => MethodsRequiredBodyParameter.Contains(method); + + private static readonly RequestMethod[] MethodsRequiredBodyParameter = new[] { RequestMethod.Put, RequestMethod.Patch }; + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/CodeModelTransformer.cs b/logger/autorest.csharp/mgmt/Decorator/CodeModelTransformer.cs new file mode 100644 index 0000000..1f12748 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/CodeModelTransformer.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Decorator; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.Decorator.Transformer; +using Humanizer.Inflections; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class CodeModelTransformer + { + public static void TransformForDataPlane(CodeModel codeModel) + { + SchemaUsageTransformer.Transform(codeModel); + ConstantSchemaTransformer.Transform(codeModel); + ModelPropertyClientDefaultValueTransformer.Transform(codeModel); + } + + private static void ApplyGlobalConfigurations() + { + foreach ((var word, var plural) in Configuration.MgmtConfiguration.IrregularPluralWords) + { + Vocabularies.Default.AddIrregular(word, plural); + } + } + + public static void TransformForMgmt(CodeModel codeModel) + { + ApplyGlobalConfigurations(); + + // schema usage transformer must run first + SchemaUsageTransformer.Transform(codeModel); + OmitOperationGroups.RemoveOperationGroups(codeModel); + SubscriptionIdUpdater.Update(codeModel); + ConstantSchemaTransformer.Transform(codeModel); + CommonSingleWordModels.Update(codeModel); + SchemaNameAndFormatUpdater.ApplyRenameMapping(codeModel); + SchemaNameAndFormatUpdater.UpdateAcronyms(codeModel); + UrlToUri.UpdateSuffix(codeModel); + FrameworkTypeUpdater.ValidateAndUpdate(codeModel); + SchemaFormatByNameTransformer.Update(codeModel); + SealedChoicesUpdater.UpdateSealChoiceTypes(codeModel); + RenameTimeToOn.Update(codeModel); + RearrangeParameterOrder.Update(codeModel); + RenamePluralEnums.Update(codeModel); + DuplicateSchemaResolver.ResolveDuplicates(codeModel); + + if (Configuration.MgmtConfiguration.MgmtDebug.ShowSerializedNames) + { + SerializedNamesUpdater.Update(codeModel); + } + //eliminate client default value from property + ModelPropertyClientDefaultValueTransformer.Transform(codeModel); + + CodeModelValidator.Validate(codeModel); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/EnumerableExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/EnumerableExtensions.cs new file mode 100644 index 0000000..3a6b973 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/EnumerableExtensions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + public static class EnumerableExtensions + { + public static IEnumerable AsIEnumerable(this T item) + { + yield return item; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/InheritanceChooser.cs b/logger/autorest.csharp/mgmt/Decorator/InheritanceChooser.cs new file mode 100644 index 0000000..3928585 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/InheritanceChooser.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class InheritanceChooser + { + internal const string ReferenceAttributeName = "ReferenceTypeAttribute"; + internal const string OptionalPropertiesName = "OptionalProperties"; + + private static ConcurrentDictionary _valueCache = new ConcurrentDictionary(); + + public static CSharpType? GetExactMatch(MgmtObjectType originalType, IReadOnlyList properties) + { + if (_valueCache.TryGetValue(originalType.InputModel, out var result)) + return result; + + foreach (var parentType in ReferenceClassFinder.ReferenceTypes) + { + var parentProperties = GetParentPropertiesToCompare(parentType, properties); + if (PropertyMatchDetection.IsEqual(parentType, originalType, parentProperties, properties.ToList())) + { + result = GetCSharpType(parentType); + _valueCache.TryAdd(originalType.InputModel, result); + return result; + } + } + _valueCache.TryAdd(originalType.InputModel, null); + return null; + } + + public static CSharpType? GetSupersetMatch(MgmtObjectType originalType, IReadOnlyList properties) + { + foreach (var parentType in ReferenceClassFinder.ReferenceTypes) + { + if (IsSuperset(parentType, originalType, properties)) + { + return GetCSharpType(parentType); + } + } + return null; + } + + private static CSharpType GetCSharpType(Type parentType) + { + return CSharpType.FromSystemType(MgmtContext.Context, parentType); + } + + private static List GetParentPropertiesToCompare(Type parentType, IReadOnlyList properties) + { + var propertyNames = properties.Select(p => p.Declaration.Name).ToHashSet(); + var attributeObj = parentType.GetCustomAttributes().Where(a => a.GetType().Name == ReferenceAttributeName).FirstOrDefault(); + var optionalPropertiesForMatch = (attributeObj?.GetType().GetProperty(OptionalPropertiesName)?.GetValue(attributeObj) as string[] ?? Array.Empty()).ToHashSet(); + List parentProperties = parentType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => !optionalPropertiesForMatch.Contains(p.Name) || propertyNames.Contains(p.Name)).ToList(); + return parentProperties; + } + + private static bool IsSuperset(Type parentType, MgmtObjectType originalType, IReadOnlyList properties) + { + var childProperties = properties.ToList(); + List parentProperties = GetParentPropertiesToCompare(parentType, properties); + if (parentProperties.Count >= childProperties.Count) + return false; + + Dictionary parentDict = new Dictionary(); + int matchCount = 0; + foreach (var parentProperty in parentProperties) + { + parentDict.Add(parentProperty.Name, parentProperty); + } + + foreach (var childProperty in childProperties) + { + if (parentProperties.Count == matchCount) + break; + + if (PropertyMatchDetection.DoesPropertyExistInParent(parentType, originalType, childProperty, parentDict)) + matchCount++; + } + + return parentProperties.Count == matchCount; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/MethodExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/MethodExtensions.cs new file mode 100644 index 0000000..aed9a62 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/MethodExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class MethodExtensions + { + /// + /// Return true if this operation is a list method. Also returns the itemType of what this operation is listing of, and the property name on the list result model. + /// This function will return true in the following circumstances: + /// 1. This operation is a paging method, in this case, valuePropertyName is the same as configured in the pageable options (x-ms-pageable) + /// 2. This operation is not a paging method, but the return value is a collection type (IReadOnlyList), in this case, valuePropertyName is the empty string + /// 3. This operation is not a paging method and the return value is not a collection type, but it has similar structure as paging method (has a value property, and value property is a collection) + /// + /// + /// + /// + /// + internal static bool IsListMethod(this RestClientMethod method, [MaybeNullWhen(false)] out CSharpType itemType, [MaybeNullWhen(false)] out string valuePropertyName) + { + itemType = null; + valuePropertyName = null; + var returnType = method.ReturnType; + if (returnType == null) + return false; + if (returnType.IsFrameworkType || returnType.Implementation is not SchemaObjectType) + { + if (returnType.IsList) + { + itemType = returnType.Arguments[0]; + valuePropertyName = string.Empty; + return true; + } + } + else + { + valuePropertyName = method.Operation.Paging?.ItemName ?? "value"; + var schemaObject = (SchemaObjectType)returnType.Implementation; + itemType = GetValueProperty(schemaObject, valuePropertyName)?.ValueType.Arguments.FirstOrDefault(); + } + return itemType != null; + } + + /// + /// Return true if this operation is a list method. Also returns the itemType of what this operation is listing of. + /// This function will return true in the following circumstances: + /// 1. This operation is a paging method. + /// 2. This operation is not a paging method, but the return value is a collection type (IReadOnlyList) + /// 3. This operation is not a paging method and the return value is not a collection type, but it has similar structure as paging method (has a value property, and value property is a collection) + /// + /// + /// The type of the item in the collection + /// + public static bool IsListMethod(this RestClientMethod method, [MaybeNullWhen(false)] out CSharpType itemType) => IsListMethod(method, out itemType, out _); + + + private static ObjectTypeProperty? GetValueProperty(SchemaObjectType schemaObject, string pageingItemName) + { + return schemaObject.Properties.FirstOrDefault(p => p.InputModelProperty?.SerializedName == pageingItemName && + (p.InputModelProperty?.FlattenedNames is null || p.InputModelProperty?.FlattenedNames?.Count == 0) && p.Declaration.Type.IsFrameworkType && + p.Declaration.Type.IsList); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/OperationExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/OperationExtensions.cs new file mode 100644 index 0000000..8127ff0 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/OperationExtensions.cs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Utilities; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class OperationExtensions + { + private static readonly ConcurrentDictionary _operationIdCache = new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary<(InputOperation, ResourceTypeSegment?), RequestPath> _operationToRequestPathCache = new ConcurrentDictionary<(InputOperation, ResourceTypeSegment?), RequestPath>(); + + private static readonly ConcurrentDictionary> _operationToResourceCache = new ConcurrentDictionary>(); + + /// + /// Returns the CSharpName of an operation in management plane pattern where we replace the word List with Get or GetAll depending on if there are following words + /// + /// + /// + /// + public static string MgmtCSharpName(this InputOperation operation, bool hasSuffix) + { + var originalName = operation.CleanName; + var words = originalName.SplitByCamelCase(); + if (!words.First().Equals("List", StringComparison.InvariantCultureIgnoreCase)) + return originalName; + words = words.Skip(1); // remove the word List + if (words.Any() && words.First().Equals("All", StringComparison.InvariantCultureIgnoreCase)) + words = words.Skip(1); + hasSuffix = hasSuffix || words.Any(); + var wordToReplace = hasSuffix ? "Get" : "GetAll"; + var replacedWords = wordToReplace.AsIEnumerable().Concat(words); + return string.Join("", replacedWords); + } + + /// + /// Search the configuration for an overridden of this operation's name + /// + /// + /// + /// + /// + public static bool TryGetConfigOperationName(this InputOperation operation, [MaybeNullWhen(false)] out string name) + { + if (Configuration.MgmtConfiguration.OverrideOperationName.TryGetValue(operation.OperationId!, out name)) + { + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.OverrideOperationName, operation.OperationId!, name), + operation.GetFullSerializedName(), + "OverrideOperationName", operation.Name, name); + return true; + } + + if (operation.IsNameChanged) + { + name = operation.Name; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.OverrideOperationName, operation.OperationId!, name), + operation.GetFullSerializedName(), + "OverrideOperationName", operation.Name, name); + return true; + } + + return false; + } + + public static RequestPath GetRequestPath(this InputOperation operation, ResourceTypeSegment? hint = null) + { + if (_operationToRequestPathCache.TryGetValue((operation, hint), out var requestPath)) + return requestPath; + + requestPath = MgmtContext.Library.GetRequestPath(operation); + if (hint.HasValue) + requestPath = requestPath.ApplyHint(hint.Value); + + _operationToRequestPathCache.TryAdd((operation, hint), requestPath); + return requestPath; + } + + public static bool IsResourceCollectionOperation(this InputOperation operation, [MaybeNullWhen(false)] out OperationSet operationSetOfResource) + { + operationSetOfResource = null; + // first we need to ensure this operation at least returns a collection of something + var restClientMethod = MgmtContext.Library.GetRestClientMethod(operation); + if (!restClientMethod.IsListMethod(out var valueType)) + return false; + + // then check if its path is a prefix of which resource's operationSet + // if there are multiple resources that share the same prefix of request path, we choose the shortest one + var requestPath = operation.GetRequestPath(); + operationSetOfResource = FindOperationSetOfResource(requestPath); + // if we find none, this cannot be a resource collection operation + if (operationSetOfResource is null) + return false; + + // then check if this method returns a collection of the corresponding resource data + // check if valueType is the current resource data type + var resourceData = MgmtContext.Library.GetResourceData(operationSetOfResource.RequestPath); + return valueType.AreNamesEqual(resourceData.Type); + } + + private static OperationSet? FindOperationSetOfResource(RequestPath requestPath) + { + if (Configuration.MgmtConfiguration.RequestPathToParent.TryGetValue(requestPath, out var rawPath)) + return MgmtContext.Library.GetOperationSet(rawPath); + var candidates = new List(); + // we need to iterate all resources to find if this is the parent of that + foreach (var operationSet in MgmtContext.Library.ResourceOperationSets) + { + var resourceRequestPath = operationSet.GetRequestPath(); + // we compare the request with the resource request in two parts: + // 1. Compare if they have the same scope + // 2. Compare if they have the "compatible" remaining path + // check if they have compatible scopes + if (!RequestPath.IsScopeCompatible(requestPath, resourceRequestPath)) + continue; + // check the remaining path + var trimmedRequestPath = requestPath.TrimScope(); + var trimmedResourceRequestPath = resourceRequestPath.TrimScope(); + // For a path of a scope like /subscriptions/{subscriptionId}/resourcegroups, the trimmed path is empty. The path of its resource should also be a scope, its trimmed path should also be empty. + if (trimmedRequestPath.Count == 0 && trimmedResourceRequestPath.Count != 0) + continue; + // In the case that the full path of requestPath and resourceRequestPath are both scopes (trimmed path is empty), comparing the scope part is enough. + // We should not compare the remaining paths as both will be empty path and Tenant.IsAncestorOf(Tenant) always returns false. + else if (trimmedRequestPath.Count != 0 || trimmedResourceRequestPath.Count != 0) + { + if (!trimmedRequestPath.IsAncestorOf(trimmedResourceRequestPath)) + continue; + } + candidates.Add(operationSet); + } + + if (candidates.Count == 0) + return null; + + // choose the toppest of the rank + return candidates.OrderBy(operationSet => RankRequestPath(operationSet.GetRequestPath())).First(); + } + + /// + /// Rank the request path to serve that which request path to choose. + /// For normal request paths, we just return its count, and we choose the shortest one. + /// For request paths with parameterized scope, we rank it as 0 so that it will always be the first. + /// For request paths that only accepts an Id, we rank it as int.MaxValue so that it will always be our last choice + /// + /// + /// + private static int RankRequestPath(RequestPath requestPath) + { + if (requestPath.IsById) + return int.MaxValue; + if (requestPath.GetScopePath().IsParameterizedScope()) + return 0; + return requestPath.Count; + } + + public static string GetHttpPath(this InputOperation operation) + { + var path = operation.Path; + // Do not trim the tenant resource path '/'. + return (path?.Length == 1 ? path : path?.TrimEnd('/')) ?? + throw new InvalidOperationException($"Cannot get HTTP path from operation {operation.CleanName}"); + } + + public static HttpRequest? GetHttpRequest(this Operation operation) + { + foreach (var request in operation.Requests) + { + var httpRequest = request.Protocol.Http as HttpRequest; + if (httpRequest is not null) + return httpRequest; + } + + return null; + } + + public static RequestParameter? GetBodyParameter(this Operation operation) + { + var serviceRequest = operation.GetServiceRequest(); + return serviceRequest?.Parameters.FirstOrDefault(parameter => parameter.In == HttpParameterIn.Body); + } + + public static InputParameter? GetBodyParameter(this InputOperation operation) + => operation.Parameters.FirstOrDefault(parameter => parameter.Location == RequestLocation.Body); + + public static ServiceRequest? GetServiceRequest(this Operation operation) + { + return operation.Requests.FirstOrDefault(); + } + + public static OperationResponse? GetServiceResponse(this InputOperation operation, int code = 200) + { + return operation.Responses.FirstOrDefault(r => r.StatusCodes.Contains(code)); + } + + public static bool IsGetResourceOperation(this InputOperation operation, string? responseBodyType, ResourceData resourceData) + { + // first we need to be a GET operation + if (operation.HttpMethod != RequestMethod.Get) + return false; + // then we get the corresponding OperationSet and see if this OperationSet corresponds to a resource + var operationSet = MgmtContext.Library.GetOperationSet(operation.GetHttpPath()); + if (!operationSet.IsResource()) + return false; + return responseBodyType == resourceData.Type.Name; + } + + internal static IEnumerable GetResourceFromResourceType(this InputOperation operation) + { + if (_operationToResourceCache.TryGetValue(operation, out var cacheResult)) + return cacheResult; + + // we expand the path here to ensure the resource types we are dealing with here are all constants (at least ensure they are constants when we are expecting to find a resource) + var requestPaths = operation.GetRequestPath().Expand(); + var candidates = new List(); + foreach (var path in requestPaths) + { + var resourceType = path.GetResourceType(); + // we find the resource with the same type of this operation, and under the same scope + var resources = MgmtContext.Library.ArmResources.Where(resource => resource.ResourceType.DoesMatch(resourceType) && resource.RequestPath.GetScopePath().Equals(path.GetScopePath())); + candidates.AddRange(resources); + } + + return candidates; + } + + internal static string GetFullSerializedName(this OperationGroup operationGroup) + { + return operationGroup.Language.Default.SerializedName ?? operationGroup.Language.Default.Name; + } + + internal static string GetFullSerializedName(this Operation operation) + { + return operation.OperationId ?? operation.Language.Default.SerializedName ?? operation.Language.Default.Name; + } + + internal static string GetFullSerializedName(this InputOperation operation) + { + return operation.SpecName; + } + + internal static string GetFullSerializedName(this InputOperation operation, InputParameter parameter) + { + return $"{operation.GetFullSerializedName()}.{parameter.NameInRequest}"; + } + + internal static string GetFullSerializedName(this Operation operation, RequestParameter parameter) + { + return $"{operation.GetFullSerializedName()}.{parameter.GetOriginalName()}"; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ParameterMappingBuilder.cs b/logger/autorest.csharp/mgmt/Decorator/ParameterMappingBuilder.cs new file mode 100644 index 0000000..1b58ab5 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ParameterMappingBuilder.cs @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class ParameterMappingBuilder + { + /// + /// Builds the parameter mapping for contextual paths. The parameters in the contextual path will be treated as "known information" + /// when writing other operations in the same resource or resource collection class and be passed into the corresponding RestOperation + /// method using their "value expression"s + /// + /// The contextual path, which is usually the path creating a resource + /// The variable name of the Id variable + /// + public static IEnumerable BuildContextualParameters(this RequestPath requestPath, FormattableString idVariableName) + { + var stack = new Stack(); + BuildContextualParameterMappingHierarchy(requestPath, stack, idVariableName); + return stack; + } + + private static void BuildContextualParameterMappingHierarchy(RequestPath current, Stack parameterMappingStack, FormattableString idVariableName, string invocationSuffix = "") + { + // Check if the current path is a scope parameter + if (current.IsRawParameterizedScope()) + { + // in this case, we should only have one segment in this current path + parameterMappingStack.Push(new ContextualParameterMapping(string.Empty, current.Last(), $"{idVariableName}{invocationSuffix}")); + return; + } + // RequestPath of tenant does not have any parameter in it (actually it does not have anything), we take this as an exit + if (current == RequestPath.Tenant) + return; + var parent = current.ParentRequestPath(); + // Subscription and ManagementGroup are not terminal states - tenant is their parent + if (current == RequestPath.Subscription) + { + // using the reference name of the last segment as the parameter name, aka, subscriptionId + parameterMappingStack.Push(new ContextualParameterMapping(current.SkipLast(1).Last().ConstantValue, current.Last(), $"{idVariableName}.SubscriptionId")); + } + else if (current == RequestPath.ManagementGroup) + { + // using the reference name of the last segment as the parameter name, aka, groupId + parameterMappingStack.Push(new ContextualParameterMapping(current.SkipLast(1).Last().ConstantValue, current.Last(), $"{idVariableName}{invocationSuffix}.Name")); + } + // ResourceGroup is not terminal state - Subscription is its parent + else if (current == RequestPath.ResourceGroup) + { + // using the reference name of the last segment as the parameter name, aka, resourceGroupName + parameterMappingStack.Push(new ContextualParameterMapping(current.SkipLast(1).Last().ConstantValue, current.Last(), $"{idVariableName}.ResourceGroupName")); + } + // this branch is for every other cases - all the request path that corresponds to a resource in this swagger + else + { + // get the diff between current and parent + var diffPath = parent.TrimAncestorFrom(current); + // get the segment in pairs + var segmentPairs = SplitDiffIntoPairs(diffPath).ToList(); + var indexOfProvidersPair = segmentPairs.FindIndex(pair => pair[0] == Segment.Providers); + var resourceTypeIdVariableName = idVariableName; + // from the tail, check these segments in pairs + for (int i = 0; i < segmentPairs.Count; i++) + { + var pair = segmentPairs[i]; + if (pair.Count == 2) + { + // we have a pair of segment, therefore here pair[0] will always be the key, `resourceGroups` for instance. + // The key can also be variable in some scenarios + // pair[1] will always be the value, which is Id.Name or Id.Namespace (if its key is providers) + var keySegment = pair[0]; + var valueSegment = pair[1]; + var appendParent = false; + if (valueSegment.IsReference) + { + if (keySegment == Segment.Providers) // if the key is providers and the value is a parameter + { + if (current.Count <= 4) // path is /providers/{resourceProviderNamespace} or /subscriptions/{subscriptionId}/providers/{resourceProviderNamespace} + { + parameterMappingStack.Push(new ContextualParameterMapping(keySegment.ConstantValue, valueSegment, $"{idVariableName}.Provider")); + } + else + { + parameterMappingStack.Push(new ContextualParameterMapping(keySegment.ConstantValue, valueSegment, $"{resourceTypeIdVariableName}.ResourceType.Namespace")); + } + // do not append a new .Parent to the id + } + else // for all other normal keys + { + parameterMappingStack.Push(new ContextualParameterMapping(keySegment.IsConstant ? keySegment.ConstantValue : string.Empty, valueSegment, $"{idVariableName}{invocationSuffix}.Name")); + appendParent = true; + } + } + else // in this branch pair[1] is a constant + { + if (keySegment != Segment.Providers) + { + // if the key is not providers, we need to skip this level and increment the parent hierarchy + appendParent = true; + } + } + if (keySegment.IsReference) + { + parameterMappingStack.Push(new ContextualParameterMapping(string.Empty, keySegment, $"{idVariableName}{invocationSuffix}.ResourceType.GetLastType()", new[] { "System.Linq" })); + resourceTypeIdVariableName = $"{idVariableName}{invocationSuffix}"; + appendParent = true; + } + else if (keySegment.IsExpandable) + { + //this is the case where we have expanded the reference into its enumerations + var keyParam = keySegment.Type.Name.ToVariableName(); + parameterMappingStack.Push(new ContextualParameterMapping(keyParam, keyParam, keySegment.Type, $"\"{keySegment.ConstantValue}\"", Enumerable.Empty())); + appendParent = true; + } + // add .Parent suffix + if (appendParent) + invocationSuffix += ".Parent"; + } + else + { + if (pair[0].IsReference && pair[0].SkipUrlEncoding) + { + // we never have a case that we need to get the substring that have a gap after the provider-namespace key pair, throw an exception when it happens + if (segmentPairs.Count - indexOfProvidersPair != 1) + throw new NotImplementedException("We have a gap between the substring to get and the provider-namespace key pair. We need to update SubstringAfterProviderNamespace function to make sure it can accept an index to adopt this"); + // if we only have one segment in this group, it should always be a reference + parameterMappingStack.Push(new ContextualParameterMapping(string.Empty, pair[0], $"{idVariableName}{invocationSuffix}.SubstringAfterProviderNamespace()")); + } + } + } + } + // recursively get the parameters of its parent + BuildContextualParameterMappingHierarchy(parent, parameterMappingStack, idVariableName, invocationSuffix); + } + + /// + /// This bases on the fact that the contextual path should always be a resource identifier in its value, + /// therefore we should always have the ability to split the contextual path into pairs. + /// But the request path has variables in it, therefore we need to split the diff into pairs considering that some segment might have the x-ms-skip-url-encoding = true, + /// which means it can be not only a single variable, but also at least a subset of a resource ID (virtualMachines/myVM for instance) + /// If we have two segments with all x-ms-skip-url-encoding = false, they should be able to go into pairs + /// A segment with x-ms-skip-url-encoding = true has the ability to go alone, since it could have multiple segments in its value. + /// How many segment could go solo? Say we have a total number of X segments with x-ms-skip-url-encoding = true + /// and N is the total number of the segments. + /// If N is an odd number, we must have an odd number of segments that go solo. + /// If N is an even number, we must have an even number of segments that go solo. (zero is an even number) + /// + /// + /// + private static IEnumerable> SplitDiffIntoPairs(RequestPath diff) + { + // if N is odd, we allow 1 segment to go alone. if N is even, we allow 0 segments to go alone + int maximumNumberOfAloneSegments = diff.Count % 2 == 0 ? 0 : 1; + var result = new Stack>(); + var indices = new List(); + for (int i = 0; i < diff.Count; i++) + { + var current = diff[i]; + if (current.IsConstant || !current.SkipUrlEncoding || maximumNumberOfAloneSegments == 0) + { + // key is constant, or key is a reference but it is not enabling `x-ms-skip-url-encoding`, we could include a pair + if (i + 1 < diff.Count) + { + result.Push(new List { diff[i], diff[i + 1] }); + i++; + } + else + { + result.Push(new List { diff[i] }); + } + continue; + } + if (current.SkipUrlEncoding && maximumNumberOfAloneSegments > 0) + { + result.Push(new List { diff[i] }); + maximumNumberOfAloneSegments--; + } + } + + return result; + } + + public static FormattableString GetValueExpression(CSharpType type, FormattableString rawExpression) + { + // if the type is string + if (type.EqualsIgnoreNullable(typeof(string))) + return rawExpression; + + // or if the type is extensible enum and its underlying type is also string + if (type is { IsFrameworkType: false, Implementation: EnumType { IsExtensible: true, ValueType: { } enumValueType } } && enumValueType.EqualsIgnoreNullable(typeof(string))) + return rawExpression; + + if (!type.IsFrameworkType) + { + if (type.Implementation is EnumType { IsExtensible: false } enumType) + { + return $"{rawExpression}.To{enumType.Declaration.Name}()"; + } + throw new InvalidOperationException($"Type {type} is not supported to construct parameter mapping"); + } + // TODO: The deserialize type value logic is existing in multiple writers, similar but slightly different, + // should be abstracted into one place in future refactoring. + if (type.FrameworkType == typeof(Azure.ETag) || + type.FrameworkType == typeof(Uri) || + type.FrameworkType == typeof(Azure.Core.ResourceIdentifier) || + type.FrameworkType == typeof(Azure.Core.ResourceType) || + type.FrameworkType == typeof(Azure.Core.ContentType) || + type.FrameworkType == typeof(Azure.Core.RequestMethod) || + type.FrameworkType == typeof(Azure.Core.AzureLocation)) + { + return $"new {type.FrameworkType}({rawExpression})"; + } + + return $"{type.FrameworkType}.Parse({rawExpression})"; + } + + /// + /// Represents how a parameter of rest operation is mapped to a parameter of a collection method or an expression. + /// + public record ContextualParameterMapping + { + public string Key; + /// + /// The parameter name + /// + public string ParameterName; + /// + /// The parameter type + /// + public CSharpType ParameterType; + /// + /// This is the value expression to pass in a method + /// + public FormattableString ValueExpression; + /// + /// The using statements in the ValueExpression + /// + public IEnumerable Usings; + + public ContextualParameterMapping(string key, Segment value, FormattableString valueExpression, IEnumerable? usings = default) + : this(key, value.Reference.Name, value.Reference.Type, valueExpression, usings ?? Enumerable.Empty()) + { + } + + internal ContextualParameterMapping(string key, string parameterName, CSharpType parameterType, FormattableString valueExpression, IEnumerable usings) + { + Key = key; + ParameterName = parameterName; + ParameterType = parameterType; + ValueExpression = GetValueExpression(parameterType, valueExpression); + Usings = usings; + } + + /// + /// Returns true if the given can match this + /// + /// + /// + public bool MatchesParameter(string key, Parameter parameter) + { + return key.Equals(Key, StringComparison.InvariantCultureIgnoreCase) && ParameterType.Equals(parameter.Type); + } + } + + public static IEnumerable BuildParameterMapping(this MgmtRestOperation operation, IEnumerable contextualParameterMappings) + { + var method = operation.Method; + var contextualParameterMappingCache = new List(contextualParameterMappings); + foreach (var parameter in method.Parameters) + { + // find this parameter name in the contextual parameter mappings + // if there is one, this parameter should use the same value expression + // if there is none of this, this parameter should be a pass through parameter + var mapping = FindContextualParameterForMethod(parameter, operation.RequestPath, contextualParameterMappingCache); + // Update parameter type if the method is a `ById` method + var p = UpdateParameterTypeOfByIdMethod(operation.RequestPath, parameter); + if (mapping == null) + { + yield return new ParameterMapping(p, true, $"", Enumerable.Empty()); + } + else + { + yield return new ParameterMapping(p, false, mapping.ValueExpression, mapping.Usings); + } + } + } + + private static Parameter UpdateParameterTypeOfByIdMethod(RequestPath requestPath, Parameter parameter) + { + if (requestPath.IsById) + { + var reference = requestPath.First().Reference; + if (parameter.Name.Equals(reference.Name, StringComparison.InvariantCultureIgnoreCase) && parameter.Type.AreNamesEqual(reference.Type)) + { + return parameter with { Type = typeof(Azure.Core.ResourceIdentifier) }; + } + } + + return parameter; + } + + /// + /// Represents how a parameter of rest operation is mapped to a parameter of a collection method or an expression. + /// + public record ParameterMapping + { + /// + /// The parameter object in . + /// + public Parameter Parameter; + /// + /// Should the parameter be passed through from the method in collection class? + /// + public bool IsPassThru; + /// + /// if not pass-through, this is the value to pass in . + /// + public FormattableString ValueExpression; + /// + /// the using statements used in the ValueExpression + /// + public IEnumerable Usings; + + public ParameterMapping(Parameter parameter, bool isPassThru, FormattableString valueExpression, IEnumerable usings) + { + Parameter = parameter; + IsPassThru = isPassThru; + ValueExpression = valueExpression; + Usings = usings; + } + } + + private static ContextualParameterMapping? FindContextualParameterForMethod(Parameter pathParameter, RequestPath requestPath, List contextualParameterMappings) + { + // skip non-path parameters + if (pathParameter.RequestLocation != RequestLocation.Path) + return null; + var result = contextualParameterMappings.FirstOrDefault(mapping => mapping.MatchesParameter(FindKeyOfParameter(pathParameter, requestPath), pathParameter)); + // if we match one parameter, we need to remove the matching ContextualParameterMapping from the list to avoid multiple matching + if (result != null) + contextualParameterMappings.Remove(result); + if (result is null && pathParameter.Type.IsEnum) + { + var requestSegment = requestPath.Where(s => s.IsExpandable && s.Type.Equals(pathParameter.Type) && s.IsConstant); + if (requestSegment.Any()) + { + var keySegment = requestSegment.First(); + var keyParam = keySegment.Type.Name.ToVariableName(); + return new ContextualParameterMapping(keyParam, keyParam, keySegment.Type, $"\"{keySegment.ConstantValue}\"", Enumerable.Empty()); + } + } + return result; + } + + public static string FindKeyOfParameter(Reference reference, RequestPath requestPath) + { + var segments = requestPath.ToList(); + int index = segments.FindIndex(segment => + { + if (segment.IsReference && segment.ReferenceName == reference.Name && segment.Type.Equals(reference.Type)) + return true; + if (segment.IsExpandable && segment.Type.Equals(reference.Type)) + return true; + + return false; + }); + if (index < 0) + throw new InvalidOperationException($"Cannot find the key corresponding to parameter {reference.Name} in path {requestPath}"); + + if (index == 0) + return string.Empty; + + if (segments[index].IsExpandable) + return segments[index].Type.Name.ToVariableName(); + + var keySegment = segments[index - 1]; + return keySegment.IsConstant ? keySegment.ConstantValue : string.Empty; + } + + public static List GetPassThroughParameters(this IEnumerable parameterMappings) + { + return parameterMappings.Where(p => p.IsPassThru).Select(p => p.Parameter).ToList(); + } + + public static string GetPropertyBagValueExpression(this Parameter parameter) + { + return $"options.{parameter.Name.FirstCharToUpperCase()}"; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ParentDetection.cs b/logger/autorest.csharp/mgmt/Decorator/ParentDetection.cs new file mode 100644 index 0000000..791ffdc --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ParentDetection.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using Azure.ResourceManager; +using Azure.ResourceManager.ManagementGroups; +using Azure.ResourceManager.Resources; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class ParentDetection + { + private static ConcurrentDictionary _requestPathToParentCache = new ConcurrentDictionary(); + private static ConcurrentDictionary _inputOperationToParentRequestPathCache = new ConcurrentDictionary(); + + private static ConcurrentDictionary> _resourceParentCache = new ConcurrentDictionary>(); + + /// + /// Returns the collection of the parent of the given resource. + /// This is not initialized while the TypeProviders are constructing and can only be used in the writers. + /// + /// + /// + public static IEnumerable GetParents(this Resource resource) + { + if (_resourceParentCache.TryGetValue(resource, out var parentList)) + return parentList; + + parentList = resource.DoGetParents(); + _resourceParentCache.TryAdd(resource, parentList); + return parentList; + } + + private static IEnumerable DoGetParents(this Resource resource) + { + var scope = resource.RequestPath.GetScopePath(); + var resourceOperationSet = resource.OperationSet; + var parentRequestPath = resourceOperationSet.ParentRequestPath(resource.ResourceType); + + if (parentRequestPath.Equals(resourceOperationSet.GetRequestPath())) + { + // my parent is myself? Only tenant has this attribute, return empty + return Enumerable.Empty(); + } + // if the scope of this request path is parameterized, and the direct parent path we get from the resource list is parent of the scope, we return the scope as its parent since the scope here is a child + // if the request path is a "by id" path, its scope is the same as itself, therefore this condition here is nullified and should be skipped + if (!resource.RequestPath.IsById && scope.IsParameterizedScope() && (parentRequestPath.IsAncestorOf(scope) || parentRequestPath == scope)) + { + // we already verified that the scope is parameterized, therefore we assert the type can never be null + var types = resource.RequestPath.GetParameterizedScopeResourceTypes()!; + return FindScopeParents(types).Distinct(); + } + + if (MgmtContext.Library.TryGetArmResource(parentRequestPath, out var parent)) + { + return parent.AsIEnumerable(); + } + // if we cannot find a resource as its parent, its parent must be one of the Extensions + if (parentRequestPath.Equals(RequestPath.ManagementGroup)) + return MgmtContext.Library.GetExtension(typeof(ManagementGroupResource)).AsIEnumerable(); + if (parentRequestPath.Equals(RequestPath.ResourceGroup)) + return MgmtContext.Library.GetExtension(typeof(ResourceGroupResource)).AsIEnumerable(); + if (parentRequestPath.Equals(RequestPath.Subscription)) + return MgmtContext.Library.GetExtension(typeof(SubscriptionResource)).AsIEnumerable(); + // the only option left is the tenant. But we have our last chance that its parent could be the scope of this + scope = parentRequestPath.GetScopePath(); // we do this because some request path its scope is the same as itself + if (scope.IsParameterizedScope()) + { + // we already verified that the scope is parameterized, therefore we assert the type can never be null + var types = resource.RequestPath.GetParameterizedScopeResourceTypes()!; + return FindScopeParents(types).Distinct(); + } + // otherwise we use the tenant as a fallback + return MgmtContext.Library.GetExtension(typeof(TenantResource)).AsIEnumerable(); + } + + // TODO -- enhence this to support the new arm-id format + private static IEnumerable FindScopeParents(ResourceTypeSegment[] parameterizedScopeTypes) + { + if (parameterizedScopeTypes.Contains(ResourceTypeSegment.Any)) + { + yield return MgmtContext.Library.GetExtension(typeof(ArmResource)); + yield break; + } + + foreach (var type in parameterizedScopeTypes) + { + if (type == ResourceTypeSegment.ManagementGroup) + yield return MgmtContext.Library.GetExtension(typeof(ManagementGroupResource)); + else if (type == ResourceTypeSegment.ResourceGroup) + yield return MgmtContext.Library.GetExtension(typeof(ResourceGroupResource)); + else if (type == ResourceTypeSegment.Subscription) + yield return MgmtContext.Library.GetExtension(typeof(SubscriptionResource)); + else if (type == ResourceTypeSegment.Tenant) + yield return MgmtContext.Library.GetExtension(typeof(TenantResource)); + else + yield return MgmtContext.Library.GetExtension(typeof(ArmResource)); // we return anything unrecognized scope parent resource type as ArmResourceExtension + } + } + + public static RequestPath ParentRequestPath(this OperationSet operationSet, ResourceTypeSegment resourceTypeHint) + { + // escape the calculation if this is configured in the configuration + if (Configuration.MgmtConfiguration.RequestPathToParent.TryGetValue(operationSet.RequestPath, out var rawPath)) + return GetRequestPathFromRawPath(rawPath); + + return operationSet.GetRequestPath(resourceTypeHint).ParentRequestPath(); + } + + private static RequestPath GetRequestPathFromRawPath(string rawPath) + { + var parentSet = MgmtContext.Library.GetOperationSet(rawPath); + return parentSet.GetRequestPath(); + } + + /// + /// This method gives the proper grouping of the given operation by testing the following: + /// 1. If this operation comes from a resource operation set, return the request path of the resource + /// 2. If this operation is a collection operation of a resource, return the request path of the resource + /// 3. If neither of above meets, return the parent request path of an existing resource + /// + /// + /// + public static RequestPath ParentRequestPath(this InputOperation operation, InputNamespace inputNamespace) + { + if (_inputOperationToParentRequestPathCache.TryGetValue(operation, out var result)) + return result; + + result = GetParentRequestPath(operation, inputNamespace); + _inputOperationToParentRequestPathCache.TryAdd(operation, result); + return result; + } + + private static RequestPath GetParentRequestPath(InputOperation operation, InputNamespace inputNamespace) + { + // escape the calculation if this is configured in the configuration + if (Configuration.MgmtConfiguration.RequestPathToParent.TryGetValue(operation.Path, out var rawPath)) + return GetRequestPathFromRawPath(rawPath); + + var currentRequestPath = operation.GetRequestPath(); + var currentOperationSet = MgmtContext.Library.GetOperationSet(currentRequestPath); + // if this operation comes from a resource, return itself + if (currentOperationSet.IsResource(inputNamespace)) + return currentRequestPath; + + // if this operation corresponds to a collection operation of a resource, return the path of the resource + if (operation.IsResourceCollectionOperation(out var operationSetOfResource)) + return operationSetOfResource.GetRequestPath(); + + // if neither of the above, we find a request path that is the longest parent of this, and belongs to a resource + return currentRequestPath.ParentRequestPath(); + } + + internal static RequestPath ParentRequestPath(this RequestPath requestPath) + { + if (_requestPathToParentCache.TryGetValue(requestPath, out var result)) + { + return result; + } + + result = requestPath.GetParent(); + _requestPathToParentCache.TryAdd(requestPath, result); + + return result; + } + + private static RequestPath GetParent(this RequestPath requestPath) + { + // find a parent resource in the resource list + // we are taking the resource with a path that is the child of this operationSet and taking the longest candidate + // or null if none matched + // NOTE that we are always using fuzzy match in the IsAncestorOf method, we need to block the ById operations - they literally can be anyone's ancestor when there is no better choice. + // We will never want this + var scope = requestPath.GetScopePath(); + var candidates = MgmtContext.Library.ResourceOperationSets.Select(operationSet => operationSet.GetRequestPath()) + .Concat(new List { RequestPath.ResourceGroup, RequestPath.Subscription, RequestPath.ManagementGroup }) // When generating management group in management.json, the path is /providers/Microsoft.Management/managementGroups/{groupId} while RequestPath.ManagementGroup is /providers/Microsoft.Management/managementGroups/{managementGroupId}. We pick the first one. + .Concat(Configuration.MgmtConfiguration.ParameterizedScopes) + .Where(r => r.IsAncestorOf(requestPath)).OrderByDescending(r => r.Count); + if (candidates.Any()) + { + var parent = candidates.First(); + if (parent == RequestPath.Tenant) + { + // when generating for tenant and a scope path like policy assignment in Azure.ResourceManager, Tenant could be the only parent in context.Library.ResourceOperationSets. + // we need to return the parameterized scope instead. + if (scope != requestPath && scope.IsParameterizedScope()) + parent = scope; + } + return parent; + } + // the only option left is the tenant. But we have our last chance that its parent could be the scope of this + // if the scope of this request path is parameterized, we return the scope as its parent + if (scope != requestPath && scope.IsParameterizedScope()) + return scope; + // we do not have much choice to make, return tenant as the parent + return RequestPath.Tenant; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/PropertyMatchDetection.cs b/logger/autorest.csharp/mgmt/Decorator/PropertyMatchDetection.cs new file mode 100644 index 0000000..e42db9c --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/PropertyMatchDetection.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class PropertyMatchDetection + { + internal static bool IsEqual(Type sourceType, MgmtObjectType targetType, List parentProperties, List childProperties, Dictionary? propertiesInComparison = null) + { + AddInternalIncludes(sourceType, parentProperties); + + bool allowExtra = GetAllowSetting(sourceType); + + if (parentProperties.Count != childProperties.Count && !allowExtra) + return false; + + int matchCount = 0; + Dictionary parentDict = new Dictionary(); + foreach (var parentProperty in parentProperties) + { + parentDict.Add(parentProperty.Name, parentProperty); + } + + foreach (var childProperty in childProperties) + { + if (!DoesPropertyExistInParent(sourceType, targetType, childProperty, parentDict, propertiesInComparison)) + { + if (allowExtra) + continue; + + return false; + } + + matchCount++; + } + + return matchCount == parentProperties.Count; + } + + private static bool GetAllowSetting(Type sourceType) + { + var attribute = sourceType.GetCustomAttributes(false).FirstOrDefault(a => a.GetType().Name.Equals(ReferenceClassFinder.TypeReferenceTypeAttributeName)); + var allowExtraValue = attribute?.GetType().GetProperty("IgnoreExtraProperties", BindingFlags.Instance | BindingFlags.Public)?.GetValue(attribute); + return allowExtraValue is null ? false : (bool)allowExtraValue; + } + + internal static void AddInternalIncludes(Type sourceType, List parentProperties) + { + // Both TypeReferenceTypeAttribute and PropertyReferenceTypeAttribute allow specifying internal properties to include + var referenceAttribute = sourceType.GetCustomAttributes(false) + .FirstOrDefault(a => a.GetType().Name is ReferenceClassFinder.TypeReferenceTypeAttributeName or ReferenceClassFinder.PropertyReferenceTypeAttributeName); + if (referenceAttribute is not null) + { + var internalToInclude = referenceAttribute.GetType().GetProperty("InternalPropertiesToInclude", BindingFlags.Instance | BindingFlags.Public)?.GetValue(referenceAttribute); + if (internalToInclude is not null) + { + foreach (var propertyName in (string[])internalToInclude) + { + var property = sourceType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic); + if (property is null) + throw new InvalidOperationException($"{sourceType.Name} listed {propertyName} as an internal property to include in the match but that property was not found."); + parentProperties.Add(property); + } + } + } + } + + /// + /// Check if a has the same properties as our own . + /// + /// from reflection. + /// A from M4 output. + /// + internal static bool IsEqual(Type sourceType, MgmtObjectType targetType) + { + var sourceTypeProperties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList(); + var targetTypeProperties = targetType.MyProperties.ToList(); + + return IsEqual(sourceType, targetType, sourceTypeProperties, targetTypeProperties, new Dictionary { { sourceType, targetType.Type } }); + } + + internal static bool DoesPropertyExistInParent(Type sourceType, MgmtObjectType targetType, ObjectTypeProperty childProperty, Dictionary parentDict, Dictionary? propertiesInComparison = null) + { + if (!parentDict.TryGetValue(childProperty.Declaration.Name, out var parentProperty)) + { + // If exact property name match fails, we match their serialized name + // first get the serialized name dict + if (ReferenceClassFinder.TryGetPropertyMetadata(sourceType, out var serializedNameDict)) + { + // find if any PropertyInfo in the serializedNameDict could match the serialized name as this childProperty + var childPropertySerializedName = childProperty.InputModelProperty!.SerializedName; + string? parentPropertyName = null; + foreach ((var propertyName, (var serializedName, _)) in serializedNameDict) + { + if (serializedName == childPropertySerializedName) + { + parentPropertyName = propertyName; + break; + } + } + if (parentPropertyName == null) + return false; + // we have a parentPropertyName + parentProperty = parentDict[parentPropertyName]; + } + else + { + // otherwise we always return false - they do not match + return false; + } + } + + // here we cannot find a property from its declared name + var childPropertyType = childProperty.Declaration.Type; + var isInternal = childProperty.Declaration.Accessibility == "internal"; + if (parentProperty.PropertyType.FullName == $"{childPropertyType.Namespace}.{childPropertyType.Name}" || + IsAssignable(parentProperty.PropertyType, childPropertyType)) + { + if (childProperty.IsReadOnly != parentProperty.IsReadOnly(allowInternal: isInternal)) + return false; + } + else if (!ArePropertyTypesMatch(sourceType, targetType, parentProperty.PropertyType!, childPropertyType, propertiesInComparison)) + { + return false; + } + + return true; + } + + private static bool ArePropertyTypesMatch(Type sourceType, MgmtObjectType targetType, Type parentPropertyType, CSharpType childPropertyType, Dictionary? propertiesInComparison = null) + { + if (DoesPropertyReferenceItself(sourceType, targetType, parentPropertyType, childPropertyType)) + { + //both reference themselves and in the case of a TypeReplacement this is equivalent + return true; + } + else if (IsGuidAndStringType(parentPropertyType!, childPropertyType!)) + { + return true; + } + else if (parentPropertyType.IsGenericType) + { + return IsMatchingGenericType(sourceType, targetType, parentPropertyType!, childPropertyType!, propertiesInComparison); + } + else if (IsAssignable(parentPropertyType!, childPropertyType)) + { + return true; + } + else if (parentPropertyType.FullName == $"{childPropertyType.Namespace}.{childPropertyType.Name}") + { + // This condition branch implies parentPropertyType is not a class because if it is a class, IsEqual() will always be called first and if the comparison for this branch is true, in DoesPropertyExistInParent, the branch for ArePropertyTypesMatch will never be called. + // It can be used to match strings. + return true; + } + // Need to compare subproperties recursively when the property Types have different names but should avoid infinite loop in cases like ErrorResponse has a property of List, so we'll check whether we've compared properties in propertiesInComparison. + else if (MatchProperty(sourceType, targetType, parentPropertyType, childPropertyType, propertiesInComparison, fromArePropertyTypesMatch: true)) + { + return true; + } + else if (!(parentPropertyType.IsGenericParameter && IsAssignable(parentPropertyType.BaseType!, childPropertyType))) + { + return false; + } + + return true; + } + + private static bool DoesPropertyReferenceItself(Type sourceType, MgmtObjectType targetType, Type parentPropertyType, CSharpType childPropertyType) + { + return parentPropertyType.Equals(sourceType) && + childPropertyType.Equals(targetType.Type) && + sourceType.GetCustomAttributes(false).Any(a => a.GetType().Name.Equals(ReferenceClassFinder.TypeReferenceTypeAttributeName)); + } + + /// + /// Tells if can be assigned to + /// by checking if there's an implicit type convertor in . + /// Todo: should we check childPropertyType as well since an implicit can be defined in either classes? + /// + /// The type to be assigned to. + /// The type to assign. + /// + private static bool IsAssignable(System.Type parentPropertyType, CSharpType childPropertyType) + { + if (parentPropertyType.Name == "ResourceIdentifier" && childPropertyType.IsFrameworkType && childPropertyType.FrameworkType == typeof(string)) + return true; + + return parentPropertyType.GetMethods().Where(m => m.Name == "op_Implicit" && + m.ReturnType == parentPropertyType && + m.GetParameters().First().ParameterType.FullName == $"{childPropertyType.Namespace}.{childPropertyType.Name}").Count() > 0; + } + + private static bool IsGuidAndStringType(System.Type parentPropertyType, CSharpType childPropertyType) + { + var isParentGuidType = parentPropertyType.GetTypeInfo() == typeof(Guid) || parentPropertyType.GetTypeInfo() == typeof(Guid?); + var isChildStringType = childPropertyType.IsFrameworkType && childPropertyType.FrameworkType == typeof(string); + + var isParentStringType = parentPropertyType.GetTypeInfo() == typeof(string); + var isChildGuidType = childPropertyType.IsFrameworkType && (childPropertyType.FrameworkType == typeof(Guid) || childPropertyType.FrameworkType == typeof(Guid?)); + + return (isParentGuidType && isChildStringType) || (isParentStringType && isChildGuidType); + } + + private static bool IsMatchingGenericType(Type sourceType, MgmtObjectType targetType, Type parentPropertyType, CSharpType childPropertyType, Dictionary? propertiesInComparison = null) + { + var parentGenericTypeDef = parentPropertyType.GetGenericTypeDefinition(); + if (parentGenericTypeDef == typeof(Nullable<>)) + { + if (!childPropertyType.IsNullable) + return false; + else + { + Type parentArgType = parentPropertyType.GetGenericArguments()[0]; + var isArgMatches = MatchProperty(sourceType, targetType, parentArgType, childPropertyType, propertiesInComparison); + return isArgMatches; + } + } + else if (!(childPropertyType.IsFrameworkType && childPropertyType.FrameworkType.IsGenericType && childPropertyType.FrameworkType.GetGenericTypeDefinition() == parentGenericTypeDef)) + { + return false; + } + for (int i = 0; i < parentPropertyType.GetGenericArguments().Length; i++) + { + Type parentArgType = parentPropertyType.GetGenericArguments()[i]; + CSharpType childArgType = childPropertyType.Arguments[i]; + var isArgMatches = MatchProperty(sourceType, targetType, parentArgType, childArgType, propertiesInComparison); + if (!isArgMatches) + return false; + } + return true; + } + + private static bool MatchProperty(Type sourceType, MgmtObjectType targetType, Type parentPropertyType, CSharpType childPropertyType, Dictionary? propertiesInComparison = null, bool fromArePropertyTypesMatch = false) + { + if (propertiesInComparison != null && propertiesInComparison.TryGetValue(parentPropertyType, out var val) && val.Equals(childPropertyType)) + return true; + + if (DoesPropertyReferenceItself(sourceType, targetType, parentPropertyType, childPropertyType)) + return true; + + var isArgMatches = false; + if (parentPropertyType.IsClass && !childPropertyType.IsFrameworkType && childPropertyType.Implementation as MgmtObjectType != null) + { + var mgmtObjectType = childPropertyType.Implementation as MgmtObjectType; + if (mgmtObjectType != null) + isArgMatches = IsEqual(parentPropertyType, mgmtObjectType, parentPropertyType.GetProperties().ToList(), mgmtObjectType.MyProperties.ToList(), new Dictionary { { parentPropertyType, childPropertyType } }); + } + else if (!childPropertyType.IsFrameworkType && childPropertyType.Implementation as EnumType != null) + { + var childEnumType = childPropertyType.Implementation as EnumType; + if (childEnumType != null) + isArgMatches = MatchEnum(parentPropertyType, childEnumType); + } + else if (!fromArePropertyTypesMatch) + { + isArgMatches = ArePropertyTypesMatch(sourceType, targetType, parentPropertyType, childPropertyType); + } + return isArgMatches; + } + + private static bool MatchEnum(Type parentPropertyType, EnumType childPropertyType) + { + var parentProperties = parentPropertyType.GetProperties().ToList(); + if (parentProperties.Count != childPropertyType.Values.Count) + { + // For ManagedServiceIdentityType, if the parent choice values is a superset of the child choice values, then we treat it as a match. + if (parentPropertyType != typeof(Azure.ResourceManager.Models.ManagedServiceIdentityType)) + return false; + else if (parentProperties.Count < childPropertyType.Values.Count) + return false; + } + Dictionary parentDict = parentProperties.ToDictionary(p => p.Name, p => p); + foreach (var enumValue in childPropertyType.Values) + { + if (!parentDict.TryGetValue(enumValue.Declaration.Name, out var parentProperty)) + return false; + if (parentProperty.Name != enumValue.Declaration.Name) + return false; + } + + return true; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ReferenceClassFinder.cs b/logger/autorest.csharp/mgmt/Decorator/ReferenceClassFinder.cs new file mode 100644 index 0000000..307bfbd --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ReferenceClassFinder.cs @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core.Expressions.DataFactory; +using Azure.ResourceManager; +using Azure.ResourceManager.Models; +using Operation = Azure.Operation; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + public class ReferenceClassFinder + { + internal const string InitializationCtorAttribute = "InitializationConstructor"; + internal const string SerializationCtorAttribute = "SerializationConstructor"; + internal const string ReferenceTypeAttribute = "ReferenceType"; + + internal const string InitializationCtorAttributeName = "InitializationConstructorAttribute"; + internal const string SerializationCtorAttributeName = "SerializationConstructorAttribute"; + internal const string ReferenceTypeAttributeName = "ReferenceTypeAttribute"; + + internal const string PropertyReferenceTypeAttribute = "PropertyReferenceType"; + internal const string PropertyReferenceTypeAttributeName = "PropertyReferenceTypeAttribute"; + + internal const string TypeReferenceTypeAttribute = "TypeReferenceType"; + internal const string TypeReferenceTypeAttributeName = "TypeReferenceTypeAttribute"; + + public record PropertyMetadata(string SerializedName, bool Required) + { + public PropertyMetadata(string serializedName) : this(serializedName, false) + { + } + } + + private static readonly Dictionary> _referenceTypesPropertyMetadata = new() + { + [typeof(ResourceData)] = new() + { + ["Id"] = new PropertyMetadata("id", true), + ["Name"] = new PropertyMetadata("name", true), + ["ResourceType"] = new PropertyMetadata("type", true), + ["SystemData"] = new PropertyMetadata("systemData", false), + }, + [typeof(TrackedResourceData)] = new() + { + ["Id"] = new PropertyMetadata("id", true), + ["Name"] = new PropertyMetadata("name", true), + ["ResourceType"] = new PropertyMetadata("type", true), + ["SystemData"] = new PropertyMetadata("systemData", false), + ["Location"] = new PropertyMetadata("location", true), + ["Tags"] = new PropertyMetadata("tags"), + }, + [typeof(ManagedServiceIdentity)] = new() + { + ["PrincipalId"] = new PropertyMetadata("principalId"), + ["TenantId"] = new PropertyMetadata("tenantId"), + ["ManagedServiceIdentityType"] = new PropertyMetadata("type", true), + ["UserAssignedIdentities"] = new PropertyMetadata("userAssignedIdentities"), + }, + [typeof(SystemData)] = new() + { + ["CreatedBy"] = new PropertyMetadata("createdBy"), + ["CreatedByType"] = new PropertyMetadata("createdByType"), + ["CreatedOn"] = new PropertyMetadata("createdAt"), + ["LastModifiedBy"] = new PropertyMetadata("lastModifiedBy"), + ["LastModifiedByType"] = new PropertyMetadata("lastModifiedByType"), + ["LastModifiedOn"] = new PropertyMetadata("lastModifiedAt") + }, + [typeof(ResponseError)] = new() + { + ["Code"] = new PropertyMetadata("code", true), + ["Message"] = new PropertyMetadata("message", true), + ["Target"] = new PropertyMetadata("target"), + ["Details"] = new PropertyMetadata("details") + }, + [typeof(DataFactoryLinkedServiceReference)] = new() + { + [nameof(DataFactoryLinkedServiceReference.ReferenceKind)] = new PropertyMetadata("type", true), + [nameof(DataFactoryLinkedServiceReference.ReferenceName)] = new PropertyMetadata("referenceName", true), + [nameof(DataFactoryLinkedServiceReference.Parameters)] = new PropertyMetadata("parameters") + } + }; + + public static bool TryGetPropertyMetadata(Type type, [MaybeNullWhen(false)] out Dictionary dict) + { + dict = null; + if (_referenceTypesPropertyMetadata.TryGetValue(type, out dict)) + return dict != null; + + if (TryConstructPropertyMetadata(type, out dict)) + { + _referenceTypesPropertyMetadata.Add(type, dict); + return true; + } + + return false; + } + + public static Dictionary GetPropertyMetadata(Type type) + { + if (_referenceTypesPropertyMetadata.TryGetValue(type, out var dict)) + return dict; + dict = ConstructPropertyMetadata(type); + _referenceTypesPropertyMetadata.Add(type, dict); + return dict; + } + + private static bool TryConstructPropertyMetadata(Type type, [MaybeNullWhen(false)] out Dictionary dict) + { + var publicCtor = type.GetConstructors().Where(c => c.IsPublic).OrderBy(c => c.GetParameters().Count()).FirstOrDefault(); + if (publicCtor == null && !type.IsAbstract) + { + dict = null; + return false; + } + dict = new Dictionary(); + var internalPropertiesToInclude = new List(); + PropertyMatchDetection.AddInternalIncludes(type, internalPropertiesToInclude); + foreach (var property in type.GetProperties().Where(p => p.DeclaringType == type).Concat(internalPropertiesToInclude)) + { + var metadata = new PropertyMetadata(property.Name.ToVariableName(), publicCtor != null && GetRequired(publicCtor, property)); + dict.Add(property.Name, metadata); + } + return true; + } + + private static Dictionary ConstructPropertyMetadata(Type type) + { + if (TryConstructPropertyMetadata(type, out var dict)) + return dict; + + throw new InvalidOperationException($"Property metadata information for type {type} cannot be constructed automatically because it does not have a public constructor"); + } + + private static bool GetRequired(ConstructorInfo publicCtor, PropertyInfo property) + => publicCtor.GetParameters().Any(param => param.Name?.Equals(property.Name, StringComparison.OrdinalIgnoreCase) == true && param.GetType() == property.GetType()); + + internal class Node + { + public Type Type { get; } + public List Children { get; } + + public Node(Type type) + { + Type = type; + Children = new List(); + } + } + + private static IReadOnlyList? _externalTypes; + /// + /// All external types, right now they are all defined in Azure.Core, Azure.Core.Expressions.DataFactory, and Azure.ResourceManager. + /// See: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/resourcemanager/Azure.ResourceManager/src + /// https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src + /// https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core.Expressions.DataFactory/src + /// + private static IReadOnlyList ExternalTypes => _externalTypes ??= GetExternalTypes(); + + private static IReadOnlyList? _referenceTypes; + internal static IReadOnlyList ReferenceTypes => _referenceTypes ??= GetOrderedList(GetReferenceClassCollectionInternal()); + + private static IReadOnlyList? _propertyReferenceTypes; + internal static IReadOnlyList PropertyReferenceTypes + => _propertyReferenceTypes ??= ExternalTypes.Where(t => IsPropertyReferenceType(t) && !IsObsolete(t)).ToArray(); + + private static IReadOnlyList? _typeReferenceTypes; + internal static IReadOnlyList TypeReferenceTypes + // we only include armcore types when we are not generating armCore + => _typeReferenceTypes ??= ExternalTypes.Where(IsTypeReferenceType).ToList(); + + private static IReadOnlyList GetExternalTypes() + { + List types = new List(); + var assembly = Assembly.GetAssembly(typeof(ArmClient)); + if (assembly != null) + types.AddRange(assembly.GetTypes()); + + assembly = Assembly.GetAssembly(typeof(Operation)); + if (assembly != null) + types.AddRange(assembly.GetTypes()); + + if (Configuration.UseCoreDataFactoryReplacements) + { + assembly = Assembly.GetAssembly(typeof(DataFactoryElement<>)); + if (assembly != null) + types.AddRange(assembly.GetTypes()); + } + + return types; + } + + private static IList GetReferenceClassCollectionInternal() + // For ReferenceType we always include armcore types because types in armcore we also need to replace + => ExternalTypes.Where(t => IsReferenceType(t) && !IsObsolete(t)).ToArray(); + + internal static bool HasAttribute(Type type, string attributeName) + => type.GetCustomAttributes(false).Where(a => a.GetType().Name == attributeName).Any(); + + private static bool IsReferenceType(Type type) => HasAttribute(type, ReferenceTypeAttributeName); + + private static bool IsPropertyReferenceType(Type type) => HasAttribute(type, PropertyReferenceTypeAttributeName); + + private static bool IsTypeReferenceType(Type type) => HasAttribute(type, TypeReferenceTypeAttributeName); + + private static bool IsObsolete(Type type) + => type.GetCustomAttributes(false).Where(a => a.GetType() == typeof(ObsoleteAttribute)).Any(); + + internal static List GetOrderedList(IList referenceTypes) + { + var rootNodes = GetRootNodes(referenceTypes); + rootNodes.Sort((a, b) => a.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length.CompareTo(b.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length) * -1); + var output = new List(); + foreach (var root in rootNodes) + { + var treeNodes = new List(); + Queue queue = new Queue(); + queue.Enqueue(root); + while (queue.Count != 0) + { + Node tempNode = queue.Dequeue(); + treeNodes.Add(tempNode.Type); + List tempChildren = tempNode.Children; + if (tempChildren != null) + { + int childNum = tempChildren.Count; + while (childNum > 0) + { + queue.Enqueue(tempChildren[childNum - 1]); + childNum--; + } + } + } + treeNodes.Reverse(); + output.AddRange(PromoteGenericType(treeNodes)); + } + return output; + } + + private static List PromoteGenericType(List output) + { + bool swapped = false; + for (int i = 0; i < output.Count; i++) + { + if (output[i].IsGenericType) + { + // since we need to ensure the base generic type is before + // any other inheritors we just need to search behind + for (int j = i - 1; j > -1; j--) + { + if (output[j].IsGenericType == false + && output[j].BaseType == output[i]) + { + + System.Type temp = output[j]; + output[j] = output[i]; + output[i] = temp; + swapped = true; + } + } + } + } + if (swapped) + return PromoteGenericType(output); + + return output; + } + + internal static List GetRootNodes(IList referenceClassCollection) + { + List rootNodes = new List(); + var added = new Dictionary(); + var rootHash = new Dictionary>(); + foreach (System.Type reference in referenceClassCollection) + { + if (!added.ContainsKey(reference)) + { + Node node = new Node(reference); + System.Type baseType = reference.BaseType ?? typeof(object); + if (baseType != typeof(object) && added.ContainsKey(baseType)) + { + added[baseType].Children.Add(node); + } + else + { + if (rootHash.ContainsKey(node.Type)) + { + foreach (var child in rootHash[node.Type]) + { + node.Children.Add(child); + rootNodes.Remove(child); + } + rootHash.Remove(baseType); + } + else + { + if (baseType != typeof(object)) + { + List? list; + if (!rootHash.TryGetValue(baseType, out list)) + { + list = new List(); + rootHash.Add(baseType, list); + } + list.Add(node); + } + } + rootNodes.Add(node); + } + added.Add(reference, node); + } + } + return rootNodes; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ReferenceTypePropertyChooser.cs b/logger/autorest.csharp/mgmt/Decorator/ReferenceTypePropertyChooser.cs new file mode 100644 index 0000000..6d9f329 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ReferenceTypePropertyChooser.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Models.Types; +using Azure.ResourceManager.Models; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class ReferenceTypePropertyChooser + { + internal const string OptionalPropertiesName = "OptionalProperties"; + + private static ConcurrentDictionary _valueCache = new ConcurrentDictionary(); + + private static readonly Type _locationType = typeof(Azure.Core.AzureLocation); + private static readonly Type _resourceIdentifierType = typeof(Azure.Core.ResourceIdentifier); + private static readonly Type _resourceTypeType = typeof(Azure.Core.ResourceType); + + public static ObjectTypeProperty? GetExactMatchForReferenceType(ObjectTypeProperty originalType, Type frameworkType) + { + return FindSimpleReplacements(originalType, frameworkType); + } + + public static bool TryGetCachedExactMatch(InputType schema, out CSharpType? result) + { + return _valueCache.TryGetValue(schema, out result); + } + + public static CSharpType? GetExactMatch(MgmtObjectType typeToReplace) + { + if (_valueCache.TryGetValue(typeToReplace.InputModel, out var result)) + return result; + + if (!typeToReplace.ShouldNotReplaceForProperty()) + { + foreach (Type replacementType in ReferenceClassFinder.PropertyReferenceTypes) + { + var typeToReplacePropertyNames = typeToReplace.MyProperties.Select(p => p.Declaration.Name).ToHashSet(); + var attributeObj = replacementType.GetCustomAttributes()?.Where(a => a.GetType().Name == ReferenceClassFinder.PropertyReferenceTypeAttributeName).FirstOrDefault(); + var optionalPropertiesForMatch = (attributeObj?.GetType().GetProperty(OptionalPropertiesName)?.GetValue(attributeObj) as string[] ?? Array.Empty()).ToHashSet(); + List replacementTypeProperties = replacementType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => !optionalPropertiesForMatch.Contains(p.Name) || typeToReplacePropertyNames.Contains(p.Name)).ToList(); + List typeToReplaceProperties = typeToReplace.MyProperties.ToList(); + + if (PropertyMatchDetection.IsEqual(replacementType, typeToReplace, replacementTypeProperties, typeToReplaceProperties, new Dictionary { { replacementType, typeToReplace.Type } })) + { + result = CSharpType.FromSystemType(MgmtContext.Context, replacementType); + _valueCache.TryAdd(typeToReplace.InputModel, result); + return result; + } + } + } + else + { + MgmtReport.Instance.TransformSection.AddTransformLog(new TransformItem(TransformTypeName.NoPropertyTypeReplacement, typeToReplace.Type.Name), typeToReplace.Type.Name, "NoReplaceForType:" + typeToReplace.Type.Name); + } + _valueCache.TryAdd(typeToReplace.InputModel, null); + return null; + } + + private static ObjectTypeProperty? FindSimpleReplacements(ObjectTypeProperty originalType, Type frameworkType) + { + //TODO for core generation this list is small enough we can simply define each of them here. + //eventually we might want to come up with a more robust way of doing this + + bool isString = frameworkType == typeof(string); + + if (originalType.Declaration.Name == "Location" && (isString || frameworkType.Name == _locationType.Name)) + return GetObjectTypeProperty(originalType, _locationType); + + if (originalType.Declaration.Name == "ResourceType" && (isString || frameworkType.Name == _resourceTypeType.Name)) + return GetObjectTypeProperty(originalType, _resourceTypeType); + + if (originalType.Declaration.Name == "Id" && (isString || frameworkType.Name == _resourceIdentifierType.Name)) + return GetObjectTypeProperty(originalType, _resourceIdentifierType); + + return null; + } + + public static ObjectTypeProperty GetObjectTypeProperty(ObjectTypeProperty originalType, CSharpType replacementCSharpType) + { + var extraDescription = IsReplacementTypeManagedServiceIdentity(replacementCSharpType) ? originalType.CreateExtraDescriptionWithManagedServiceIdentity() : string.Empty; + var originalDescription = originalType.Description; + var periodAndSpace = originalDescription.ToString().EndsWith(".") ? " " : ". "; + var description = string.IsNullOrEmpty(extraDescription) ? originalDescription : $"{originalDescription}{periodAndSpace}{extraDescription}"; + return new ObjectTypeProperty( + new MemberDeclarationOptions(originalType.Declaration.Accessibility, originalType.Declaration.Name, replacementCSharpType), + description, + originalType.IsReadOnly, + originalType.InputModelProperty + ); + } + + private static bool IsReplacementTypeManagedServiceIdentity(CSharpType replacementCSharpType) + { + return !replacementCSharpType.IsFrameworkType && replacementCSharpType.Implementation is SystemObjectType systemObjectType && systemObjectType.SystemType == typeof(ManagedServiceIdentity); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/RequestPathExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/RequestPathExtensions.cs new file mode 100644 index 0000000..d6414d9 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/RequestPathExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using AutoRest.CSharp.Mgmt.Models; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class RequestPathExtensions + { + private static bool TryMinus(RequestPath requestPath, RequestPath other, [MaybeNullWhen(false)] out string diff) + { + diff = null; + if (requestPath == other) + { + diff = RequestPath.Tenant; + return true; + } + + if (requestPath.IsAncestorOf(other)) + { + diff = $"-{requestPath.TrimAncestorFrom(other)}"; + return true; + } + + if (other.IsAncestorOf(requestPath)) + { + diff = other.TrimAncestorFrom(requestPath); + return true; + } + + return false; + } + + public static string Minus(this RequestPath requestPath, RequestPath other) + { + if (TryMinus(requestPath, other, out var diff)) + return diff; + + // if they do not have parent relationship, this could be because of the different scopes + // therefore we trim the scope out of them and then minus + var requestTrimmed = requestPath.TrimScope(); + var otherTrimmed = other.TrimScope(); + + return TryMinus(requestTrimmed, otherTrimmed, out diff) ? diff : requestTrimmed; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ResourceDetection.cs b/logger/autorest.csharp/mgmt/Decorator/ResourceDetection.cs new file mode 100644 index 0000000..883c197 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ResourceDetection.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Utilities; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class ResourceDetection + { + private const string ProvidersSegment = "/providers/"; + private static ConcurrentDictionary _resourceDataSchemaCache = new ConcurrentDictionary(); + + public static bool IsResource(this OperationSet set, InputNamespace? inputNamespace = null) + { + return set.TryGetResourceDataSchema(out _, out _, inputNamespace); + } + + private static InputModelType? FindObjectSchemaWithName(string name, InputNamespace? inputNamespace = null) + => inputNamespace?.Models.OfType().FirstOrDefault(inputModel => inputModel.GetOriginalName() == name); + + public static bool TryGetResourceDataSchema(this OperationSet set, [MaybeNullWhen(false)] out string resourceSchemaName, out InputModelType? inputModel, InputNamespace? inputNamespace) + { + resourceSchemaName = null; + inputModel = null; + + // get the result from cache + if (_resourceDataSchemaCache.TryGetValue(set.RequestPath, out var resourceSchemaTuple)) + { + resourceSchemaName = resourceSchemaTuple?.Name; + inputModel = resourceSchemaTuple?.InputModel; + return resourceSchemaTuple != null; + } + + // try to get result from configuration + if (Configuration.MgmtConfiguration.RequestPathToResourceData.TryGetValue(set.RequestPath, out resourceSchemaName)) + { + // find a schema with this name + inputModel = FindObjectSchemaWithName(resourceSchemaName, inputNamespace); + if (inputModel == null) + { + throw new ErrorHelpers.ErrorException($"cannot find an object schema with name {resourceSchemaName} in the request-path-to-resource-data configuration"); + } + _resourceDataSchemaCache.TryAdd(set.RequestPath, (resourceSchemaName, inputModel)); + return true; + } + + // try to find it in the partial resource list + if (Configuration.MgmtConfiguration.PartialResources.TryGetValue(set.RequestPath, out resourceSchemaName)) + { + inputModel = null; + return true; + } + + // try to get another configuration to see if this is marked as not a resource + if (Configuration.MgmtConfiguration.RequestPathIsNonResource.Contains(set.RequestPath)) + { + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.RequestPathIsNonResource, set.RequestPath), set.RequestPath, "Path marked as non-resource: " + set.RequestPath); + _resourceDataSchemaCache.TryAdd(set.RequestPath, null); + return false; + } + + // Check if the request path has even number of segments after the providers segment + if (!CheckEvenSegments(set.RequestPath)) + return false; + + // before we are finding any operations, we need to ensure this operation set has a GET request. + if (set.FindOperation(RequestMethod.Get) is null) + return false; + + // try put operation to get the resource name + if (set.TryOperationWithMethod(RequestMethod.Put, out inputModel)) + { + resourceSchemaName = inputModel.Name; + _resourceDataSchemaCache.TryAdd(set.RequestPath, (resourceSchemaName, inputModel)); + return true; + } + + // try get operation to get the resource name + if (set.TryOperationWithMethod(RequestMethod.Get, out inputModel)) + { + resourceSchemaName = inputModel.Name; + _resourceDataSchemaCache.TryAdd(set.RequestPath, (resourceSchemaName, inputModel)); + return true; + } + + // We tried everything, this is not a resource + _resourceDataSchemaCache.TryAdd(set.RequestPath, null); + return false; + } + + private static bool CheckEvenSegments(string requestPath) + { + var index = requestPath.LastIndexOf(ProvidersSegment); + // this request path does not have providers segment - it can be a "ById" request, skip to next criteria + if (index < 0) + return true; + // get whatever following the providers + var following = requestPath.Substring(index); + var segments = following.Split("/", StringSplitOptions.RemoveEmptyEntries); + return segments.Length % 2 == 0; + } + + private static bool TryOperationWithMethod(this OperationSet set, RequestMethod method, [MaybeNullWhen(false)] out InputModelType inputModel) + { + inputModel = null; + + var operation = set.FindOperation(method); + if (operation is null) + return false; + // find the response with code 200 + var response = operation.GetServiceResponse(); + if (response is null) + return false; + // find the response schema + var responseType = response.BodyType?.GetImplementType() as InputModelType; + if (responseType is null) + return false; + + // we need to verify this schema has ID, type and name so that this is a resource model + if (!responseType.IsResourceModel()) + return false; + + inputModel = responseType; + return true; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ResourceTypeBuilder.cs b/logger/autorest.csharp/mgmt/Decorator/ResourceTypeBuilder.cs new file mode 100644 index 0000000..8ec807e --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ResourceTypeBuilder.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Models; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class ResourceTypeBuilder + { + private static ConcurrentDictionary _requestPathToResourceTypeCache = new ConcurrentDictionary(); + + static ResourceTypeBuilder() + { + _requestPathToResourceTypeCache.TryAdd(RequestPath.Subscription, ResourceTypeSegment.Subscription); + _requestPathToResourceTypeCache.TryAdd(RequestPath.ResourceGroup, ResourceTypeSegment.ResourceGroup); + _requestPathToResourceTypeCache.TryAdd(RequestPath.Tenant, ResourceTypeSegment.Tenant); + _requestPathToResourceTypeCache.TryAdd(RequestPath.ManagementGroup, ResourceTypeSegment.ManagementGroup); + } + + public static ResourceTypeSegment GetResourceType(this RequestPath requestPath) + { + if (_requestPathToResourceTypeCache.TryGetValue(requestPath, out var resourceType)) + return resourceType; + + resourceType = CalculateResourceType(requestPath); + _requestPathToResourceTypeCache.TryAdd(requestPath, resourceType); + return resourceType; + } + + private static ResourceTypeSegment CalculateResourceType(RequestPath requestPath) + { + if (Configuration.MgmtConfiguration.RequestPathToResourceType.TryGetValue(requestPath.SerializedPath, out var resourceType)) + return new ResourceTypeSegment(resourceType); + + // we cannot directly return the new ResourceType here, the requestPath here can be a parameterized scope, which does not have a resource type + // even if we have the configuration to assign explicit types to a parameterized scope, we do not have enough information to get which request path the current scope variable belongs + // therefore we can only return a place holder here to let the caller decide the actual resource type + if (requestPath.IsParameterizedScope()) + return ResourceTypeSegment.Scope; + return ResourceTypeSegment.ParseRequestPath(requestPath); + } + + public static ICollection? GetScopeTypeStrings(IEnumerable? scopeTypes) + { + if (scopeTypes == null || !scopeTypes.Any() || scopeTypes.Contains(ResourceTypeSegment.Any)) + return null; + + return scopeTypes.Select(type => (FormattableString)$"{type}").ToArray(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/SchemaExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/SchemaExtensions.cs new file mode 100644 index 0000000..bf3452a --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/SchemaExtensions.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Mgmt.Decorator; + +internal static class SchemaExtensions +{ + /// + /// Union all the properties on myself and all the properties from my parents + /// + /// + /// + internal static IEnumerable GetAllProperties(this InputModelType inputModel) + { + return inputModel.GetAllBaseModels().SelectMany(parentInputModelType => parentInputModelType.Properties).Concat(inputModel.Properties); + } + + /// + /// Union all the properties on myself and all the properties from my parents + /// + /// + /// + internal static IEnumerable GetAllProperties(this ObjectSchema schema) + { + return schema.Parents!.All.OfType().SelectMany(parentSchema => parentSchema.Properties).Concat(schema.Properties); + } + + private static bool IsTagsProperty(InputModelProperty property) + => property.CSharpName().Equals("Tags") + && property.Type.GetImplementType() is InputDictionaryType dictType + && dictType.ValueType.GetImplementType() is InputPrimitiveType inputPrimitive + && inputPrimitive.Kind == InputPrimitiveTypeKind.String; + + public static bool HasTags(this InputType schema) + { + if (schema.GetImplementType() is not InputModelType inputModel) + { + return false; + } + + var allProperties = inputModel.GetAllProperties(); + + return allProperties.Any(property => IsTagsProperty(property) && !property.IsReadOnly); + } + + public static bool IsResourceModel(this InputModelType inputModelType) + { + var allProperties = inputModelType.GetAllProperties(); + bool idPropertyFound = false; + bool typePropertyFound = !Configuration.MgmtConfiguration.DoesResourceModelRequireType; + bool namePropertyFound = !Configuration.MgmtConfiguration.DoesResourceModelRequireName; + + foreach (var property in allProperties) + { + // check if this property is flattened from lower level, we should only consider first level properties in this model + // therefore if flattenedNames is not empty, this property is flattened, we skip this property + if (property.FlattenedNames is not null && property.FlattenedNames.Any()) + continue; + + switch (property.SerializedName) + { + case "id": + if (property.Type.GetImplementType() is InputPrimitiveType { Kind: InputPrimitiveTypeKind.String } inputPrimitiveType) + idPropertyFound = true; + continue; + case "type": + if (property.Type.GetImplementType() is InputPrimitiveType { Kind: InputPrimitiveTypeKind.String } inputPrimitive) + typePropertyFound = true; + continue; + case "name": + if (property.Type.GetImplementType() is InputPrimitiveType { Kind: InputPrimitiveTypeKind.String } primitive) + namePropertyFound = true; + continue; + } + } + + return idPropertyFound && typePropertyFound && namePropertyFound; + } + + // TODO: we may reuse the IsResourceModel instead of creating this method, but the result for flattened properties is different as although models with matched flattened properties are not treated as Resource but they still inherit from ResourceData. We should probably consider to align the behavior before we can refactor the methods. + internal static bool IsResourceData(this InputModelType objSchema) + { + return objSchema.ContainsStringProperty("id") && objSchema.ContainsStringProperty("name") && objSchema.ContainsStringProperty("type"); + } + + private static bool ContainsStringProperty(this InputModelType inputModelType, string propertyName) + { + return inputModelType.GetAllProperties().Any(p => p.SerializedName.Equals(propertyName, StringComparison.Ordinal) && p.Type is InputPrimitiveType { Kind: InputPrimitiveTypeKind.String }); + } + + // TODO: we may reuse the IsResourceModel instead of creating this method, but the result for flattened properties is different as although models with matched flattened properties are not treated as Resource but they still inherit from ResourceData. We should probably consider to align the behavior before we can refactor the methods. + internal static bool IsResourceData(this ObjectSchema objSchema) + { + return objSchema.ContainsStringProperty("id") && objSchema.ContainsStringProperty("name") && objSchema.ContainsStringProperty("type"); + } + + private static bool ContainsStringProperty(this ObjectSchema objSchema, string propertyName) + { + return objSchema.GetAllProperties().Any(p => p.SerializedName.Equals(propertyName, StringComparison.Ordinal) && p.Schema.Type == AllSchemaTypes.String); + } + + internal static string GetOriginalName(this InputType inputType) => inputType.GetImplementType().SpecName ?? inputType.GetImplementType().Name; + + internal static string GetOriginalName(this Schema schema) => schema.Language.Default.SerializedName ?? schema.Language.Default.Name; + + internal static string GetOriginalName(this RequestParameter parameter) => parameter.Language.Default.SerializedName ?? parameter.Language.Default.Name; + + internal static string GetFullSerializedName(this Schema schema) => schema.GetOriginalName(); + + internal static string GetFullSerializedName(this InputType inputType) => inputType.GetOriginalName(); + + internal static string GetFullSerializedName(this InputType inputType, InputEnumTypeValue choice) + { + return inputType switch + { + InputEnumType c => c.GetFullSerializedName(choice), + InputNullableType { Type: InputType i } => i.GetFullSerializedName(choice), + _ => throw new InvalidOperationException($"Given input type is not InputEnumType: {inputType.Name}") + }; + } + + internal static string GetFullSerializedName(this Schema schema, ChoiceValue choice) + { + return schema switch + { + ChoiceSchema c => c.GetFullSerializedName(choice), + SealedChoiceSchema sc => sc.GetFullSerializedName(choice), + _ => throw new InvalidOperationException("Given schema is not ChoiceSchema or SealedChoiceSchema: " + schema.Name) + }; + } + + internal static string GetFullSerializedName(this InputEnumType inputEnum, InputEnumTypeValue choice) + { + if (!inputEnum.Values.Contains(choice)) + throw new InvalidOperationException($"enum value {choice.Value} doesn't belong to enum {inputEnum.Name}"); + return $"{inputEnum.Name}.{choice.Value}"; + } + + internal static string GetFullSerializedName(this ChoiceSchema schema, ChoiceValue choice) + { + if (!schema.Choices.Contains(choice)) + throw new InvalidOperationException($"choice value {choice.Value} doesn't belong to choice {schema.Name}"); + return $"{schema.GetFullSerializedName()}.{choice.Value}"; + } + + internal static string GetFullSerializedName(this SealedChoiceSchema schema, ChoiceValue choice) + { + if (!schema.Choices.Contains(choice)) + throw new InvalidOperationException($"choice value {choice.Value} doesn't belong to SealedChoice {schema.Name}"); + return $"{schema.GetFullSerializedName()}.{choice.Value}"; + } + + internal static string GetFullSerializedName(this ObjectSchema schema, Property property) + { + if (!schema.Properties.Contains(property)) + throw new InvalidOperationException($"property {property.SerializedName} doesn't belong to object {schema.Name}"); + string propertySerializedName; + if (property.FlattenedNames.Count == 0) + propertySerializedName = $"{property.SerializedName}"; + else + propertySerializedName = string.Join(".", property.FlattenedNames); + return $"{schema.GetFullSerializedName()}.{propertySerializedName}"; + } + + internal static string GetFullSerializedName(this InputModelType inputModel, InputModelProperty property) + { + if (!inputModel.Properties.Contains(property)) + throw new InvalidOperationException($"property {property.SerializedName} doesn't belong to object {inputModel.Name}"); + string propertySerializedName; + if (property.FlattenedNames is null) + propertySerializedName = $"{property.SerializedName}"; + else + propertySerializedName = string.Join(".", property.FlattenedNames); + return $"{inputModel.GetFullSerializedName()}.{propertySerializedName}"; + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/ScopeDetection.cs b/logger/autorest.csharp/mgmt/Decorator/ScopeDetection.cs new file mode 100644 index 0000000..41dc142 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/ScopeDetection.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Concurrent; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Models; + +namespace AutoRest.CSharp.Mgmt.Decorator; + +internal static class ScopeDetection +{ + public const string Subscriptions = "subscriptions"; + public const string ResourceGroups = "resourceGroups"; + public const string Tenant = "tenant"; + public const string ManagementGroups = "managementGroups"; + public const string Any = "*"; + + private static ConcurrentDictionary _scopePathCache = new ConcurrentDictionary(); + private static ConcurrentDictionary _scopeTypesCache = new ConcurrentDictionary(); + + public static RequestPath GetScopePath(this RequestPath requestPath) + { + if (_scopePathCache.TryGetValue(requestPath, out var result)) + return result; + + result = CalculateScopePath(requestPath); + _scopePathCache.TryAdd(requestPath, result); + return result; + } + + /// + /// Returns true if this request path is a parameterized scope, like the "/{scope}" in "/{scope}/providers/M.C/virtualMachines/{vmName}" + /// Also returns true when this scope is explicitly set as a parameterized scope in the configuration + /// + /// + /// + public static bool IsParameterizedScope(this RequestPath scopePath) + { + // if this path could be found inside the configuration, we just return true for that. + if (Configuration.MgmtConfiguration.ParameterizedScopes.Contains(scopePath)) + return true; + + // if the path is not in the configuration, we go through the default logic to check if it is parameterized scope + return IsRawParameterizedScope(scopePath); + } + + public static bool IsRawParameterizedScope(this RequestPath scopePath) + { + // if a request is an implicit scope, it must only have one segment + if (scopePath.Count != 1) + return false; + // now the path only has one segment + var first = scopePath.First(); + // then we need to ensure the corresponding parameter enables `x-ms-skip-url-encoding` + if (first.IsConstant) + return false; // actually this cannot happen + // now the first segment is a reference + // we ensure this parameter enables x-ms-skip-url-encoding, aka Escape is false + return first.SkipUrlEncoding; + } + + private static RequestPath CalculateScopePath(RequestPath requestPath) + { + var indexOfProvider = requestPath.IndexOfLastProviders; + // if there is no providers segment, myself should be a scope request path. Just return myself + if (indexOfProvider >= 0) + { + if (indexOfProvider == 0 && requestPath.SerializedPath.StartsWith(RequestPath.ManagementGroupScopePrefix, StringComparison.InvariantCultureIgnoreCase)) + return RequestPath.ManagementGroup; + return RequestPath.FromSegments(requestPath.Take(indexOfProvider)); + } + if (requestPath.SerializedPath.StartsWith(RequestPath.ResourceGroupScopePrefix, StringComparison.InvariantCultureIgnoreCase)) + return RequestPath.ResourceGroup; + if (requestPath.SerializedPath.StartsWith(RequestPath.SubscriptionScopePrefix, StringComparison.InvariantCultureIgnoreCase)) + return RequestPath.Subscription; + if (requestPath.SerializedPath.Equals(RequestPath.TenantScopePrefix)) + return RequestPath.Tenant; + return requestPath; + } + + public static ResourceTypeSegment[]? GetParameterizedScopeResourceTypes(this RequestPath requestPath) + { + if (_scopeTypesCache.TryGetValue(requestPath, out var result)) + return result; + + result = requestPath.CalculateScopeResourceTypes(); + _scopeTypesCache.TryAdd(requestPath, result); + return result; + } + + private static ResourceTypeSegment[]? CalculateScopeResourceTypes(this RequestPath requestPath) + { + var scope = requestPath.GetScopePath(); + if (!scope.IsParameterizedScope()) + return null; + if (Configuration.MgmtConfiguration.RequestPathToScopeResourceTypes.TryGetValue(requestPath, out var resourceTypes)) + return resourceTypes.Select(v => BuildResourceType(v)).ToArray(); + + if (Configuration.MgmtConfiguration.ParameterizedScopes.Contains(scope)) + { + // if this configuration has this scope configured + // here we use this static method instead of scope.GetResourceType() to skip another check of IsParameterizedScope + var resourceType = ResourceTypeSegment.ParseRequestPath(scope); + return new[] { resourceType }; + } + + // otherwise we just assume this is scope and this scope could be anything + return new[] { ResourceTypeSegment.Subscription, ResourceTypeSegment.ResourceGroup, ResourceTypeSegment.ManagementGroup, ResourceTypeSegment.Tenant, ResourceTypeSegment.Any }; + } + + private static ResourceTypeSegment BuildResourceType(string resourceType) => resourceType switch + { + Subscriptions => ResourceTypeSegment.Subscription, + ResourceGroups => ResourceTypeSegment.ResourceGroup, + ManagementGroups => ResourceTypeSegment.ManagementGroup, + Tenant => ResourceTypeSegment.Tenant, + Any => ResourceTypeSegment.Any, + _ => new ResourceTypeSegment(resourceType) + }; +} diff --git a/logger/autorest.csharp/mgmt/Decorator/SingletonDetection.cs b/logger/autorest.csharp/mgmt/Decorator/SingletonDetection.cs new file mode 100644 index 0000000..2d4e12a --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/SingletonDetection.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Mgmt.Decorator; + +internal static class SingletonDetection +{ + private static string[] SingletonKeywords = { "default", "latest", "current" }; + + private static ConcurrentDictionary _singletonResourceCache = new ConcurrentDictionary(); + + public static bool IsSingletonResource(this OperationSet operationSet) + { + return operationSet.TryGetSingletonResourceSuffix(out _); + } + + public static bool TryGetSingletonResourceSuffix(this OperationSet operationSet, [MaybeNullWhen(false)] out SingletonResourceSuffix suffix) + { + suffix = null; + if (_singletonResourceCache.TryGetValue(operationSet, out suffix)) + return suffix != null; + + bool result = IsSingleton(operationSet, out var singletonIdSuffix); + suffix = ParseSingletonIdSuffix(operationSet, singletonIdSuffix); + _singletonResourceCache.TryAdd(operationSet, suffix); + return result; + } + + private static SingletonResourceSuffix? ParseSingletonIdSuffix(OperationSet operationSet, string? singletonIdSuffix) + { + if (singletonIdSuffix == null) + return null; + + var segments = singletonIdSuffix.Split('/', System.StringSplitOptions.RemoveEmptyEntries); + + // check if even segments + if (segments.Length % 2 != 0) + { + throw new ErrorHelpers.ErrorException($"the singleton suffix set for operation set {operationSet.RequestPath} must have even segments, but got {singletonIdSuffix}"); + } + + return SingletonResourceSuffix.Parse(segments); + } + + private static bool IsSingleton(OperationSet operationSet, [MaybeNullWhen(false)] out string singletonIdSuffix) + { + // we should first check the configuration for the singleton settings + if (Configuration.MgmtConfiguration.RequestPathToSingletonResource.TryGetValue(operationSet.RequestPath, out singletonIdSuffix)) + { + // ensure the singletonIdSuffix does not have a slash at the beginning + singletonIdSuffix = singletonIdSuffix.TrimStart('/'); + return true; + } + + // we cannot find the corresponding request path in the configuration, trying to deduce from the path + // return false if this is not a resource + if (!operationSet.IsResource()) + return false; + // get the request path + var currentRequestPath = operationSet.GetRequestPath(); + // if we are a singleton resource, + // we need to find the suffix which should be the difference between our path and our parent resource + var parentRequestPath = currentRequestPath.ParentRequestPath(); + var diff = parentRequestPath.TrimAncestorFrom(currentRequestPath); + // if not all of the segment in difference are constant, we cannot be a singleton resource + if (!diff.Any() || !diff.All(s => s.IsConstant)) + return false; + // see if the configuration says that we need to honor the dictionary for singletons + if (!Configuration.MgmtConfiguration.DoesSingletonRequiresKeyword) + { + singletonIdSuffix = string.Join('/', diff.Select(s => s.ConstantValue)); + return true; + } + // now we can ensure the last segment of the path is a constant + var lastSegment = currentRequestPath.Last(); + if (lastSegment.Constant.Type.Equals(typeof(string)) && SingletonKeywords.Any(w => lastSegment.ConstantValue == w)) + { + singletonIdSuffix = string.Join('/', diff.Select(s => s.ConstantValue)); + return true; + } + + return false; + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/StringExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/StringExtensions.cs new file mode 100644 index 0000000..7c61905 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/StringExtensions.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class StringExtensions + { + private const string ResourceSuffix = "Resource"; + private static HashSet _vowels = new HashSet(new char[] { 'a', 'e', 'i', 'o', 'u' }); + + // words in propery which we do not want to split + private static string[][] PropertyGroupWords = new string[][] + { + new string[] { "Extended", "Location" }, + new string[] { "Resource", "Type" }, + new string[] { "Virtual", "Machine" }, + }; + private static WordTrieNode? PropertyGroupWordsRootTrieNode; + + /// + /// This function changes a resource name to its plural form. If it has the same plural and singular form, it will add "All" prefix before the resource name. + /// + /// + /// + public static string ResourceNameToPlural(this string resourceName) + { + var pluralResourceName = resourceName.LastWordToPlural(false); + return pluralResourceName != resourceName ? + pluralResourceName : + $"All{resourceName}"; + } + + /// + /// Add `Resource` suffix to a resource name if that resource doesn't end with `Resource`. + /// + /// + /// + public static string AddResourceSuffixToResourceName(this string resourceName) + { + return resourceName.EndsWith(ResourceSuffix) ? resourceName : resourceName + ResourceSuffix; + } + + public static bool StartsWithVowel(this string resourceName) + { + return !string.IsNullOrEmpty(resourceName) && _vowels.Contains(char.ToLower(resourceName[0])); + } + + public static IEnumerable SplitByCamelCaseAndGroup(this string camelCase) + { + var words = camelCase.SplitByCamelCase().ToList(); + var i = 0; + while (i < words.Count) + { + if (TryToFindGroup(words.TakeLast(words.Count - i), out var groupWords)) + { + var newWord = string.Join("", groupWords); + words[i] = newWord; + words.RemoveRange(i + 1, groupWords.Count() - 1); + } + i++; + } + return words; + } + + private static bool TryToFindGroup(IEnumerable words, out IEnumerable groupWords) + { + if (PropertyGroupWordsRootTrieNode == null) + { + PropertyGroupWordsRootTrieNode = new WordTrieNode(); + foreach (var propertyGroupWords in PropertyGroupWords) + { + PropertyGroupWordsRootTrieNode.Save(propertyGroupWords); + } + } + groupWords = PropertyGroupWordsRootTrieNode.GetLongestPrefixes(words); + return (groupWords.Count() > 0); + } + + private class WordTrieNode + { + public Dictionary children = new Dictionary(); + + public void Save(IEnumerable words) + { + if (words.Count() == 0) + { + return; + } + + var firstWord = words.First(); + if (!children.TryGetValue(firstWord, out var child)) + { + child = new WordTrieNode(); + children[firstWord] = child; + } + child.Save(words.TakeLast(words.Count() - 1).ToList()); + } + + public IEnumerable GetLongestPrefixes(IEnumerable words) + { + var prefixes = new List(); + + if (words.Count() == 0) + { + return prefixes; + } + + var firstWord = words.First(); + if (children.TryGetValue(firstWord, out var child)) + { + prefixes.Add(firstWord); + prefixes.AddRange(child.GetLongestPrefixes(words.TakeLast(words.Count() - 1))); + } + return prefixes; + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/CodeModelValidator.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/CodeModelValidator.cs new file mode 100644 index 0000000..329898c --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/CodeModelValidator.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class CodeModelValidator + { + public static void Validate(CodeModel codeModel) + { + VerifyApiVersions(codeModel); + } + + private static void VerifyApiVersions(CodeModel codeModel) + { + foreach (var operationGroup in codeModel.OperationGroups) + { + VerifyApiVersionsWithinOperationGroup(operationGroup); + } + } + + // Operations within an operation group should use the same API version. + // TODO: this might be able to be removed after https://github.com/Azure/autorest.csharp/issues/1917 is resolved. + private static void VerifyApiVersionsWithinOperationGroup(OperationGroup operationGroup) + { + var apiVersionValues = operationGroup.Operations + .SelectMany(op => op.Parameters.Where(p => p.Origin == "modelerfour:synthesized/api-version").Select(p => ((ConstantSchema)p.Schema).Value.Value)) + .ToHashSet(); + if (apiVersionValues.Count > 1) + { + throw new InvalidOperationException($"Multiple api-version values found in the operation group: {operationGroup.Key}. Please rename the operation group for some operations so that all operations in one operation group share the same API version."); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/CommonSingleWordModels.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/CommonSingleWordModels.cs new file mode 100644 index 0000000..0dd4549 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/CommonSingleWordModels.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Report; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class CommonSingleWordModels + { + private static readonly HashSet _schemasToChange = new HashSet() + { + "Sku", + "SkuName", + "SkuTier", + "SkuFamily", + "SkuInformation", + "Plan", + "Usage", + "Resource", + "Kind", + // Private endpoint definitions which are defined in swagger common-types/privatelinks.json and are used by RPs + "PrivateEndpointConnection", + "PrivateLinkResource", + "PrivateLinkServiceConnectionState", + "PrivateEndpointServiceConnectionStatus", + "PrivateEndpointConnectionProvisioningState", + // not defined in common-types, but common in various RP + "PrivateLinkResourceProperties", + "PrivateLinkServiceConnectionStateProperty", + // internal, but could be public in the future, also make the names more consistent + "PrivateEndpointConnectionListResult", + "PrivateLinkResourceListResult" + }; + + public static void Update(CodeModel codeModel) + { + foreach (var schemaName in Configuration.MgmtConfiguration.PrependRPPrefix) + { + _schemasToChange.Add(schemaName); + } + foreach (var schema in codeModel.AllSchemas) + { + string serializedName = schema.Language.Default.SerializedName ?? schema.Language.Default.Name; + if (_schemasToChange.Contains(serializedName)) + { + string oriName = schema.Language.Default.Name; + string prefix = Configuration.Namespace.Equals(typeof(ArmClient).Namespace) ? "Arm" : MgmtContext.RPName; + string suffix = serializedName.Equals("Resource") ? "Data" : string.Empty; + schema.Language.Default.SerializedName ??= schema.Language.Default.Name; + schema.Language.Default.Name = prefix + serializedName + suffix; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.PrependRpPrefix, serializedName), schema.GetFullSerializedName(), "ApplyPrependRpPrefix", oriName, schema.Language.Default.Name); + } + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/DuplicateSchemaResolver.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/DuplicateSchemaResolver.cs new file mode 100644 index 0000000..731053c --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/DuplicateSchemaResolver.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + /// + /// Since we changed quite a few names of the schemas inside the code model after the modeler parses the model, + /// it is very possible that in the schemas, there are schemas with the same name. + /// These scheams with the same name must be resolved, otherwise we will get ArgumentException when adding schemas to the `LookupDictionary`. + /// + internal static class DuplicateSchemaResolver + { + public static void ResolveDuplicates(CodeModel codeModel) + { + // step 1: categorize the schema by their names + var schemaNameDict = new Dictionary>(); + foreach (var schema in codeModel.AllSchemas) + { + schemaNameDict.AddInList(schema.Name, schema); + } + + // step 2: collapse the schemas with the same name + foreach (var schemas in schemaNameDict.Values.Where(l => l.Count > 1)) + { + CollapseMultipleSchemas(schemas, codeModel); + } + } + + private static void CollapseMultipleSchemas(HashSet schemas, CodeModel codeModel) + { + // for simplicity, if the list has any ObjectSchema, we just throw exception. We should never use this to combine schemas - maybe we could add the support in the future? If needed. + if (schemas.Any(schema => schema is not ChoiceSchema && schema is not SealedChoiceSchema)) + throw new InvalidOperationException($"There are duplicated schemas and there is at least one schema `{schemas.First().Name}` which is not a ChoiceSchema or SealedChoiceSchema, this is not allowed (for now)"); + // now all things in the list should be Choices or SealedChoices + var collapsedSchema = CollapseChoices(schemas); + // now we need to update everything which is referencing the schemas in the list to the new schema + ReplaceSchemas(schemas, collapsedSchema, codeModel); + } + + private static void ReplaceSchemas(HashSet schemas, Schema replaceSchema, CodeModel codeModel) + { + // remove the things that should be replaced by the replaceSchmea + foreach (var schema in schemas) + { + if (schema == replaceSchema) + continue; + switch (schema) + { + case ChoiceSchema choiceSchema: + codeModel.Schemas.Choices.Remove(choiceSchema); + break; + case SealedChoiceSchema sealedChoiceSchema: + codeModel.Schemas.SealedChoices.Remove(sealedChoiceSchema); + break; + default: + throw new InvalidOperationException("This will never happen"); + } + } + // we have to iterate everything on the code model + foreach (var schema in codeModel.AllSchemas) + { + // only change things in ObjectSchema because we only change ChoiceSchema and SealedChoiceSchema + // they could only appear as properties of ObjectSchemas + if (schema is ObjectSchema objectSchema) + { + foreach (var property in objectSchema.Properties) + { + if (schemas.Contains(property.Schema)) + property.Schema = replaceSchema; + } + } + } + + // we also have to iterate all operations + foreach (var operationGroup in codeModel.OperationGroups) + { + foreach (var operation in operationGroup.Operations) + { + foreach (var operationResponse in operation.Responses) + { + ReplaceResponseSchema(schemas, operationResponse as SchemaResponse, replaceSchema); + } + + foreach (var operationResponse in operation.Exceptions) + { + ReplaceResponseSchema(schemas, operationResponse as SchemaResponse, replaceSchema); + } + + foreach (var parameter in operation.Parameters) + { + ReplaceRequestParamSchema(schemas, parameter, replaceSchema); + } + + foreach (var request in operation.Requests) + { + foreach (var parameter in request.Parameters) + { + ReplaceRequestParamSchema(schemas, parameter, replaceSchema); + } + } + } + } + } + + private static void ReplaceResponseSchema(HashSet schemas, SchemaResponse? response, Schema replaceSchema) + { + if (response == null || response.Schema == null) + return; + if (response.Schema is ChoiceSchema || response.Schema is SealedChoiceSchema) + { + if (schemas.Contains(response.Schema)) + response.Schema = replaceSchema; + } + } + + private static void ReplaceRequestParamSchema(HashSet schemas, RequestParameter parameter, Schema replaceSchema) + { + if (parameter.Schema is ChoiceSchema || parameter.Schema is SealedChoiceSchema) + if (schemas.Contains(parameter.Schema)) + parameter.Schema = replaceSchema; + } + + private static Schema CollapseChoices(IEnumerable schemas) + { + var choiceValuesList = schemas.Select(schema => schema switch + { + ChoiceSchema choiceSchema => choiceSchema.Choices.Select(v => v.Value).OrderBy(v => v), + SealedChoiceSchema sealedChoiceSchema => sealedChoiceSchema.Choices.Select(v => v.Value).OrderBy(v => v), + _ => throw new InvalidOperationException("This will never happen"), + }); + // determine if the choices in this list are the same + var deduplicated = choiceValuesList.ToHashSet(new CollectionComparer()); + if (deduplicated.Count == 1) + { + // this means all the choices in the in-coming list are the same + // return the first ChoiceSchema, if none is ChoiceSchema (which means all of these are SealedChoiceSchema), we just return the first + return schemas.FirstOrDefault(schema => schema is ChoiceSchema) ?? schemas.First(); + } + + // otherwise throw exception to say we have unresolvable duplicated schema after our renaming + var listStrings = deduplicated.Select(list => $"[{string.Join(", ", list)}]"); + throw new InvalidOperationException($"We have unresolvable duplicated ChoiceSchemas. These are: {string.Join(", ", listStrings)}"); + } + + private struct CollectionComparer : IEqualityComparer> + { + public bool Equals([AllowNull] IOrderedEnumerable x, [AllowNull] IOrderedEnumerable y) + { + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; + return x.SequenceEqual(y); + } + + public int GetHashCode([DisallowNull] IOrderedEnumerable obj) + { + return string.Join("", obj).GetHashCode(); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/FrameworkTypeUpdater.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/FrameworkTypeUpdater.cs new file mode 100644 index 0000000..b54fce5 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/FrameworkTypeUpdater.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class FrameworkTypeUpdater + { + // max number of words to keep if trimming the property + private const int MaxTrimmingPropertyWordCount = 2; + + public static void ValidateAndUpdate(CodeModel codeModel) + { + foreach (var schema in codeModel.AllSchemas) + { + if (schema is not ObjectSchema objSchema) + continue; + + foreach (var property in objSchema.Properties) + { + if (property.CSharpName().EndsWith("Duration", StringComparison.Ordinal) && property.Schema.Type == AllSchemaTypes.String && property.Schema.Extensions?.Format == null) + throw new InvalidOperationException($"The {property.Language.Default.Name} property of {objSchema.Name} ends with \"Duration\" but does not use the duration format to be generated as TimeSpan type. Add \"format\": \"duration\" with directive in autorest.md for the property if it's ISO 8601 format like P1DT2H59M59S. Add \"x-ms-format\": \"{XMsFormat.DurationConstant}\" if it's the constant format like 1.2:59:59.5000000. If the property does not conform to a TimeSpan format, please use \"x-ms-client-name\" to rename the property for the client."); + else if (property.CSharpName().Equals("Type", StringComparison.Ordinal)) + { + // Do not use property.SerializedName=="type" so that we can still use x-ms-client-name to override the auto-renaming here if there is some edge case. + if (objSchema.IsResourceData() || objSchema.CSharpName().Contains("NameAvailability", StringComparison.Ordinal)) + { + property.Language.Default.Name = "resourceType"; + } + else if (property.Schema.Name.EndsWith("Type", StringComparison.Ordinal) && property.Schema.Name.Length != 4) + { + property.Language.Default.Name = GetEnclosingTypeName(objSchema.Name, property.Schema.Name, property.Schema.Type); + } + else if (property.Schema.Name.EndsWith("Types", StringComparison.Ordinal) && property.Schema.Name.Length != 5) + { + property.Language.Default.Name = GetEnclosingTypeName(objSchema.Name, property.Schema.Name.TrimEnd('s'), property.Schema.Type); + } + else + { + throw new InvalidOperationException($"{objSchema.Name} has a property named \"{property.CSharpName()}\" which is not allowed. Please add \"{objSchema.Name}.{property.Language.Default.Name}: {{YourNewPropertyName}}\" into the `rename-mapping` section of your `autorest.md` file to rename the property for the client."); + } + } + } + } + } + + internal static string GetEnclosingTypeName(string parentName, string propertyTypeName, AllSchemaTypes type) + { + if (type == AllSchemaTypes.String) + { + // for string type property, return the original property name so that it's easier to identify the semantic meaning + return propertyTypeName; + } + + var propertyWords = propertyTypeName.SplitByCamelCaseAndGroup().ToArray(); + // we keep at most 2 words, if trim the property + if (propertyWords.Length < MaxTrimmingPropertyWordCount) + { + return propertyTypeName; + } + + var parentWords = parentName.SplitByCamelCaseAndGroup().ToArray(); + var commonPrefixes = new List(); + for (int i = 0; i < parentWords.Length && i < propertyWords.Length; i++) + { + if (parentWords[i] == propertyWords[i]) + { + commonPrefixes.Add(parentWords[i]); + } + else + { + break; + }; + } + + if (commonPrefixes.Count == 0) + { + // if no common prefix, just return the original property name + return propertyTypeName; + } + + var newPropertyWords = propertyWords.TakeLast(propertyWords.Length - commonPrefixes.Count()).ToList(); + if (newPropertyWords.Count > MaxTrimmingPropertyWordCount) + { + commonPrefixes.AddRange(newPropertyWords.Take(newPropertyWords.Count - MaxTrimmingPropertyWordCount)); + newPropertyWords.RemoveRange(0, newPropertyWords.Count - MaxTrimmingPropertyWordCount); + } + + // A property namne cannot start with number, so we need to shift another word from prefixes to new property. + // The worst case is that new property is the original property. The loop should end eventually. + while (newPropertyWords.Count < MaxTrimmingPropertyWordCount || int.TryParse(newPropertyWords.First(), out _)) + { + newPropertyWords.Insert(0, commonPrefixes.Last()); + commonPrefixes.RemoveAt(commonPrefixes.Count - 1); + } + + return String.Join("", newPropertyWords); + } + + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/NormalizeParamNames.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/NormalizeParamNames.cs new file mode 100644 index 0000000..e5ff176 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/NormalizeParamNames.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class NormalizeParamNames + { + internal static string GetNewName(string paramName, string schemaName, IDictionary> dataSchemaHash) + { + if (schemaName.EndsWith("Options", StringComparison.Ordinal)) + return "options"; + + if (schemaName.EndsWith("Info", StringComparison.Ordinal)) + return "info"; + + if (schemaName.EndsWith("Details", StringComparison.Ordinal)) + return "details"; + + if (schemaName.EndsWith("Content", StringComparison.Ordinal)) + return "content"; + + if (schemaName.EndsWith("Patch", StringComparison.Ordinal)) + return "patch"; + + if (schemaName.EndsWith("Input", StringComparison.Ordinal)) + return "input"; + + if (schemaName.EndsWith("Data", StringComparison.Ordinal) || dataSchemaHash.ContainsKey(schemaName)) + return "data"; + + if (paramName.Equals("parameters", StringComparison.OrdinalIgnoreCase)) + return schemaName.FirstCharToLowerCase(); + + return paramName; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/OmitOperationGroups.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/OmitOperationGroups.cs new file mode 100644 index 0000000..f54e43d --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/OmitOperationGroups.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Report; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class OmitOperationGroups + { + public static void RemoveOperationGroups(CodeModel codeModel) + { + var omitSet = Configuration.MgmtConfiguration.OperationGroupsToOmit.ToHashSet(); + if (codeModel.OperationGroups.FirstOrDefault(og => og.Key == "Operations") != null) + { + omitSet.Add("Operations"); + } + if (omitSet.Count > 0) + { + var omittedOGs = codeModel.OperationGroups.Where(og => omitSet.Contains(og.Key)).ToList(); + var nonOmittedOGs = codeModel.OperationGroups.Where(og => !omitSet.Contains(og.Key)).ToList(); + + omittedOGs.ForEach(og => MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.OperationGroupsToOmit, og.Key), + og.GetFullSerializedName(), $"Omit OperationGroup: '{og.GetFullSerializedName()}'")); + + codeModel.OperationGroups = nonOmittedOGs; + var schemasToOmit = new Dictionary>(); + var schemasToKeep = new Dictionary>(); + foreach (var operationGroup in codeModel.OperationGroups) + { + DetectSchemas(operationGroup, schemasToKeep); + } + AddDependantSchemasRecursively(schemasToKeep); + + foreach (var operationGroup in omittedOGs) + { + DetectSchemas(operationGroup, schemasToOmit); + } + AddDependantSchemasRecursively(schemasToOmit); + + RemoveSchemas(schemasToOmit, schemasToKeep, codeModel); + } + } + + private static void RemoveSchemas(Dictionary> schemasToOmit, Dictionary> schemasToKeep, CodeModel codeModel) + { + foreach (var schema in schemasToOmit.Keys) + { + if (schema is ObjectSchema objSchema && !schemasToKeep.ContainsKey(objSchema)) + { + codeModel.Schemas.Objects.Remove(objSchema); + foreach (var og in schemasToOmit[schema]) + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.OperationGroupsToOmit, og.Key), + schema.GetFullSerializedName(), $"Omit Object '{schema.GetFullSerializedName()}' under OperationGroup '{og.GetFullSerializedName()}'"); + RemoveRelations(objSchema, schemasToOmit[objSchema]); + } + else if (schema is ChoiceSchema choiceSchema && !schemasToKeep.ContainsKey(choiceSchema)) + { + codeModel.Schemas.Choices.Remove(choiceSchema); + foreach (var og in schemasToOmit[schema]) + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.OperationGroupsToOmit, og.Key), + schema.GetFullSerializedName(), $"Omit Choice '{schema.GetFullSerializedName()}' under OperationGroup '{og.GetFullSerializedName()}'"); + } + else if (schema is SealedChoiceSchema sealChoiceSchema && !schemasToKeep.ContainsKey(sealChoiceSchema)) + { + codeModel.Schemas.SealedChoices.Remove(sealChoiceSchema); + foreach (var og in schemasToOmit[schema]) + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.OperationGroupsToOmit, og.Key), + schema.GetFullSerializedName(), $"Omit SealedChoice '{schema.GetFullSerializedName()}' under OperationGroup '{og.GetFullSerializedName()}'"); + } + } + } + + private static void RemoveRelations(ObjectSchema schema, HashSet groups) + { + if (schema.Parents != null) + { + foreach (ObjectSchema parent in schema.Parents.Immediate) + { + if (parent.Children != null) + { + parent.Children.Immediate.Remove(schema); + foreach (var og in groups) + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.OperationGroupsToOmit, og.Key), + schema.GetFullSerializedName(), $"Omit related Object '{schema.GetFullSerializedName()}' from related Object '{parent.GetFullSerializedName()}' under OperationGroup '{og.GetFullSerializedName}'"); + } + } + } + } + + private static void AddDependantSchemasRecursively(Dictionary> setToProcess) + { + Queue sQueue = new Queue(setToProcess.Keys); + HashSet handledSchemas = new HashSet(); + while (sQueue.Count > 0) + { + var cur = sQueue.Dequeue(); + handledSchemas.Add(cur); + if (cur is ObjectSchema curSchema) + { + foreach (var property in curSchema.Properties) + { + var propertySchema = property.Schema; + if (propertySchema is ObjectSchema || propertySchema is ChoiceSchema || propertySchema is SealedChoiceSchema) + { + if (!handledSchemas.Contains(propertySchema)) + { + sQueue.Enqueue(propertySchema); + setToProcess.AddSchema(propertySchema, setToProcess[cur].ToArray()); + } + } + else if (propertySchema is ArraySchema arraySchema && arraySchema.ElementType is ObjectSchema arrayPropertySchema) + { + if (!handledSchemas.Contains(arrayPropertySchema)) + { + sQueue.Enqueue(arrayPropertySchema); + setToProcess.AddSchema(arrayPropertySchema, setToProcess[cur].ToArray()); + } + } + } + if (curSchema.Parents != null) + { + foreach (var parent in curSchema.Parents.Immediate) + { + if (parent is ObjectSchema parentSchema) + { + if (!handledSchemas.Contains(parentSchema)) + { + sQueue.Enqueue(parentSchema); + setToProcess.AddSchema(parentSchema, setToProcess[cur].ToArray()); + } + } + } + } + } + } + } + + private static void DetectSchemas(OperationGroup operationGroup, Dictionary> setToProcess) + { + foreach (var operation in operationGroup.Operations) + { + AddResponseSchemas(operationGroup, operation, setToProcess); + AddRequestSchemas(operationGroup, operation, setToProcess); + } + } + + private static void AddResponseSchemas(OperationGroup group, Operation operation, Dictionary> setToProcess) + { + foreach (var response in operation.Responses.Concat(operation.Exceptions)) + { + var schema = response.ResponseSchema; + if (schema != null && schema is ObjectSchema objSchema) + { + setToProcess.AddSchema(objSchema, group); + } + } + } + + private static void AddRequestSchemas(OperationGroup group, Operation operation, Dictionary> setToProcess) + { + foreach (var request in operation.Requests) + { + if (request.Parameters != null) + { + foreach (var param in request.Parameters) + { + if (param.Schema is ObjectSchema objSchema) + { + setToProcess.AddSchema(objSchema, group); + } + } + } + } + } + + private static void AddSchema(this Dictionary> dict, Schema schema, params OperationGroup[] groups) + { + if (!dict.ContainsKey(schema)) + dict.Add(schema, new HashSet()); + foreach (var group in groups) + dict[schema].Add(group); + } + + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/RearrangeParameterOrder.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/RearrangeParameterOrder.cs new file mode 100644 index 0000000..560f161 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/RearrangeParameterOrder.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer; + +internal static class RearrangeParameterOrder +{ + public static void Update(CodeModel codeModel) + { + foreach (var operationGroup in codeModel.OperationGroups) + { + foreach (var operation in operationGroup.Operations) + { + var httpRequest = operation.GetHttpRequest(); + if (httpRequest != null) + { + var orderedParams = operation.Parameters + .Where(p => p.In == HttpParameterIn.Path) + .OrderBy( + p => httpRequest.Path.IndexOf( + "{" + p.GetOriginalName() + "}", + StringComparison.InvariantCultureIgnoreCase)); + operation.Parameters = orderedParams.Concat(operation.Parameters + .Where(p => p.In != HttpParameterIn.Path).ToList()) + .ToList(); + } + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/RenamePluralEnums.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/RenamePluralEnums.cs new file mode 100644 index 0000000..5ac68c8 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/RenamePluralEnums.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class RenamePluralEnums + { + public static void Update(CodeModel codeModel) + { + ImmutableHashSet enumsToKeepPlural = Configuration.MgmtConfiguration.KeepPluralEnums.ToImmutableHashSet(); + + foreach (var schema in codeModel.AllSchemas) + { + if (schema is not SealedChoiceSchema && schema is not ChoiceSchema) + continue; + string schemaName = schema.Language.Default.Name; + if (enumsToKeepPlural.Contains(schemaName)) + { + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.KeepPluralEnums, schemaName), + schema.GetOriginalName(), $"Keep Enum {schemaName} Plural"); + continue; + } + schema.Language.Default.SerializedName ??= schemaName; + schema.Language.Default.Name = schemaName.LastWordToSingular(inputIsKnownToBePlural: false); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/RenameTimeToOn.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/RenameTimeToOn.cs new file mode 100644 index 0000000..a1edbb0 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/RenameTimeToOn.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class RenameTimeToOn + { + private static readonly Dictionary _nounToVerbDicts = new() + { + {"Creation", "Created"}, + {"Deletion", "Deleted"}, + {"Expiration", "Expire"}, + {"Modification", "Modified"}, + }; + + public static void Update(CodeModel codeModel) + { + foreach (var schema in codeModel.AllSchemas) + { + if (schema is not ObjectSchema objSchema) + continue; + + foreach (var property in objSchema.Properties) + { + if (property.Schema.Type is AllSchemaTypes.String or AllSchemaTypes.AnyObject) + { + if (TypeFactory.ToXMsFormatType(property.Schema.Extensions?.Format) != typeof(DateTimeOffset)) + { + continue; + } + } + else if (property.Schema.Type is not (AllSchemaTypes.Date or AllSchemaTypes.DateTime or AllSchemaTypes.Unixtime)) + { + continue; + } + + var propName = property.CSharpName(); + + if (propName.StartsWith("From", StringComparison.Ordinal) || + propName.StartsWith("To", StringComparison.Ordinal) || + propName.EndsWith("PointInTime", StringComparison.Ordinal)) + continue; + + var lengthToCut = 0; + if (propName.Length > 8 && + propName.EndsWith("DateTime", StringComparison.Ordinal)) + { + lengthToCut = 8; + } + else if (propName.Length > 4 && + propName.EndsWith("Time", StringComparison.Ordinal) || + propName.EndsWith("Date", StringComparison.Ordinal)) + { + lengthToCut = 4; + } + else if (propName.Length > 2 && + propName.EndsWith("At", StringComparison.Ordinal)) + { + lengthToCut = 2; + } + + if (lengthToCut > 0) + { + var prefix = propName.Substring(0, propName.Length - lengthToCut); + var newName = (_nounToVerbDicts.TryGetValue(prefix, out var verb) ? verb : prefix) + "On"; + property.Language.Default.Name = newName; + } + } + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/SchemaFormatByNameTransformer.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/SchemaFormatByNameTransformer.cs new file mode 100644 index 0000000..0c65f8f --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/SchemaFormatByNameTransformer.cs @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal class SchemaFormatByNameTransformer + { + internal enum MatchPattern + { + Full = 0, + StartWith = 1, + EndWith = 2, + } + + internal struct FormatRule + { + internal NamePattern NamePattern { get; init; } + internal FormatPattern FormatPattern { get; init; } + } + + internal record FormatPattern(bool IsPrimitiveType, AllSchemaTypes? PrimitiveType, string RawValue, string? ExtensionType) + { + internal static FormatPattern Parse(string value) + { + if (TypeFactory.ToXMsFormatType(value) != null) + { + return new FormatPattern(false, null, value, value); + } + else + { + if (!Enum.TryParse(value, true, result: out var primitiveType)) + { + throw new Exception($"Invalid FormatByName rule value: {value}."); + } + return new FormatPattern(true, primitiveType, value, null); + } + } + } + + internal record NamePattern(MatchPattern Pattern, string Name, string RawValue) + { + internal static NamePattern Parse(string key) => key switch + { + _ when key.StartsWith('*') => new NamePattern(MatchPattern.EndWith, key.TrimStart('*'), key), + _ when key.EndsWith('*') => new NamePattern(MatchPattern.StartWith, key.TrimEnd('*'), key), + _ => new NamePattern(MatchPattern.Full, key, key) + }; + } + + /// + /// Change the Schema's format by its name. + /// + internal static void Update(CodeModel codeModel) + { + SchemaFormatByNameTransformer transformer = new SchemaFormatByNameTransformer( + codeModel.AllSchemas, + codeModel.OperationGroups, + Configuration.MgmtConfiguration.FormatByNameRules); + transformer.UpdateAllSchemas(); + } + + private IEnumerable allGeneralSchemas; + private IEnumerable allOperationGroups; + private IReadOnlyDictionary allFormatByNameRules; + private Dictionary schemaCache = new(); + + internal SchemaFormatByNameTransformer( + IEnumerable generalSchemas, + IEnumerable operationGroups, + IReadOnlyDictionary allFormatByNameRules) + { + this.allGeneralSchemas = generalSchemas; + this.allOperationGroups = operationGroups; + this.allFormatByNameRules = allFormatByNameRules; + } + + public void UpdateAllSchemas() + { + var rules = ParseRules(allFormatByNameRules).ToList(); + if (rules.Count == 0) + return; + UpdateGeneralSchema(rules); + UpdateOperationSchema(rules); + } + + internal void UpdateGeneralSchema(IReadOnlyList rules) + { + foreach (Schema schema in allGeneralSchemas) + { + if (schema is ObjectSchema objectSchema) + { + TryUpdateObjectSchemaFormat(objectSchema, rules); + } + } + } + + internal void UpdateOperationSchema(IReadOnlyList rules) + { + foreach (var operationGroup in allOperationGroups) + { + foreach (var operation in operationGroup.Operations) + { + foreach (var parameter in operation.Parameters) + { + TryUpdateParameterFormat(operation, parameter, rules); + } + } + } + } + + private void TryUpdateParameterFormat(Operation operation, RequestParameter parameter, IReadOnlyList rules) + { + if (parameter.Schema is PrimitiveSchema) + { + int ruleIdx = CheckRules(parameter.CSharpName(), rules); + if (ruleIdx >= 0) + { + var curRule = rules[ruleIdx]; + var formatPattern = curRule.FormatPattern; + if (!formatPattern.IsPrimitiveType) + { + // As the Schema is shared by parameter, so here only can change the ext. format + if (parameter.Extensions == null) + parameter.Extensions = new RecordOfStringAndAny(); + var oriFormat = parameter.Extensions.Format; + parameter.Extensions.Format = formatPattern.ExtensionType; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + TransformTypeName.FormatByNameRules, curRule.NamePattern.RawValue, curRule.FormatPattern.RawValue, + operation.GetFullSerializedName(parameter), + "ApplyNewExFormatOnOperationParameter", oriFormat, parameter.Extensions.Format); + } + } + } + } + + private void TryUpdateObjectSchemaFormat(ObjectSchema objectSchema, IReadOnlyList rules) + { + foreach (var property in objectSchema.Properties) + { + if (property.Schema is ArraySchema propertyArraySchema) + TryUpdateSchemaFormat(property.CSharpName(), propertyArraySchema.ElementType, rules, objectSchema.GetFullSerializedName(property)); + else + TryUpdateSchemaFormat(property.CSharpName(), property.Schema, rules, objectSchema.GetFullSerializedName(property)); + } + } + + private int TryUpdateSchemaFormat(string name, Schema schema, IReadOnlyList rules, string targetFullSerializedName) + { + int ruleIdx = -1; + if (schema is not PrimitiveSchema) + return ruleIdx; + if (schemaCache.TryGetValue(schema, out var cache)) + { + if (!name.Equals(cache.CSharpName)) + Console.Error.WriteLine($"WARNING: The schema '{schema.CSharpName()}' is shared by '{name}' and '{cache.CSharpName}' which is unexpected."); + if (cache.Transform != null) + MgmtReport.Instance.TransformSection.AddTransformLog(cache.Transform, targetFullSerializedName, cache.TransformLogMessage); + return ruleIdx; + } + ruleIdx = CheckRules(name, rules); + TransformItem? transform = null; + string transformLogMessage = ""; + if (ruleIdx >= 0) + { + var curRule = rules[ruleIdx]; + var formatPattern = curRule.FormatPattern; + transform = new TransformItem(TransformTypeName.FormatByNameRules, curRule.NamePattern.RawValue, curRule.FormatPattern.RawValue); + if (formatPattern.IsPrimitiveType) + { + var oriType = schema.Type; + schema.Type = formatPattern.PrimitiveType!.Value; + transformLogMessage = $"ApplyNewType '{oriType}' --> '{schema.Type}'"; + MgmtReport.Instance.TransformSection.AddTransformLog(transform, targetFullSerializedName, transformLogMessage); + } + else + { + if (schema.Extensions == null) + schema.Extensions = new RecordOfStringAndAny(); + string? oriExFormat = schema.Extensions.Format; + schema.Extensions.Format = formatPattern.ExtensionType!; + transformLogMessage = $"ApplyNewExFormat '{oriExFormat ?? ""}' --> '{schema.Extensions.Format ?? ""}'"; + MgmtReport.Instance.TransformSection.AddTransformLog(transform, targetFullSerializedName, transformLogMessage); + } + } + schemaCache[schema] = (CSharpName: name, Transform: transform, TransformLogMessage: transformLogMessage); + return ruleIdx; + } + + private int CheckRules(string name, IReadOnlyList rules) + { + for (int i = 0; i < rules.Count; i++) + { + var namePattern = rules[i].NamePattern; + var isMatch = namePattern.Pattern switch + { + MatchPattern.StartWith => name.StartsWith(namePattern.Name, StringComparison.Ordinal), + MatchPattern.EndWith => name.EndsWith(namePattern.Name, StringComparison.Ordinal), + MatchPattern.Full => FullStringComapre(name, namePattern.Name), + _ => throw new NotImplementedException($"Unknown pattern {namePattern.Pattern}"), + }; + if (isMatch) + { + return i; + } + } + return -1; + } + + private IEnumerable ParseRules(IReadOnlyDictionary formatByNameRules) + { + if (formatByNameRules == null) + yield break; + foreach ((var key, var value) in formatByNameRules) + { + // parse match pattern + var matchPattern = NamePattern.Parse(key); + // parse format pattern + var formatPattern = FormatPattern.Parse(value); + yield return new FormatRule() + { + NamePattern = matchPattern, + FormatPattern = formatPattern + }; + } + } + + private bool FullStringComapre(string strA, string strB) + { + if (strA.Length != strB.Length) + return false; + if (char.ToLower(strA[0]) != char.ToLower(strB[0])) + { + // Ignore case for the first character, + // as autorect auto-upper case the first character for model & property name but not for parameter name. + return false; + } + for (int i = 1; i < strA.Length; i++) + { + if (!strA[i].Equals(strB[i])) + return false; + } + return true; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/SchemaNameAndFormatUpdater.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/SchemaNameAndFormatUpdater.cs new file mode 100644 index 0000000..ab8e824 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/SchemaNameAndFormatUpdater.cs @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Report; +using static AutoRest.CSharp.Mgmt.Decorator.Transformer.SchemaFormatByNameTransformer; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer; + +internal static class SchemaNameAndFormatUpdater +{ + private const char NameFormatSeparator = '|'; + private const string EmptyName = "-"; + + public static void ApplyRenameMapping(CodeModel codeModel) + { + var renameTargets = GetRenameAndReformatTargets().ToList(); + // apply them one by one + foreach (var schema in codeModel.AllSchemas) + { + ApplyRenameTargets(schema, renameTargets); + } + + var parameterRenameTargets = new Dictionary>(); + foreach ((var operationId, var values) in Configuration.MgmtConfiguration.ParameterRenameMapping) + { + parameterRenameTargets.Add(operationId, GetParameterRenameTargets(values)); + } + + foreach (var operationGroup in codeModel.OperationGroups) + { + foreach (var operation in operationGroup.Operations) + { + if (parameterRenameTargets.TryGetValue(operation.OperationId ?? string.Empty, out var parameterTargets)) + { + ApplyRenameTargets(operation, parameterTargets); + } + } + } + } + + private static IEnumerable GetRenameAndReformatTargets() + { + foreach ((var key, var value) in Configuration.MgmtConfiguration.RenameMapping) + { + yield return new RenameAndReformatTarget(TransformTypeName.RenameMapping, key, value); + } + } + + private static IReadOnlyDictionary GetParameterRenameTargets(IReadOnlyDictionary rawMapping) + { + var result = new Dictionary(); + foreach ((var key, var value) in rawMapping) + { + result.Add(key, new RenameAndReformatTarget(TransformTypeName.ParameterRenameMapping, key, value)); + } + + return result; + } + + private static void ApplyRenameTargets(Operation operation, IReadOnlyDictionary renameTargets) + { + // temporarily we only support change name of the parameter + // change the path and query parameters + foreach (var parameter in operation.Parameters) + { + ApplyRenameTarget(operation, parameter, renameTargets); + } + + // body parameter is not included above + var bodyParameter = operation.GetBodyParameter(); + if (bodyParameter != null) + { + ApplyRenameTarget(operation, bodyParameter, renameTargets); + } + } + + private static void ApplyRenameTarget(Operation operation, RequestParameter parameter, IReadOnlyDictionary renameTargets) + { + if (renameTargets.TryGetValue(parameter.GetOriginalName(), out var target)) + { + // apply the rename + string oriName = parameter.Language.Default.Name; + parameter.Language.Default.SerializedName ??= parameter.Language.Default.Name; + parameter.Language.Default.Name = target.NewName; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(target.Source, target.Key, operation.OperationId!, target.Value), + operation.GetFullSerializedName(parameter), + "ApplyRenameParameter", oriName, parameter.Language.Default.Name); + } + } + + private static void ApplyRenameTargets(Schema schema, IEnumerable renameTargets) + { + foreach (var target in renameTargets) + { + ApplyRenameTarget(schema, target); + } + } + + private static void ApplyRenameTarget(Schema schema, RenameAndReformatTarget renameTarget) + { + switch (schema) + { + case ChoiceSchema choiceSchema: + ApplyChoiceSchema(choiceSchema, renameTarget); + break; + case SealedChoiceSchema sealedChoiceSchema: + ApplySealedChoiceSchema(sealedChoiceSchema, renameTarget); + break; + case ObjectSchema objectSchema: // GroupSchema inherits from ObjectSchema, therefore this line changes both + ApplyObjectSchema(objectSchema, renameTarget); + break; + } + } + + private static void ApplyChoiceSchema(ChoiceSchema choiceSchema, RenameAndReformatTarget renameTarget) + { + switch (renameTarget.RenameType) + { + case RenameType.Type: + ApplyToType(choiceSchema, renameTarget); + break; + case RenameType.Property: + ApplyToChoiceValue(choiceSchema, choiceSchema.Choices, renameTarget); + break; + } + } + + private static void ApplySealedChoiceSchema(SealedChoiceSchema sealedChoiceSchema, RenameAndReformatTarget renameTarget) + { + switch (renameTarget.RenameType) + { + case RenameType.Type: + ApplyToType(sealedChoiceSchema, renameTarget); + break; + case RenameType.Property: + ApplyToChoiceValue(sealedChoiceSchema, sealedChoiceSchema.Choices, renameTarget); + break; + } + } + + private static void ApplyObjectSchema(ObjectSchema objectSchema, RenameAndReformatTarget renameTarget) + { + switch (renameTarget.RenameType) + { + case RenameType.Type: + ApplyToType(objectSchema, renameTarget); + break; + case RenameType.Property: + ApplyToProperty(objectSchema, objectSchema.Properties, renameTarget); + break; + } + } + + private static void ApplyToType(Schema schema, RenameAndReformatTarget renameTarget) + { + if (schema.GetOriginalName() != renameTarget.TypeName) + return; + if (!string.IsNullOrEmpty(renameTarget.NewName)) + { + ApplyNewName(schema.Language, renameTarget, schema.GetFullSerializedName()); + } + // we just ignore the format information on this + } + + private static void ApplyToChoiceValue(Schema schema, IEnumerable choices, RenameAndReformatTarget renameTarget) + { + if (schema.GetOriginalName() != renameTarget.TypeName) + return; + var choiceValue = choices.FirstOrDefault(choice => choice.Value == renameTarget.PropertyName); + if (choiceValue == null) + return; + ApplyNewName(choiceValue.Language, renameTarget, schema.GetFullSerializedName(choiceValue)); + // we just ignore the format information on this + } + + private static void ApplyToProperty(ObjectSchema schema, IEnumerable properties, RenameAndReformatTarget renameTarget) + { + Debug.Assert(renameTarget.PropertyName != null); + if (schema.GetOriginalName() != renameTarget.TypeName) + return; + // check if the property renaming is targeting a flattened property + var flattenedNames = Array.Empty(); + if (renameTarget.PropertyName.Contains('.')) + { + flattenedNames = renameTarget.PropertyName.Split('.'); + } + var propertySerializedName = flattenedNames.LastOrDefault() ?? renameTarget.PropertyName; + // filter the property name by the serialized name + var filteredProperties = properties.Where(p => p.SerializedName == propertySerializedName); + var property = filteredProperties.FirstOrDefault(p => p.FlattenedNames.SequenceEqual(flattenedNames)); + if (property == null) + return; + ApplyNewName(property.Language, renameTarget, schema.GetFullSerializedName(property)); + + if (property.Schema is ArraySchema arraySchema) + { + ApplyNewFormat(arraySchema.ElementType, renameTarget, schema.GetFullSerializedName(property)); + } + else + { + ApplyNewFormat(property.Schema, renameTarget, schema.GetFullSerializedName(property)); + } + } + + public static void UpdateAcronyms(CodeModel codeModel) + { + if (Configuration.MgmtConfiguration.AcronymMapping.Count == 0) + return; + // first transform all the name of schemas, properties + UpdateAcronyms(codeModel.AllSchemas); + // transform all the parameter names + UpdateAcronyms(codeModel.OperationGroups); + } + + private static void ApplyNewName(Languages language, RenameAndReformatTarget rrt, string targetFullSerializedName) + { + string? value = rrt.NewName; + if (value == null) + return; + string oriName = language.Default.Name; + language.Default.SerializedName ??= language.Default.Name; + language.Default.Name = value; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange(rrt.Source, rrt.Key, rrt.Value, targetFullSerializedName, + "ApplyNewName", oriName, value); + } + + private static void ApplyNewFormat(Schema schema, RenameAndReformatTarget rrt, string targetFullSerializedName) + { + FormatPattern? formatPattern = rrt.NewFormat; + if (formatPattern == null) + return; + if (formatPattern.IsPrimitiveType) + { + AllSchemaTypes oriType = schema.Type; + schema.Type = formatPattern.PrimitiveType!.Value; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange(rrt.Source, rrt.Key, rrt.Value, targetFullSerializedName, + "ApplyNewType", oriType.ToString(), schema.Type.ToString()); + } + else + { + if (schema.Extensions == null) + schema.Extensions = new RecordOfStringAndAny(); + string? oriFormat = schema.Extensions.Format; + schema.Extensions.Format = formatPattern.ExtensionType!; + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange(rrt.Source, rrt.Key, rrt.Value, targetFullSerializedName, + "ApplyNewFormat", oriFormat, schema.Extensions.Format); + } + } + + private record RenameAndReformatTarget + { + internal string Source { get; } + internal string Key { get; } + internal string Value { get; } + + internal RenameType RenameType { get; } + internal string TypeName { get; } + internal string? PropertyName { get; } + internal string? NewName { get; } + internal FormatPattern? NewFormat { get; } + + internal RenameAndReformatTarget(string source, string renameKey, string value) + { + this.Source = source; + Key = renameKey; + Value = value; + // we do not support escape the character dot right now. In case in the future some swagger might have dot inside a property name, we need to support this. Really? + if (renameKey.Contains('.')) + { + // this should be a renaming of property + var segments = renameKey.Split('.', 2); // split at the first dot + RenameType = RenameType.Property; + TypeName = segments[0]; + PropertyName = segments[1]; + } + else + { + // this should be a renaming of type + RenameType = RenameType.Type; + TypeName = renameKey; + PropertyName = null; + } + if (value.Contains(NameFormatSeparator)) + { + var segments = value.Split(NameFormatSeparator); + if (segments.Length > 2) + throw new InvalidOperationException($"value for rename-mapping can only contains one |, but get `{value}`"); + + NewName = IsEmptyName(segments[0]) ? null : segments[0]; + NewFormat = FormatPattern.Parse(segments[1]); + } + else + { + NewName = value; + NewFormat = null; + } + } + + private static bool IsEmptyName(string name) => string.IsNullOrEmpty(name) || name == EmptyName; + } + + private enum RenameType + { + Type = 0, Property = 1 + } + + public static void UpdateAcronym(InputType schema) + { + if (Configuration.MgmtConfiguration.AcronymMapping.Count == 0) + return; + TransformInputType(schema); + } + + private static void UpdateAcronyms(IEnumerable allSchemas) + { + foreach (var schema in allSchemas) + { + TransformSchema(schema); + } + } + + private static void UpdateAcronyms(IEnumerable operationGroups) + { + foreach (var operationGroup in operationGroups) + { + foreach (var operation in operationGroup.Operations) + { + TransformOperation(operation); + } + } + } + + private static void TransformOperation(Operation operation) + { + TransformLanguage(operation.Language, operation.GetFullSerializedName()); + // this iteration only applies to path and query parameter (maybe headers?) but not to body parameter + foreach (var parameter in operation.Parameters) + { + TransformLanguage(parameter.Language, operation.GetFullSerializedName(parameter)); + } + + // we need to iterate over the parameters in each request (actually only one request) to ensure the name of body parameters are also taken care of + foreach (var request in operation.Requests) + { + foreach (var parameter in request.Parameters) + { + TransformLanguage(parameter.Language, operation.GetFullSerializedName(parameter)); + } + } + } + + private static void TransformInputType(InputType inputType) + { + switch (inputType) + { + case InputEnumType inputEnum: + TransformInputEnumType(inputEnum, inputEnum.Values); + break; + case InputModelType inputModel: + TransformInputModel(inputModel); + break; + default: + throw new InvalidOperationException($"Unknown input type {inputType.GetType()}"); + } + } + + private static void TransformInputEnumType(InputEnumType inputEnum, IReadOnlyList choiceValues) + { + TransformInputType(inputEnum, inputEnum.GetFullSerializedName()); + TransformChoices(inputEnum, choiceValues); + } + + private static void TransformChoices(InputEnumType schema, IReadOnlyList choiceValues) + { + foreach (var choiceValue in choiceValues) + { + var originalName = choiceValue.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName, (applyStep) => + { + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange(TransformTypeName.AcronymMapping, applyStep.MappingKey, applyStep.MappingValue.RawValue!, schema.GetFullSerializedName(choiceValue), + "ApplyAcronymMapping", tempName, applyStep.NewName.Name); + tempName = applyStep.NewName.Name; + }); + choiceValue.Name = result.Name; + } + } + + private static void TransformInputType(InputType inputType, string targetFullSerializedName) + { + var originalName = inputType.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName, (applyStep) => + { + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange(TransformTypeName.AcronymMapping, applyStep.MappingKey, applyStep.MappingValue.RawValue!, targetFullSerializedName, + "ApplyAcronymMapping", tempName, applyStep.NewName.Name); + tempName = applyStep.NewName.Name; + }); + inputType.Name = result.Name; + } + + private static void TransformInputModel(InputModelType inputModel) + { + // transform the name of this schema + TransformInputType(inputModel, inputModel.GetFullSerializedName()); + foreach (var property in inputModel.Properties) + { + TransformInputType(property.Type, inputModel.GetFullSerializedName(property)); + } + } + + private static void TransformSchema(Schema schema) + { + switch (schema) + { + case ChoiceSchema choiceSchema: + TransformChoiceSchema(choiceSchema, choiceSchema.Choices); + break; + case SealedChoiceSchema sealedChoiceSchema: + TransformChoiceSchema(sealedChoiceSchema, sealedChoiceSchema.Choices); + break; + case ObjectSchema objSchema: // GroupSchema inherits ObjectSchema, therefore we do not need to handle that + TransformObjectSchema(objSchema); + break; + default: + throw new InvalidOperationException($"Unknown schema type {schema.GetType()}"); + } + } + + private static void TransformChoiceSchema(Schema schema, ICollection choiceValues) + { + TransformLanguage(schema.Language, schema.GetFullSerializedName()); + TransformChoices(schema, choiceValues); + } + + private static void TransformChoices(Schema schema, ICollection choiceValues) + { + foreach (var choiceValue in choiceValues) + { + TransformLanguage(choiceValue.Language, schema.GetFullSerializedName(choiceValue)); + } + } + + private static void TransformLanguage(Languages languages, string targetFullSerializedName) + { + var originalName = languages.Default.Name; + var tempName = originalName; + var result = NameTransformer.Instance.EnsureNameCase(originalName, (applyStep) => + { + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange(TransformTypeName.AcronymMapping, applyStep.MappingKey, applyStep.MappingValue.RawValue!, targetFullSerializedName, + "ApplyAcronymMapping", tempName, applyStep.NewName.Name); + tempName = applyStep.NewName.Name; + }); + languages.Default.Name = result.Name; + languages.Default.SerializedName ??= originalName; + } + + private static void TransformObjectSchema(ObjectSchema objSchema) + { + // transform the name of this schema + TransformLanguage(objSchema.Language, objSchema.GetFullSerializedName()); + foreach (var property in objSchema.Properties) + { + TransformLanguage(property.Language, objSchema.GetFullSerializedName(property)); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/SealedChoicesUpdater.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/SealedChoicesUpdater.cs new file mode 100644 index 0000000..34f839d --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/SealedChoicesUpdater.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class SealedChoicesUpdater + { + private static readonly List EnumValuesShouldBePrompted = new() + { + "None", "NotSet", "Unknown", "NotSpecified", "Unspecified", "Undefined" + }; + + public static void UpdateSealChoiceTypes(CodeModel codeModel) + { + var wordCandidates = new List(EnumValuesShouldBePrompted.Concat(Configuration.MgmtConfiguration.PromptedEnumValues)); + foreach (var schema in codeModel.AllSchemas) + { + if (schema is not SealedChoiceSchema choiceSchema) + continue; + + // rearrange the sequence in the choices + choiceSchema.Choices = RearrangeChoices(choiceSchema.Choices, wordCandidates); + } + } + + internal static ICollection RearrangeChoices(ICollection originalValues, List wordCandidates) + { + return originalValues.OrderBy(choice => + { + var name = choice.CSharpName(); + var index = wordCandidates.IndexOf(name); + return index >= 0 ? index : wordCandidates.Count; + }).ToList(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/SerializedNamesUpdater.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/SerializedNamesUpdater.cs new file mode 100644 index 0000000..c5bc04d --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/SerializedNamesUpdater.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class SerializedNamesUpdater + { + public static void Update(CodeModel codeModel) + { + foreach (var schema in codeModel.AllSchemas) + { + switch (schema) + { + case ChoiceSchema choiceSchema: + UpdateChoiceSchema(choiceSchema); + break; + case SealedChoiceSchema sealedSchema: + UpdateSealedChoiceSchema(sealedSchema); + break; + case ObjectSchema objectSchema: + UpdateObjectSchema(objectSchema); + break; + } + } + } + + private static void UpdateChoiceSchema(ChoiceSchema choiceSchema) + { + // update the choice type, append the serialized name of this type to its original description + choiceSchema.Language.Default.Description = $"{CreateDescription(choiceSchema)}\n{CreateSerializedNameDescription(choiceSchema.GetFullSerializedName())}"; + // update the choice values + foreach (var choice in choiceSchema.Choices) + { + choice.Language.Default.Description = $"{CreateDescription(choice)}\n{CreateSerializedNameDescription(choiceSchema.GetFullSerializedName(choice))}"; + } + } + + private static void UpdateSealedChoiceSchema(SealedChoiceSchema sealedChoiceSchema) + { + // update the sealed choice type, append the serialized name of this type to its original description + sealedChoiceSchema.Language.Default.Description = $"{CreateDescription(sealedChoiceSchema)}\n{CreateSerializedNameDescription(sealedChoiceSchema.GetFullSerializedName())}"; + foreach (var choice in sealedChoiceSchema.Choices) + { + choice.Language.Default.Description = $"{CreateDescription(choice)}\n{CreateSerializedNameDescription(sealedChoiceSchema.GetFullSerializedName(choice))}"; + } + } + + private static void UpdateObjectSchema(ObjectSchema objectSchema) + { + // update the sealed choice type, append the serialized name of this type to its original description + objectSchema.Language.Default.Description = $"{CreateDescription(objectSchema)}\n{CreateSerializedNameDescription(objectSchema.GetFullSerializedName())}"; + foreach (var property in objectSchema.Properties) + { + var originalDescription = string.IsNullOrEmpty(property.Language.Default.Description) ? string.Empty : $"{property.Language.Default.Description}\n"; + property.Language.Default.Description = $"{originalDescription}{CreateSerializedNameDescription(objectSchema.GetFullSerializedName(property))}"; + } + } + + private static string CreateDescription(Schema schema) => string.IsNullOrWhiteSpace(schema.Language.Default.Description) ? + $"The {schema.Name}.": + schema.Language.Default.Description; + + private static string CreateDescription(ChoiceValue choiceValue) => string.IsNullOrWhiteSpace(choiceValue.Language.Default.Description) + ? choiceValue.Value + : choiceValue.Language.Default.Description; + + private static string CreateSerializedNameDescription(string fullSerializedName) => $"Serialized Name: {fullSerializedName}"; + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/SubscriptionIdUpdater.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/SubscriptionIdUpdater.cs new file mode 100644 index 0000000..4271957 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/SubscriptionIdUpdater.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class SubscriptionIdUpdater + { + public static void Update(CodeModel codeModel) + { + bool setSubParam = false; + foreach (var operationGroup in codeModel.OperationGroups) + { + foreach (var op in operationGroup.Operations) + { + foreach (var p in op.Parameters) + { + // update the first subscriptionId parameter to be 'method' parameter + if (!setSubParam && p.Language.Default.Name.Equals("subscriptionId", StringComparison.OrdinalIgnoreCase)) + { + setSubParam = true; + p.Implementation = ImplementationLocation.Method; + p.Schema.Type = AllSchemaTypes.String; + } + // update the apiVersion parameter to be 'client' parameter + if (p.Origin is not null && p.Origin.Equals("modelerfour:synthesized/api-version", StringComparison.OrdinalIgnoreCase)) + { + p.Implementation = ImplementationLocation.Client; + } + } + } + } + } + + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/Transformer/UrlToUri.cs b/logger/autorest.csharp/mgmt/Decorator/Transformer/UrlToUri.cs new file mode 100644 index 0000000..be66202 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/Transformer/UrlToUri.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using AutoRest.CSharp.Input; + +namespace AutoRest.CSharp.Mgmt.Decorator.Transformer +{ + internal static class UrlToUri + { + private static readonly char LowerCaseI = 'i'; + + public static void UpdateSuffix(CodeModel codeModel) + { + foreach (var schema in codeModel.AllSchemas) + { + if (schema is not ObjectSchema objSchema) + continue; + + var schemaName = schema.Language.Default.Name; + if (schemaName.EndsWith("Url", StringComparison.Ordinal)) + schema.Language.Default.Name = schemaName.Substring(0, schemaName.Length - 1) + LowerCaseI; + + foreach (var property in objSchema.Properties) + { + var propertyName = property.Language.Default.Name; + if (propertyName.EndsWith("Url", StringComparison.Ordinal)) + property.Language.Default.Name = propertyName.Substring(0, propertyName.Length - 1) + LowerCaseI; + } + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/TypeExtensions.cs b/logger/autorest.csharp/mgmt/Decorator/TypeExtensions.cs new file mode 100644 index 0000000..5f66a76 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/TypeExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using Azure; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + internal static class TypeExtensions + { + public static CSharpType WrapPageable(this CSharpType type, bool isAsync) + { + return isAsync ? new CSharpType(typeof(AsyncPageable<>), type) : new CSharpType(typeof(Pageable<>), type); + } + + public static CSharpType WrapAsync(this CSharpType type, bool isAsync) + { + return isAsync ? new CSharpType(typeof(Task<>), type) : type; + } + + public static CSharpType WrapResponse(this CSharpType type, bool isAsync, bool isNullable) + { + var response = new CSharpType(isNullable ? typeof(NullableResponse<>) : Configuration.ApiTypes.ResponseOfTType, type); + return isAsync ? new CSharpType(typeof(Task<>), response) : response; + } + + public static CSharpType WrapOperation(this CSharpType type, bool isAsync) + { + var response = new CSharpType(typeof(ArmOperation<>), type); + return isAsync ? new CSharpType(typeof(Task<>), response) : response; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Decorator/TypeReferenceTypeChooser.cs b/logger/autorest.csharp/mgmt/Decorator/TypeReferenceTypeChooser.cs new file mode 100644 index 0000000..0dd2526 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Decorator/TypeReferenceTypeChooser.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Output; + +namespace AutoRest.CSharp.Mgmt.Decorator +{ + /// + /// This is a utility class to check and keep the result of whether a MgmtObjectType class can be replaced by + /// an external type. + /// + internal static class TypeReferenceTypeChooser + { + private static ConcurrentDictionary _valueCache = new ConcurrentDictionary(); + + /// + /// Check whether a MgmtObjectType class can be replaced by an external type, and return the external type if available. + /// + /// Type to check + /// Matched external type or null if not found + public static CSharpType? GetExactMatch(MgmtObjectType typeToReplace) + { + if (_valueCache.TryGetValue(typeToReplace.InputModel, out var result)) + return result; + + var replacedType = BuildExactMatchType(typeToReplace); + + _valueCache.TryAdd(typeToReplace.InputModel, replacedType); + return replacedType; + } + + private static CSharpType? BuildExactMatchType(MgmtObjectType typeToReplace) + { + foreach (System.Type replacementType in ReferenceClassFinder.TypeReferenceTypes) + { + if (PropertyMatchDetection.IsEqual(replacementType, typeToReplace)) + { + var csharpType = CSharpType.FromSystemType(MgmtContext.Context, replacementType, typeToReplace.MyProperties); + _valueCache.TryAdd(typeToReplace.InputModel, csharpType); + return csharpType; + } + } + + // nothing matches, return null + return null; + } + + /// + /// Check whether there is a match for the given schema. + /// + /// ObjectSchema of the target type + /// + public static bool HasMatch(InputModelType schema) + { + return _valueCache.TryGetValue(schema, out var match) && match != null; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/ArmClientExtensionWriter.cs b/logger/autorest.csharp/mgmt/Generation/ArmClientExtensionWriter.cs new file mode 100644 index 0000000..0c57c19 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/ArmClientExtensionWriter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal sealed class ArmClientExtensionWriter : MgmtExtensionWriter + { + private ArmClientExtension This { get; } + + public ArmClientExtensionWriter(ArmClientExtension extension) : this(new CodeWriter(), extension) + { + } + + public ArmClientExtensionWriter(CodeWriter writer, ArmClientExtension extension) : base(writer, extension) + { + This = extension; + } + + protected internal override void WriteImplementations() + { + base.WriteImplementations(); + + foreach (var method in This.ArmResourceMethods) + { + _writer.WriteMethodDocumentation(method.Signature); + _writer.WriteMethod(method); + } + } + + protected override void WriteMethod(MgmtClientOperation clientOperation, bool isAsync) + { + using (_writer.WriteCommonMethod(clientOperation.MethodSignature, null, isAsync, This.Accessibility == "public", SkipParameterValidation)) + { + WriteMethodBodyWrapper(clientOperation.MethodSignature, isAsync, clientOperation.IsPagingOperation); + } + _writer.Line(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MgmtClientBaseWriter.cs b/logger/autorest.csharp/mgmt/Generation/MgmtClientBaseWriter.cs new file mode 100644 index 0000000..4637fe4 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MgmtClientBaseWriter.cs @@ -0,0 +1,777 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using AutoRest.CSharp.Common.Generation.Writers; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.ResourceManager.ManagementGroups; +using Azure.ResourceManager.Resources; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal abstract class MgmtClientBaseWriter : ClientWriter + { + protected const string EndpointProperty = "Endpoint"; + protected delegate void WriteMethodDelegate(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync); + private string LibraryArmOperation { get; } + protected bool IsArmCore { get; } + protected CodeWriter _writer; + protected override string RestClientAccessibility => "private"; + + private MgmtTypeProvider This { get; } + + protected virtual string ArmClientReference { get; } = "Client"; + + protected virtual bool UseField => true; + + protected virtual bool SkipParameterValidation => false; + + public string FileName { get; } + + protected MgmtClientBaseWriter(CodeWriter writer, MgmtTypeProvider provider) + { + _writer = writer; + This = provider; + FileName = This.Type.Name; + IsArmCore = Configuration.MgmtConfiguration.IsArmCore; + LibraryArmOperation = $"{MgmtContext.Context.DefaultNamespace.Split('.').Last()}ArmOperation"; + } + + public virtual void Write() + { + using (_writer.Namespace(This.Namespace)) + { + WriteClassDeclaration(); + using (_writer.Scope()) + { + WriteImplementations(); + } + } + } + + protected internal virtual void WriteImplementations() + { + WriteStaticMethods(); + + WriteFields(); + + WriteCtors(); + + WriteProperties(); + + WritePrivateHelpers(); + + _writer.Line(); // TODO -- add this here to minimize the amount of code changes, this could be removed after future refactor + foreach (var method in This.ChildResourceEntryMethods) + { + _writer.WriteMethodDocumentation(method.Signature); + _writer.WriteMethod(method); + } + + WriteOperations(); + + if (This.EnumerableInterfaces.Any()) + WriteEnumerables(); + } + + protected virtual void WritePrivateHelpers() { } + protected virtual void WriteProperties() { } + protected virtual void WriteStaticMethods() { } + + protected virtual void WriteOperations() + { + foreach (var clientOperation in This.AllOperations) + { + WriteMethod(clientOperation, true); + WriteMethod(clientOperation, false); + } + } + + protected void WriteClassDeclaration() + { + _writer.WriteXmlDocumentationSummary(This.Description); + _writer.AppendRaw(This.Accessibility) + .AppendRawIf(" static", This.IsStatic) + .Append($" partial class {This.Type.Name}"); + if (This.GetImplements().Any()) + { + _writer.Append($" : "); + foreach (var type in This.GetImplements()) + { + _writer.Append($"{type:D},"); + } + _writer.RemoveTrailingComma(); + } + _writer.Line(); + } + + protected virtual void WriteCtors() + { + if (This.IsStatic) + return; + + if (This.MockingCtor is not null) + { + _writer.WriteMethodDocumentation(This.MockingCtor); + using (_writer.WriteMethodDeclaration(This.MockingCtor)) + { + } + } + + _writer.Line(); + if (This.ResourceDataCtor is not null) + { + _writer.WriteMethodDocumentation(This.ResourceDataCtor); + using (_writer.WriteMethodDeclaration(This.ResourceDataCtor)) + { + _writer.Line($"HasData = true;"); + _writer.Line($"_data = {This.DefaultResource!.ResourceDataParameter.Name};"); + } + } + + _writer.Line(); + if (This.ArmClientCtor is not null) + { + _writer.Line(); + _writer.WriteMethodDocumentation(This.ArmClientCtor); + using (_writer.WriteMethodDeclaration(This.ArmClientCtor)) + { + if (!This.IsInitializedByProperties) + { + foreach (var param in This.ExtraConstructorParameters) + { + _writer.Line($"_{param.Name} = {param.Name};"); + } + + foreach (var set in This.UniqueSets) + { + WriteRestClientConstructorPair(set.RestClient, set.Resource); + } + if (This.CanValidateResourceType) + WriteDebugValidate(); + } + } + } + _writer.Line(); + } + + private string GetEnumerableArgValue() + { + string value = string.Empty; + if (This is ResourceCollection collection) + { + if (collection.GetAllOperation?.IsPropertyBagOperation == true) + { + value = "options: null"; + } + } + return value; + } + + private void WriteIEnumerable(CSharpType type) + { + _writer.Line(); + var enumeratorType = new CSharpType(typeof(IEnumerator<>), type.Arguments); + _writer.Line($"{enumeratorType:I} {type:I}.GetEnumerator()"); + string argValue = GetEnumerableArgValue(); + using (_writer.Scope()) + { + _writer.Line($"return GetAll({argValue}).GetEnumerator();"); + } + _writer.Line(); + _writer.Line($"{typeof(IEnumerator)} {typeof(IEnumerable)}.GetEnumerator()"); + using (_writer.Scope()) + { + _writer.Line($"return GetAll({argValue}).GetEnumerator();"); + } + } + + private void WriteIAsyncEnumerable(CSharpType type) + { + _writer.Line(); + var enumeratorType = new CSharpType(typeof(IAsyncEnumerator<>), type.Arguments); + _writer.Line($"{enumeratorType:I} {type:I}.GetAsyncEnumerator({KnownParameters.CancellationTokenParameter.Type:I} {KnownParameters.CancellationTokenParameter.Name})"); + string argValue = GetEnumerableArgValue(); + using (_writer.Scope()) + { + _writer.Line($"return GetAllAsync({(argValue == string.Empty ? string.Empty : argValue + ", ")}{KnownParameters.CancellationTokenParameter.Name}: {KnownParameters.CancellationTokenParameter.Name}).GetAsyncEnumerator({KnownParameters.CancellationTokenParameter.Name});"); + } + } + + private void WriteEnumerables() + { + foreach (var type in This.EnumerableInterfaces) + { + if (type.Name.StartsWith("IEnumerable")) + WriteIEnumerable(type); + if (type.Name.StartsWith("IAsyncEnumerable")) + WriteIAsyncEnumerable(type); + } + } + + private void WriteRestClientConstructorPair(MgmtRestClient restClient, Resource? resource) + { + var resourceTypeExpression = ConstructResourceTypeExpression(resource); + var ctorString = ConstructClientDiagnostic(_writer, $"{GetProviderNamespaceFromReturnType(resourceTypeExpression)}", DiagnosticsProperty); + var diagFieldName = GetDiagnosticFieldName(restClient, resource); + _writer.Line($"{diagFieldName} = {ctorString};"); + FormattableString? apiVersionExpression = null; + if (resourceTypeExpression is not null) + { + string apiVersionVariable = GetApiVersionVariableName(restClient, resource); + _writer.Line($"TryGetApiVersion({resourceTypeExpression}, out string {apiVersionVariable});"); + apiVersionExpression = $"{apiVersionVariable}"; + } + _writer.Line($"{GetRestFieldName(restClient, resource)} = {GetRestConstructorString(restClient, apiVersionExpression)};"); + } + + protected FormattableString? ConstructResourceTypeExpression(Resource? resource) + { + if (resource != null) + return $"{resource.Type.Name}.ResourceType"; + return null; + } + + protected void WriteStaticValidate(FormattableString validResourceType) + { + using (_writer.Scope($"internal static void ValidateResourceId({typeof(Azure.Core.ResourceIdentifier)} id)")) + { + _writer.Line($"if (id.ResourceType != {validResourceType})"); + _writer.Line($"throw new {typeof(ArgumentException)}(string.Format({typeof(CultureInfo)}.CurrentCulture, \"Invalid resource type {{0}} expected {{1}}\", id.ResourceType, {validResourceType}), nameof(id));"); + } + } + + protected void WriteDebugValidate() + { + _writer.Line($"#if DEBUG"); + _writer.Line($"\t\t\tValidateResourceId(Id);"); + _writer.Line($"#endif"); + } + + protected void WriteFields() + { + foreach (var field in This.Fields) + { + _writer.WriteField(field); + } + _writer.Line(); + } + + protected FormattableString GetProviderNamespaceFromReturnType(FormattableString? resourceTypeExpression) + { + if (resourceTypeExpression is not null) + return $"{resourceTypeExpression}.Namespace"; + + return $"ProviderConstants.DefaultProviderNamespace"; + } + + protected FormattableString ConstructClientDiagnostic(CodeWriter writer, FormattableString providerNamespace, string diagnosticsOptionsVariable) + { + return $"new {typeof(ClientDiagnostics)}(\"{This.DiagnosticNamespace}\", {providerNamespace}, {diagnosticsOptionsVariable})"; + } + + protected FormattableString GetRestConstructorString(MgmtRestClient restClient, FormattableString? apiVersionExpression) + { + var paramList = new List() + { + $"{PipelineProperty}", + $"{DiagnosticsProperty}.ApplicationId" + }; + + if (restClient.Parameters.Any(p => p.Name.Equals("subscriptionId"))) + { + paramList.Add($"Id.SubscriptionId"); + } + paramList.Add($"{EndpointProperty}"); + if (apiVersionExpression != null) + { + paramList.Add(apiVersionExpression); + } + return $"new {restClient.Type}({paramList.Join(", ")})"; + } + + protected string GetRestClientName(MgmtRestOperation operation) => GetRestClientName(operation.RestClient, operation.Resource); + private string GetRestClientName(MgmtRestClient client, Resource? resource) + { + var names = This.GetRestDiagNames(new NameSetKey(client, resource)); + return UseField ? names.RestField : names.RestProperty; + } + + protected Reference GetDiagnosticReference(MgmtRestOperation operation) => new Reference(GetDiagnosticName(operation.RestClient, operation.Resource), typeof(ClientDiagnostics)); + private string GetDiagnosticName(MgmtRestClient client, Resource? resource) + { + var names = This.GetRestDiagNames(new NameSetKey(client, resource)); + return UseField ? names.DiagnosticField : names.DiagnosticProperty; + } + + protected string GetRestPropertyName(MgmtRestClient client, Resource? resource) => This.GetRestDiagNames(new NameSetKey(client, resource)).RestProperty; + protected string GetRestFieldName(MgmtRestClient client, Resource? resource) => This.GetRestDiagNames(new NameSetKey(client, resource)).RestField; + protected string GetDiagnosticsPropertyName(MgmtRestClient client, Resource? resource) => This.GetRestDiagNames(new NameSetKey(client, resource)).DiagnosticProperty; + protected string GetDiagnosticFieldName(MgmtRestClient client, Resource? resource) => This.GetRestDiagNames(new NameSetKey(client, resource)).DiagnosticField; + protected virtual string GetApiVersionVariableName(MgmtRestClient client, Resource? resource) => This.GetRestDiagNames(new NameSetKey(client, resource)).ApiVersionVariable; + + protected internal static string GetConfigureAwait(bool isAsync) + { + return isAsync ? ".ConfigureAwait(false)" : string.Empty; + } + + protected internal static string GetAsyncKeyword(bool isAsync) + { + return isAsync ? "async" : string.Empty; + } + + protected internal static string GetAwait(bool isAsync) + { + return isAsync ? "await " : string.Empty; + } + + protected internal static string GetNextLink(bool isNextPageFunc) + { + return isNextPageFunc ? "nextLink, " : string.Empty; + } + + protected FormattableString GetResourceTypeExpression(ResourceTypeSegment resourceType) + { + if (resourceType == ResourceTypeSegment.ResourceGroup) + return $"{typeof(ResourceGroupResource)}.ResourceType"; + if (resourceType == ResourceTypeSegment.Subscription) + return $"{typeof(SubscriptionResource)}.ResourceType"; + if (resourceType == ResourceTypeSegment.Tenant) + return $"{typeof(TenantResource)}.ResourceType"; + if (resourceType == ResourceTypeSegment.ManagementGroup) + return $"{typeof(ManagementGroupResource)}.ResourceType"; + + if (!resourceType.IsConstant) + throw new NotImplementedException($"ResourceType that contains variables are not supported yet"); + + // find the corresponding class of this resource type. If we find only one, use the constant inside that class. If we have multiple, use the hard-coded magic string + var candidates = MgmtContext.Library.ArmResources.Where(resource => resource.ResourceType == resourceType); + if (candidates.Count() == 1) + { + return $"{candidates.First().Type}.ResourceType"; + } + + if (string.IsNullOrEmpty(resourceType.SerializedType)) + { + throw new InvalidOperationException($"ResourceType is empty"); + } + + return $"\"{resourceType.SerializedType}\""; + } + + protected virtual void WriteMethod(MgmtClientOperation clientOperation, bool isAsync) + { + var writeBody = GetMethodDelegate(clientOperation); + using (WriteCommonMethod(clientOperation, isAsync)) + { + var diagnostic = new Diagnostic($"{This.Type.Name}.{clientOperation.Name}", Array.Empty()); + writeBody(clientOperation, diagnostic, isAsync); + } + } + + protected Dictionary _customMethods = new Dictionary(); + private WriteMethodDelegate GetMethodDelegate(MgmtClientOperation clientOperation) + { + if (!_customMethods.TryGetValue($"Write{clientOperation.Name}Body", out var function)) + { + function = GetMethodDelegate(clientOperation.IsLongRunningOperation, clientOperation.IsPagingOperation); + } + + return function; + } + + protected virtual WriteMethodDelegate GetMethodDelegate(bool isLongRunning, bool isPaging) + => (isLongRunning, isPaging) switch + { + (true, false) => WriteLROMethodBody, + (false, true) => WritePagingMethodBody, + (false, false) => WriteNormalMethodBody, + (true, true) => WritePagingLROMethodBody, + }; + + private void WritePagingLROMethodBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync) + { + throw new NotImplementedException($"Pageable LRO is not implemented yet, please use `remove-operation` directive to remove the following operationIds: {string.Join(", ", clientOperation.Select(o => o.OperationId))}"); + } + + protected virtual IDisposable WriteCommonMethod(MgmtClientOperation clientOperation, bool isAsync) + { + _writer.Line(); + var returnDescription = clientOperation.ReturnsDescription?.Invoke(isAsync); + return _writer.WriteCommonMethod(clientOperation.MethodSignature, returnDescription, isAsync, This.Accessibility == "public", SkipParameterValidation); + } + + #region PagingMethod + protected virtual void WritePagingMethodBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync) + { + // TODO -- since we are combining multiple operations under different parents, which description should we leave here + // TODO -- find a better way to get this type + var clientDiagField = GetDiagnosticReference(clientOperation.OperationMappings.First().Value); + // we need to write multiple branches for a paging method + if (clientOperation.OperationMappings.Count == 1) + { + // if we only have one branch, we would not need those if-else statements + var branch = clientOperation.OperationMappings.Keys.First(); + WritePagingMethodBranch(clientOperation.ReturnType, diagnostic, clientDiagField, clientOperation.OperationMappings[branch], clientOperation.ParameterMappings[branch], isAsync); + } + else + { + var keyword = "if"; + var escapeBranches = new List(); + foreach (var (branch, operation) in clientOperation.OperationMappings) + { + // we need to identify the correct branch using the resource type, therefore we need first to determine the resource type is a constant + var resourceType = This.GetBranchResourceType(branch); + if (!resourceType.IsConstant) + { + escapeBranches.Add(branch); + continue; + } + using (_writer.Scope($"{keyword} ({This.BranchIdVariableName}.ResourceType == {GetResourceTypeExpression(resourceType)})")) + { + WritePagingMethodBranch(clientOperation.ReturnType, diagnostic, clientDiagField, operation, clientOperation.ParameterMappings[branch], isAsync); + } + keyword = "else if"; + } + if (escapeBranches.Count == 0) + { + using (_writer.Scope($"else")) + { + _writer.Line($"throw new {typeof(InvalidOperationException)}($\"{{{This.BranchIdVariableName}.ResourceType}} is not supported here\");"); + } + } + else if (escapeBranches.Count == 1) + { + var branch = escapeBranches.First(); + using (_writer.Scope($"else")) + { + WritePagingMethodBranch(clientOperation.ReturnType, diagnostic, clientDiagField, clientOperation.OperationMappings[branch], clientOperation.ParameterMappings[branch], isAsync); + } + } + else + { + throw new InvalidOperationException($"It is impossible to identify which branch to go here using Id for request paths: [{string.Join(", ", escapeBranches)}]"); + } + } + } + + protected void WritePagingMethodBranch(CSharpType itemType, Diagnostic diagnostic, Reference clientDiagnosticsReference, MgmtRestOperation operation, IEnumerable parameterMappings, bool async) + { + var pagingMethod = operation.PagingMethod!; + var firstPageRequestArguments = GetArguments(_writer, parameterMappings); + var nextPageRequestArguments = firstPageRequestArguments.IsEmpty() ? $"{KnownParameters.NextLink.Name}" : $"{KnownParameters.NextLink.Name}, {firstPageRequestArguments}"; + + FormattableString firstPageRequest = $"{GetRestClientName(operation)}.Create{pagingMethod.Method.Name}Request({firstPageRequestArguments})"; + FormattableString? nextPageRequest = pagingMethod.NextPageMethod != null ? $"{GetRestClientName(operation)}.Create{pagingMethod.NextPageMethod.Name}Request({nextPageRequestArguments})" : (FormattableString?)null; + var pipelineReference = new Reference("Pipeline", Configuration.ApiTypes.HttpPipelineType); + var scopeName = diagnostic.ScopeName; + var itemName = pagingMethod.ItemName; + var nextLinkName = pagingMethod.NextLinkName; + + _writer.WritePageableBody(parameterMappings.Select(p => p.Parameter).Append(KnownParameters.CancellationTokenParameter).ToList(), itemType, firstPageRequest, nextPageRequest, clientDiagnosticsReference, pipelineReference, scopeName, itemName, nextLinkName, async); + } + + protected FormattableString CreateResourceIdentifierExpression(Resource resource, RequestPath requestPath, IEnumerable parameterMappings, FormattableString dataExpression) + { + var methodWithLeastParameters = resource.CreateResourceIdentifierMethod.Signature; + var cache = new List(parameterMappings); + + var parameterInvocations = new List(); + foreach (var reference in requestPath.Where(s => s.IsReference).Select(s => s.Reference)) + { + var match = cache.First(p => reference.Name.Equals(p.Parameter.Name, StringComparison.InvariantCultureIgnoreCase) && reference.Type.Equals(p.Parameter.Type)); + cache.Remove(match); + parameterInvocations.Add(match.IsPassThru ? $"{match.Parameter.Name}" : match.ValueExpression); + } + + if (parameterInvocations.Count < methodWithLeastParameters.Parameters.Count) + { + if (resource.ResourceData.GetTypeOfName() != null) + { + parameterInvocations.Add($"{dataExpression}.Name"); + } + else + { + throw new ErrorHelpers.ErrorException($"The resource data {resource.ResourceData.Type.Name} does not have a `Name` property, which is required when assigning non-resource as resources"); + } + } + + return $"{resource.Type.Name}.CreateResourceIdentifier({parameterInvocations.Join(", ")})"; + } + #endregion + + #region NormalMethod + protected virtual void WriteNormalMethodBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool async) + { + // we need to write multiple branches for a normal method + if (clientOperation.OperationMappings.Count == 1) + { + // if we only have one branch, we would not need those if-else statements + var branch = clientOperation.OperationMappings.Keys.First(); + WriteNormalMethodBranch(clientOperation.OperationMappings[branch], clientOperation.ParameterMappings[branch], diagnostic, async); + } + else + { + // branches go here + throw new NotImplementedException("multi-branch normal method not supported yet"); + } + } + + protected virtual void WriteNormalMethodBranch(MgmtRestOperation operation, IEnumerable parameterMappings, Diagnostic diagnostic, bool async) + { + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(operation))) + { + var response = new CodeWriterDeclaration(Configuration.ApiTypes.ResponseParameterName); + _writer + .Append($"var {response:D} = {GetAwait(async)} ") + .Append($"{GetRestClientName(operation)}.{CreateMethodName(operation.Method.Name, async)}("); + WriteArguments(_writer, parameterMappings); + _writer.Line($"cancellationToken){GetConfigureAwait(async)};"); + + if (operation.ThrowIfNull) + { + _writer + .Line($"if ({response}.Value == null)") + .Line($"throw new {Configuration.ApiTypes.RequestFailedExceptionType}({response}.{Configuration.ApiTypes.GetRawResponseName}());"); + } + var realReturnType = operation.MgmtReturnType; + if (realReturnType != null && realReturnType is { IsFrameworkType: false, Implementation: Resource resource } && resource.ResourceData.ShouldSetResourceIdentifier) + { + _writer.Line($"{response}.Value.Id = {CreateResourceIdentifierExpression(resource, operation.RequestPath, parameterMappings, $"{response}.Value")};"); + } + + // the case that we did not need to wrap the result + var valueConverter = operation.GetValueConverter($"{ArmClientReference}", $"{response}.Value"); + if (valueConverter != null) + { + _writer.Line($"return {Configuration.ApiTypes.ResponseType}.FromValue({valueConverter}, {response}.{Configuration.ApiTypes.GetRawResponseName}());"); + } + else + { + _writer.Line($"return {response};"); + } + } + } + #endregion + + #region LROMethod + protected virtual void WriteLROMethodBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool async) + { + // TODO -- since we are combining multiple operations under different parents, which description should we leave here? + // TODO -- find a way to properly get the LRO response type here. Temporarily we are using the first one + // TODO -- we need to write multiple branches for a LRO operation + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(clientOperation.OperationMappings.Values.First()))) + { + if (clientOperation.OperationMappings.Count == 1) + { + // if we only have one branch, we would not need those if-else statements + var branch = clientOperation.OperationMappings.Keys.First(); + WriteLROMethodBranch(clientOperation.OperationMappings[branch], clientOperation.ParameterMappings[branch], async); + } + else + { + var keyword = "if"; + var escapeBranches = new List(); + foreach ((var branch, var operation) in clientOperation.OperationMappings) + { + // we need to identify the correct branch using the resource type, therefore we need first to determine the resource type is a constant + var resourceType = This.GetBranchResourceType(branch); + if (!resourceType.IsConstant) + { + escapeBranches.Add(branch); + continue; + } + using (_writer.Scope($"{keyword} ({This.BranchIdVariableName}.ResourceType == {GetResourceTypeExpression(resourceType)})")) + { + WriteLROMethodBranch(operation, clientOperation.ParameterMappings[branch], async); + } + keyword = "else if"; + } + if (escapeBranches.Count == 0) + { + using (_writer.Scope($"else")) + { + _writer.Line($"throw new InvalidOperationException($\"{{{This.BranchIdVariableName}.ResourceType}} is not supported here\");"); + } + } + else if (escapeBranches.Count == 1) + { + var branch = escapeBranches.First(); + using (_writer.Scope($"else")) + { + WriteLROMethodBranch(clientOperation.OperationMappings[branch], clientOperation.ParameterMappings[branch], async); + } + } + else + { + throw new InvalidOperationException($"It is impossible to identify which branch to go here using Id for request paths: [{string.Join(", ", escapeBranches)}]"); + } + } + } + } + + protected virtual void WriteLROMethodBranch(MgmtRestOperation operation, IEnumerable parameterMapping, bool async) + { + _writer.Append($"var {Configuration.ApiTypes.ResponseParameterName} = {GetAwait(async)} {GetRestClientName(operation)}.{CreateMethodName(operation.Method.Name, async)}("); + WriteArguments(_writer, parameterMapping); + _writer.Line($"cancellationToken){GetConfigureAwait(async)};"); + if (operation.IsFakeLongRunningOperation) + { + _writer.Append($"var uri = "); + _writer.Append($"{GetRestClientName(operation)}.{RequestWriterHelpers.CreateRequestUriMethodName(operation.Method.Name)}("); + WriteArguments(_writer, parameterMapping); + _writer.RemoveTrailingComma(); + _writer.Line($");"); + + _writer.Append($"var rehydrationToken = {typeof(NextLinkOperationImplementation)}.GetRehydrationToken("); + + _writer.Append($"{typeof(RequestMethod)}.{new CultureInfo("en-US", false).TextInfo.ToTitleCase(operation.Method.Request.HttpMethod.ToString().ToLower())}, "); + _writer.Append($"uri.ToUri(), "); + _writer.Append($"uri.ToString(), "); + _writer.Append($"\"None\", "); + _writer.Append($"null, "); + _writer.Line($"{typeof(OperationFinalStateVia)}.OriginalUri.ToString());"); + } + WriteLROResponse(GetDiagnosticReference(operation).Name, PipelineProperty, operation, parameterMapping, async); + } + + protected virtual void WriteLROResponse(string diagnosticsVariableName, string pipelineVariableName, MgmtRestOperation operation, IEnumerable parameterMapping, bool isAsync) + { + _writer.Append($"var operation = new {LibraryArmOperation}"); + if (operation.ReturnType.IsGenericType) + { + _writer.Append($"<{operation.MgmtReturnType}>"); + } + _writer.Append($"("); + if (operation.IsFakeLongRunningOperation) + { + var valueConverter = operation.GetValueConverter($"{ArmClientReference}", $"{Configuration.ApiTypes.ResponseParameterName}"); + if (valueConverter != null) + { + _writer.Append($"{Configuration.ApiTypes.ResponseType}.FromValue({valueConverter}, {Configuration.ApiTypes.ResponseParameterName}.{Configuration.ApiTypes.GetRawResponseName}())"); + } + else + { + _writer.Append($"{Configuration.ApiTypes.ResponseParameterName}"); + } + _writer.Append($", rehydrationToken"); + } + else + { + if (operation.OperationSource is not null) + { + _writer.Append($"new {operation.OperationSource.Type}(") + .AppendIf($"{ArmClientReference}", operation.MgmtReturnType is { IsFrameworkType: false, Implementation: Resource }) + .Append($"), "); + } + + _writer.Append($"{diagnosticsVariableName}, {pipelineVariableName}, {GetRestClientName(operation)}.{RequestWriterHelpers.CreateRequestMethodName(operation.Method.Name)}("); + WriteArguments(_writer, parameterMapping); + _writer.RemoveTrailingComma(); + _writer.Append($").Request, {Configuration.ApiTypes.ResponseParameterName}, {typeof(OperationFinalStateVia)}.{operation.FinalStateVia!},"); + + if (Configuration.MgmtConfiguration.OperationsToSkipLroApiVersionOverride.Contains(operation.OperationId)) + { + _writer.AppendRaw("skipApiVersionOverride: true,"); + } + + if (Configuration.MgmtConfiguration.OperationsToLroApiVersionOverride.TryGetValue(operation.OperationId, out var apiVersionOverrideValue)) + { + _writer.Append($"apiVersionOverrideValue: {apiVersionOverrideValue:L}"); + } + } + _writer.RemoveTrailingComma(); + _writer.Line($");"); + var waitForCompletionMethod = operation.MgmtReturnType is null ? + "WaitForCompletionResponse" : + "WaitForCompletion"; + _writer.Line($"if (waitUntil == {typeof(WaitUntil)}.Completed)"); + _writer.Line($"{GetAwait(isAsync)} operation.{CreateMethodName(waitForCompletionMethod, isAsync)}(cancellationToken){GetConfigureAwait(isAsync)};"); + _writer.Line($"return operation;"); + } + #endregion + + protected void WriteArguments(CodeWriter writer, IEnumerable mapping, bool passNullForOptionalParameters = false) + { + var arguments = GetArguments(writer, mapping, passNullForOptionalParameters); + if (!arguments.IsEmpty()) + { + writer.Append(arguments).AppendRaw(", "); + } + } + + private static FormattableString GetArguments(CodeWriter writer, IEnumerable mapping, bool passNullForOptionalParameters = false) + { + var args = new List(); + foreach (var parameter in mapping) + { + if (parameter.IsPassThru) + { + if (PagingMethod.IsPageSizeName(parameter.Parameter.Name)) + { + // always use the `pageSizeHint` parameter from `AsPages(pageSizeHint)` + if (PagingMethod.IsPageSizeType(parameter.Parameter.Type.FrameworkType)) + { + args.Add($"pageSizeHint"); + } + else + { + Console.Error.WriteLine($"WARNING: Parameter '{parameter.Parameter.Name}' is like a page size parameter, but it's not a numeric type. Fix it or overwrite it if necessary."); + if (parameter.Parameter.IsPropertyBag) + args.Add($"{parameter.ValueExpression}"); + else + args.Add($"{parameter.Parameter.Name}"); + } + } + else + { + if (passNullForOptionalParameters && parameter.Parameter.Validation == ValidationType.None) + args.Add($"null"); + else if (parameter.Parameter.IsPropertyBag) + args.Add($"{parameter.ValueExpression}"); + else + args.Add($"{parameter.Parameter.Name}"); + } + } + else + { + if (parameter.Parameter.Type.IsEnum) + { + writer.UseNamespace(parameter.Parameter.Type.Namespace); + } + + foreach (var @namespace in parameter.Usings) + { + writer.UseNamespace(@namespace); + } + + args.Add($"{parameter.ValueExpression}"); + } + } + + return args.Join(", "); + } + + public override string ToString() + { + return _writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MgmtExtensionWrapperWriter.cs b/logger/autorest.csharp/mgmt/Generation/MgmtExtensionWrapperWriter.cs new file mode 100644 index 0000000..f299f80 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MgmtExtensionWrapperWriter.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Output; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class MgmtExtensionWrapperWriter : MgmtClientBaseWriter + { + private MgmtExtensionWrapper This { get; } + + public MgmtExtensionWrapperWriter(MgmtExtensionWrapper extensionWrapper) : base(new CodeWriter(), extensionWrapper) + { + This = extensionWrapper; + } + + protected override void WritePrivateHelpers() + { + foreach (var extension in This.Extensions) + { + if (extension.IsEmpty) + continue; + + _writer.Line(); + + _writer.WriteMethod(extension.MockingExtensionFactoryMethod); + } + + base.WritePrivateHelpers(); + } + + protected internal override void WriteImplementations() + { + WritePrivateHelpers(); + + foreach (var extension in This.Extensions) + { + if (extension.IsEmpty) + continue; + + MgmtExtensionWriter.GetWriter(_writer, extension).WriteImplementations(); + _writer.Line(); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MgmtExtensionWriter.cs b/logger/autorest.csharp/mgmt/Generation/MgmtExtensionWriter.cs new file mode 100644 index 0000000..54f7693 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MgmtExtensionWriter.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class MgmtExtensionWriter : MgmtClientBaseWriter + { + public static MgmtExtensionWriter GetWriter(MgmtExtension extension) => GetWriter(new CodeWriter(), extension); + + public static MgmtExtensionWriter GetWriter(CodeWriter writer, MgmtExtension extension) => extension switch + { + ArmClientExtension armClientExtension => new ArmClientExtensionWriter(writer, armClientExtension), + _ => new MgmtExtensionWriter(writer, extension) + }; + + protected override bool SkipParameterValidation => true; + + private MgmtExtension This { get; } + protected delegate void WriteResourceGetBody(MethodSignature signature, bool isAsync, bool isPaging); + + public MgmtExtensionWriter(MgmtExtension extensions) : this(new CodeWriter(), extensions) + { + This = extensions; + } + + public MgmtExtensionWriter(CodeWriter writer, MgmtExtension extensions) : base(writer, extensions) + { + This = extensions; + } + + protected override WriteMethodDelegate GetMethodDelegate(bool isLongRunning, bool isPaging) + => IsArmCore ? base.GetMethodDelegate(isLongRunning, isPaging) : GetMethodWrapperImpl; + + private void GetMethodWrapperImpl(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync) + => WriteMethodBodyWrapper(clientOperation.MethodSignature, isAsync, clientOperation.IsPagingOperation); + + protected void WriteMethodBodyWrapper(MethodSignature signature, bool isAsync, bool isPaging) + { + _writer.WriteParametersValidation(signature.Parameters.Take(1)); + + _writer.AppendRaw("return ") + .AppendRawIf("await ", isAsync && !isPaging) + .Append($"{This.MockableExtension.FactoryMethodName}({This.ExtensionParameter.Name}).{CreateMethodName(signature.Name, isAsync)}("); + + foreach (var parameter in signature.Parameters.Skip(1)) + { + _writer.Append($"{parameter.Name},"); + } + + _writer.RemoveTrailingComma(); + _writer.AppendRaw(")") + .AppendRawIf(".ConfigureAwait(false)", isAsync && !isPaging) + .LineRaw(";"); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MgmtLongRunningOperationWriter.cs b/logger/autorest.csharp/mgmt/Generation/MgmtLongRunningOperationWriter.cs new file mode 100644 index 0000000..92ab249 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MgmtLongRunningOperationWriter.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.ResourceManager; +using Request = Azure.Core.Request; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class MgmtLongRunningOperationWriter + { + private readonly CodeWriter _writer; + private readonly string _name; + private readonly string _genericString; + private readonly bool _isGeneric; + private readonly string _waitMethod; + private readonly Type _operationType; + private readonly Type _responseType; + private readonly Type _operationInternalType; + private readonly FormattableString _operationSourceString; + private readonly string _responseString; + private readonly string _sourceString; + + public string Filename { get; } + + public MgmtLongRunningOperationWriter(bool isGeneric) + { + _writer = new CodeWriter(); + _isGeneric = isGeneric; + _name = $"{MgmtContext.Context.DefaultNamespace.Split('.').Last()}ArmOperation"; + _genericString = isGeneric ? "" : string.Empty; + Filename = isGeneric ? $"LongRunningOperation/{_name}OfT.cs" : $"LongRunningOperation/{_name}.cs"; + _waitMethod = isGeneric ? "WaitForCompletion" : "WaitForCompletionResponse"; + _operationType = isGeneric ? typeof(ArmOperation<>) : typeof(ArmOperation); + _responseType = isGeneric ? Configuration.ApiTypes.ResponseOfTType : Configuration.ApiTypes.ResponseType; + _operationInternalType = isGeneric ? typeof(OperationInternal<>) : typeof(OperationInternal); + _operationSourceString = isGeneric ? (FormattableString)$"{typeof(IOperationSource<>)} source, " : (FormattableString)$""; + _responseString = isGeneric ? $"{Configuration.ApiTypes.ResponseParameterName}.{Configuration.ApiTypes.GetRawResponseName}(), {Configuration.ApiTypes.ResponseParameterName}.Value" : $"{Configuration.ApiTypes.ResponseParameterName}"; + _sourceString = isGeneric ? "source, " : string.Empty; + } + + public void Write() + { + using (_writer.Namespace(MgmtContext.Context.DefaultNamespace)) + { + _writer.Line($"#pragma warning disable SA1649 // File name should match first type name"); + _writer.Line($"internal class {_name}{_genericString} : {_operationType}"); + _writer.Line($"#pragma warning restore SA1649 // File name should match first type name"); + using (_writer.Scope()) + { + _writer.Line($"private readonly {_operationInternalType} _operation;"); + _writer.Line($"private readonly {typeof(RehydrationToken?)} _completeRehydrationToken;"); + _writer.Line($"private readonly {typeof(NextLinkOperationImplementation)}? _nextLinkOperation;"); + _writer.Line($"private readonly {typeof(string)} _operationId;"); + _writer.Line(); + + _writer.WriteXmlDocumentationSummary($"Initializes a new instance of {_name} for mocking."); + using (_writer.Scope($"protected {_name}()")) + { + } + _writer.Line(); + + using (_writer.Scope($"internal {_name}({_responseType} {Configuration.ApiTypes.ResponseParameterName}, {typeof(RehydrationToken?)} rehydrationToken = null)")) + { + _writer.Line($"_operation = {_operationInternalType}.Succeeded({_responseString});"); + _writer.Line($"_completeRehydrationToken = rehydrationToken;"); + _writer.Line($"_operationId = GetOperationId(rehydrationToken);"); + } + _writer.Line(); + + using (_writer.Scope($"internal {_name}({_operationSourceString}{typeof(ClientDiagnostics)} clientDiagnostics, {Configuration.ApiTypes.HttpPipelineType} pipeline, {typeof(Request)} request, {Configuration.ApiTypes.ResponseType} {Configuration.ApiTypes.ResponseParameterName}, {typeof(OperationFinalStateVia)} finalStateVia, bool skipApiVersionOverride = false, string apiVersionOverrideValue = null)")) + { + var nextLinkOperation = new CodeWriterDeclaration("nextLinkOperation"); + _writer.Line($"var {nextLinkOperation:D} = {typeof(NextLinkOperationImplementation)}.{nameof(NextLinkOperationImplementation.Create)}(pipeline, request.Method, request.Uri.ToUri(), {Configuration.ApiTypes.ResponseParameterName}, finalStateVia, skipApiVersionOverride, apiVersionOverrideValue);"); + using (_writer.Scope($"if (nextLinkOperation is NextLinkOperationImplementation nextLinkOperationValue)")) + { + // If nextLinkOperation is NextLinkOperationImplementation, this implies that the operation is not complete + // we need to store the nextLinkOperation to get lateset rehydration token + _writer.Line($"_nextLinkOperation = nextLinkOperationValue;"); + _writer.Line($"_operationId = _nextLinkOperation.OperationId;"); + } + using (_writer.Scope($"else")) + { + // This implies the operation is complete and we can cache the rehydration token since it won't change anymore + _writer.Line($"_completeRehydrationToken = {typeof(NextLinkOperationImplementation)}.{nameof(NextLinkOperationImplementation.GetRehydrationToken)}(request.Method, request.Uri.ToUri(), {Configuration.ApiTypes.ResponseParameterName}, finalStateVia);"); + _writer.Line($"_operationId = GetOperationId(_completeRehydrationToken);"); + } + if (_isGeneric) + { + _writer.Line($"_operation = new {_operationInternalType}({typeof(NextLinkOperationImplementation)}.{nameof(NextLinkOperationImplementation.Create)}({_sourceString}nextLinkOperation), clientDiagnostics, {Configuration.ApiTypes.ResponseParameterName}, {_name:L}, fallbackStrategy: new {typeof(SequentialDelayStrategy)}());"); + } + else + { + _writer.Line($"_operation = new {_operationInternalType}({nextLinkOperation}, clientDiagnostics, {Configuration.ApiTypes.ResponseParameterName}, {_name:L}, fallbackStrategy: new {typeof(SequentialDelayStrategy)}());"); + } + } + _writer.Line(); + + using (_writer.Scope($"private string GetOperationId(RehydrationToken? rehydrationToken)")) + { + using (_writer.Scope($"if (rehydrationToken is null)")) + { + _writer.Line($"return null;"); + } + _writer.Line($"var lroDetails = {typeof(ModelReaderWriter)}.{nameof(ModelReaderWriter.Write)}(rehydrationToken, ModelReaderWriterOptions.Json).ToObjectFromJson<{typeof(Dictionary)}>();"); + _writer.Line($"return lroDetails[\"id\"];"); + } + + _writer.WriteXmlDocumentationInheritDoc(); + _writer + .LineRaw("public override string Id => _operationId ?? NextLinkOperationImplementation.NotSet;") + .Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer + .LineRaw("public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation?.GetRehydrationToken() ?? _completeRehydrationToken;") + .Line(); + + if (_isGeneric) + { + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override T Value => _operation.Value;"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override bool HasValue => _operation.HasValue;"); + _writer.Line(); + } + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override bool HasCompleted => _operation.HasCompleted;"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {Configuration.ApiTypes.ResponseType} {Configuration.ApiTypes.GetRawResponseName}() => _operation.RawResponse;"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {Configuration.ApiTypes.ResponseType} UpdateStatus({typeof(CancellationToken)} cancellationToken = default) => _operation.UpdateStatus(cancellationToken);"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {typeof(ValueTask<>).MakeGenericType(Configuration.ApiTypes.ResponseType)} UpdateStatusAsync({typeof(CancellationToken)} cancellationToken = default) => _operation.UpdateStatusAsync(cancellationToken);"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {_responseType} {_waitMethod}({typeof(CancellationToken)} cancellationToken = default) => _operation.{_waitMethod}(cancellationToken);"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {_responseType} {_waitMethod}({typeof(TimeSpan)} pollingInterval, {typeof(CancellationToken)} cancellationToken = default) => _operation.{_waitMethod}(pollingInterval, cancellationToken);"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {typeof(ValueTask<>).MakeGenericType(_responseType)} {_waitMethod}Async({typeof(CancellationToken)} cancellationToken = default) => _operation.{_waitMethod}Async(cancellationToken);"); + _writer.Line(); + + _writer.WriteXmlDocumentationInheritDoc(); + _writer.Line($"public override {typeof(ValueTask<>).MakeGenericType(_responseType)} {_waitMethod}Async({typeof(TimeSpan)} pollingInterval, {typeof(CancellationToken)} cancellationToken = default) => _operation.{_waitMethod}Async(pollingInterval, cancellationToken);"); + _writer.Line(); + } + } + } + + public override string ToString() + { + return _writer.ToString(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MgmtMockableExtensionWriter.cs b/logger/autorest.csharp/mgmt/Generation/MgmtMockableExtensionWriter.cs new file mode 100644 index 0000000..01c1d51 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MgmtMockableExtensionWriter.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Output; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class MgmtMockableExtensionWriter : MgmtClientBaseWriter + { + public static MgmtMockableExtensionWriter GetWriter(MgmtMockableExtension extensionClient) => extensionClient switch + { + MgmtMockableArmClient armClientExtensionClient => new MockableArmClientWriter(armClientExtensionClient), + _ => new MgmtMockableExtensionWriter(extensionClient) + }; + + protected override bool UseField => false; + + private MgmtMockableExtension This { get; } + + public MgmtMockableExtensionWriter(MgmtMockableExtension extensions) : base(new CodeWriter(), extensions) + { + This = extensions; + } + + protected override void WritePrivateHelpers() + { + _writer.Line(); + using (_writer.Scope($"private string GetApiVersionOrNull({typeof(ResourceType)} resourceType)")) + { + _writer.Line($"TryGetApiVersion(resourceType, out string apiVersion);"); + _writer.Line($"return apiVersion;"); + } + } + + protected override void WriteProperties() + { + _writer.Line(); + foreach (var set in This.UniqueSets) + { + WriterPropertySet(set.RestClient, set.Resource); + } + } + + private void WriterPropertySet(MgmtRestClient client, Resource? resource) + { + var resourceTypeExpression = ConstructResourceTypeExpression(resource); + var diagPropertyName = GetDiagnosticsPropertyName(client, resource); + FormattableString diagOptionsCtor = ConstructClientDiagnostic(_writer, GetProviderNamespaceFromReturnType(resourceTypeExpression), DiagnosticsProperty); + _writer.Line($"private {typeof(ClientDiagnostics)} {diagPropertyName} => {GetDiagnosticFieldName(client, resource)} ??= {diagOptionsCtor};"); + var apiVersionExpression = resourceTypeExpression == null ? null : (FormattableString)$"GetApiVersionOrNull({resourceTypeExpression})"; + var restCtor = GetRestConstructorString(client, apiVersionExpression); + _writer.Line($"private {client.Type} {GetRestPropertyName(client, resource)} => {GetRestFieldName(client, resource)} ??= {restCtor};"); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MgmtRestClientWriter.cs b/logger/autorest.csharp/mgmt/Generation/MgmtRestClientWriter.cs new file mode 100644 index 0000000..0cfb403 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MgmtRestClientWriter.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class MgmtRestClientWriter + { + private const string UserAgentVariable = "userAgent"; + private const string UserAgentField = "_" + UserAgentVariable; + + public void WriteClient(CodeWriter writer, MgmtRestClient restClient) + { + using (writer.Namespace(restClient.Type.Namespace)) + { + using (writer.Scope($"{restClient.Declaration.Accessibility} partial class {restClient.Type:D}")) + { + WriteClientFields(writer, restClient); + WriteClientCtor(writer, restClient); + + foreach (var method in restClient.Methods.Select(m => m.Method)) + { + RequestWriterHelpers.WriteRequestAndUriCreation(writer, method, "internal", restClient.Fields, null, true, restClient.Parameters); + WriteOperation(writer, restClient, method, true); + WriteOperation(writer, restClient, method, false); + } + } + } + } + + protected void WriteClientFields(CodeWriter writer, MgmtRestClient restClient) + { + writer.Line($"private readonly {typeof(TelemetryDetails)} {UserAgentField};"); + writer.WriteFieldDeclarations(restClient.Fields); + } + + private static void WriteClientCtor(CodeWriter writer, MgmtRestClient restClient) + { + var constructorParameters = restClient.Parameters; + var constructor = new ConstructorSignature(restClient.Type, null, $"Initializes a new instance of {restClient.Type.Name}", MethodSignatureModifiers.Public, restClient.Parameters); + + writer.WriteMethodDocumentation(constructor); + using (writer.WriteMethodDeclaration(constructor)) + { + foreach (Parameter clientParameter in constructorParameters) + { + var field = restClient.Fields.GetFieldByParameter(clientParameter); + if (field != null) + { + writer.WriteVariableAssignmentWithNullCheck($"{field.Name}", clientParameter); + } + } + writer.Line($"{UserAgentField} = new {typeof(TelemetryDetails)}(GetType().Assembly, {MgmtRestClient.ApplicationIdParameter.Name});"); + } + writer.Line(); + } + + private static void WriteOperation(CodeWriter writer, MgmtRestClient restClient, RestClientMethod operation, bool async) + { + var returnType = operation.ReturnType != null + ? new CSharpType(Configuration.ApiTypes.ResponseOfTType, operation.ReturnType) + : new CSharpType(Configuration.ApiTypes.ResponseType); + + var parameters = operation.Parameters.Append(KnownParameters.CancellationTokenParameter).ToArray(); + var method = new MethodSignature(operation.Name, $"{operation.Summary}", $"{operation.Description}", MethodSignatureModifiers.Public, returnType, null, parameters).WithAsync(async); + + writer + .WriteXmlDocumentationSummary($"{method.Description}") + .WriteMethodDocumentationSignature(method); + + using (writer.WriteMethodDeclaration(method)) + { + writer.WriteParametersValidation(parameters); + var messageVariable = new CodeWriterDeclaration("message"); + var requestMethodName = RequestWriterHelpers.CreateRequestMethodName(operation.Name); + + writer + .Line($"using var {messageVariable:D} = {requestMethodName}({operation.Parameters.GetIdentifiersFormattable()});") + .WriteMethodCall(async, $"{restClient.Fields.PipelineField.Name}.SendAsync", $"{restClient.Fields.PipelineField.Name}.Send", $"{messageVariable}, {KnownParameters.CancellationTokenParameter.Name}"); + + ResponseWriterHelpers.WriteStatusCodeSwitch(writer, messageVariable.ActualName, operation, async, null); + } + writer.Line(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/MockableArmClientWriter.cs b/logger/autorest.csharp/mgmt/Generation/MockableArmClientWriter.cs new file mode 100644 index 0000000..dd0b287 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/MockableArmClientWriter.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal sealed class MockableArmClientWriter : MgmtMockableExtensionWriter + { + private readonly Parameter _scopeParameter; + private MgmtMockableArmClient This { get; } + + public MockableArmClientWriter(MgmtMockableArmClient extensionClient) : base(extensionClient) + { + This = extensionClient; + _scopeParameter = new Parameter( + Name: "scope", + Description: $"The scope that the resource will apply against.", + Type: typeof(ResourceIdentifier), + DefaultValue: null, + Validation: ValidationType.AssertNotNull, + Initializer: null); + } + + protected override void WriteCtors() + { + base.WriteCtors(); + + if (This.ArmClientCtor is { } armClientCtor) + { + // for ArmClientExtensionClient, we write an extra ctor that only takes ArmClient as parameter + var ctor = armClientCtor with + { + Parameters = new[] { KnownParameters.ArmClient }, + Initializer = new(false, new ValueExpression[] { KnownParameters.ArmClient, ResourceIdentifierExpression.Root }) + }; + + using (_writer.WriteMethodDeclaration(ctor)) + { + // it does not need a body + } + } + } + + protected internal override void WriteImplementations() + { + base.WriteImplementations(); + + foreach (var method in This.ArmResourceMethods) + { + _writer.WriteMethodDocumentation(method.Signature); + _writer.WriteMethod(method); + } + } + + protected override IDisposable WriteCommonMethod(MgmtClientOperation clientOperation, bool isAsync) + { + var originalSignature = clientOperation.MethodSignature; + var signature = originalSignature with + { + Parameters = originalSignature.Parameters.Prepend(_scopeParameter).ToArray() + }; + _writer.Line(); + var returnDescription = clientOperation.ReturnsDescription?.Invoke(isAsync); + return _writer.WriteCommonMethod(signature, returnDescription, isAsync, This.Accessibility == "public", SkipParameterValidation); + } + + protected override WriteMethodDelegate GetMethodDelegate(bool isLongRunning, bool isPaging) + { + var writeBody = base.GetMethodDelegate(isLongRunning, isPaging); + return (clientOperation, diagnostic, isAsync) => + { + var requestPaths = clientOperation.Select(restOperation => restOperation.RequestPath); + var scopeResourceTypes = requestPaths.Select(requestPath => requestPath.GetParameterizedScopeResourceTypes() ?? Enumerable.Empty()).SelectMany(types => types).Distinct(); + var scopeTypes = ResourceTypeBuilder.GetScopeTypeStrings(scopeResourceTypes); + + WriteScopeResourceTypesValidation(_scopeParameter.Name, scopeTypes); + + writeBody(clientOperation, diagnostic, isAsync); + }; + } + + private void WriteScopeResourceTypesValidation(string parameterName, ICollection? types) + { + if (types == null) + return; + // validate the scope types + var typeAssertions = types.Select(type => (FormattableString)$"!{parameterName:I}.ResourceType.Equals(\"{type}\")").ToArray(); + var assertion = typeAssertions.Join(" || "); + using (_writer.Scope($"if ({assertion})")) + { + _writer.Line($"throw new {typeof(ArgumentException)}({typeof(string)}.{nameof(string.Format)}(\"Invalid resource type {{0}} expected {types.Join(", ", " or ")}\", {parameterName:I}.ResourceType));"); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/OperationSourceWriter.cs b/logger/autorest.csharp/mgmt/Generation/OperationSourceWriter.cs new file mode 100644 index 0000000..35f5b10 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/OperationSourceWriter.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Builders; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class OperationSourceWriter + { + private readonly OperationSource _opSource; + private readonly CodeWriter _writer; + private readonly IReadOnlyDictionary? _operationIdMappings; + + public OperationSourceWriter(OperationSource opSource) + { + _writer = new CodeWriter(); + _opSource = opSource; + if (_opSource.Resource is not null && Configuration.MgmtConfiguration.OperationIdMappings.TryGetValue(_opSource.Resource.ResourceName, out var mappings)) + _operationIdMappings = mappings; + } + + public void Write() + { + using (_writer.Namespace($"{_opSource.Declaration.Namespace}")) + { + using (_writer.Scope($"{_opSource.Declaration.Accessibility} class {_opSource.Type:D} : {_opSource.Interface}")) + { + if (_opSource.IsReturningResource) + { + _writer.WriteField(_opSource.ArmClientField); + + if (_operationIdMappings is not null) + { + using (_writer.Scope($"private readonly {typeof(Dictionary)} _idMappings = new {typeof(Dictionary)}()", start: "\t\t{", end: "\t\t};")) + { + _writer.Line($"\t\t\t{{ \"subscriptionId\", \"Microsoft.Resources/subscriptions\" }},"); + _writer.Line($"\t\t\t{{ \"resourceGroupName\", \"Microsoft.Resources/resourceGroups\" }},"); + foreach (var mapping in _operationIdMappings) + { + _writer.Line($"\t\t\t{{ \"{mapping.Key}\", \"{mapping.Value}\" }},"); + } + } + } + + _writer.Line(); + using (_writer.WriteMethodDeclaration(_opSource.ArmClientCtor)) + { + _writer.Line($"{_opSource.ArmClientField.Name} = {KnownParameters.ArmClient.Name};"); + } + } + + _writer.Line(); + WriteCreateResult(); + + _writer.Line(); + WriteCreateResultAsync(); + + if (_operationIdMappings is not null) + { + var resource = _opSource.Resource!; + var resourceType = resource.Type; + var dataType = resource.ResourceData.Type; + _writer.Line(); + using (_writer.Scope($"private {dataType} ScrubId({dataType} data)")) + { + _writer.Line($"if (data.Id.ResourceType == {resourceType}.ResourceType)"); + _writer.Line($"return data;"); + _writer.Line(); + _writer.Append($"var newId = {resourceType}.CreateResourceIdentifier("); + var createIdMethod = resource.CreateResourceIdentifierMethod.Signature; + foreach (var param in createIdMethod.Parameters) + { + _writer.Line(); + _writer.Append($"\tGetName(\"{param.Name}\", data.Id),"); + } + _writer.RemoveTrailingComma(); + _writer.Line($");"); + _writer.Line(); + _writer.Line($"return new {dataType}("); + _writer.Line($"\tnewId,"); + _writer.Line($"\tnewId.Name,"); + _writer.Append($"\tnewId.ResourceType,"); + foreach (var param in resource.ResourceData.SerializationConstructor.Signature.Parameters.Skip(3)) + { + _writer.Line(); + if (param.IsRawData) + { + _writer.Append($"\tnull"); + } + else + { + _writer.Append($"\tdata.{param.Name.ToCleanName()},"); + } + } + _writer.RemoveTrailingComma(); + _writer.Line($");"); + } + + _writer.Line(); + using (_writer.Scope($"private string GetName(string param, {typeof(ResourceIdentifier)} id)")) + { + _writer.Line($"while (id.ResourceType != _idMappings[param])"); + _writer.Line($"id = id.Parent;"); + _writer.Line($"return id.Name;"); + } + } + } + } + } + + public override string ToString() + { + return _writer.ToString(); + } + + private void WriteCreateResult() + { + var responseVariable = new VariableReference(Configuration.ApiTypes.ResponseType, $"{Configuration.ApiTypes.ResponseParameterName}"); + using (_writer.Scope($"{_opSource.ReturnType} {_opSource.Interface}.CreateResult({Configuration.ApiTypes.ResponseType} {responseVariable.Declaration:D}, {typeof(CancellationToken)} cancellationToken)")) + { + BuildCreateResultBody(new ResponseExpression(responseVariable).ContentStream, false).AsStatement().Write(_writer); + } + } + + private void WriteCreateResultAsync() + { + var responseVariable = new VariableReference(Configuration.ApiTypes.ResponseType, $"{Configuration.ApiTypes.ResponseParameterName}"); + using (_writer.Scope($"async {new CSharpType(typeof(ValueTask<>), _opSource.ReturnType)} {_opSource.Interface}.CreateResultAsync({Configuration.ApiTypes.ResponseType} {responseVariable.Declaration:D}, {typeof(CancellationToken)} cancellationToken)")) + { + BuildCreateResultBody(new ResponseExpression(responseVariable).ContentStream, true).AsStatement().Write(_writer); + } + } + + private IEnumerable BuildCreateResultBody(StreamExpression stream, bool async) + { + if (_opSource.IsReturningResource) + { + var resourceData = _opSource.Resource!.ResourceData; + + yield return UsingVar("document", JsonDocumentExpression.Parse(stream, async), out var document); + + ValueExpression deserializeExpression = SerializableObjectTypeExpression.Deserialize(resourceData, document.RootElement); + if (_operationIdMappings is not null) + { + deserializeExpression = new InvokeInstanceMethodExpression(null, "ScrubId", new[]{deserializeExpression}, null, false); + } + + var dataVariable = new VariableReference(resourceData.Type, "data"); + + yield return Var(dataVariable, deserializeExpression); + if (resourceData.ShouldSetResourceIdentifier) + { + yield return Assign(new MemberExpression(dataVariable, "Id"), new MemberExpression(_opSource.ArmClientField, "Id")); + } + yield return Return(New.Instance(_opSource.Resource.Type, (ValueExpression)_opSource.ArmClientField, dataVariable)); + } + else + { + yield return JsonSerializationMethodsBuilder.BuildDeserializationForMethods(_opSource.ResponseSerialization, async, null, stream, _opSource.ReturnType.Equals(typeof(BinaryData)), null); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/PartialResourceWriter.cs b/logger/autorest.csharp/mgmt/Generation/PartialResourceWriter.cs new file mode 100644 index 0000000..fffa768 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/PartialResourceWriter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Output; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Generation; + +internal class PartialResourceWriter : ResourceWriter +{ + private PartialResource This { get; } + + internal PartialResourceWriter(PartialResource resource) : this(new CodeWriter(), resource) + { + } + + protected PartialResourceWriter(CodeWriter writer, PartialResource resource) : base(writer, resource) + { + This = resource; + } + + protected override void WriteProperties() + { + _writer.WriteXmlDocumentationSummary($"Gets the resource type for the operations"); + + _writer.Line($"public static readonly {typeof(ResourceType)} ResourceType = \"{This.ResourceType}\";"); + _writer.Line(); + + // comparing with the `ResourceWriter`, `PartialResourceWriter` does not write the `public XXXData { get; }` property because partial resources do not have resource data. + + _writer.Line(); + WriteStaticValidate($"ResourceType"); + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/ReferenceTypeWriter.cs b/logger/autorest.csharp/mgmt/Generation/ReferenceTypeWriter.cs new file mode 100644 index 0000000..2fc7c16 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/ReferenceTypeWriter.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class ReferenceTypeWriter : ModelWriter + { + protected override void AddClassAttributes(CodeWriter writer, ObjectType objectType) + { + if (objectType is not SchemaObjectType schema) + return; + + if (MgmtReferenceType.IsPropertyReferenceType(schema.InputModel)) + { + if (Configuration.IsBranded) + { + writer.UseNamespace("Azure.Core"); + } + writer.Line($"[{ReferenceClassFinder.PropertyReferenceTypeAttribute}]"); + } + else if (MgmtReferenceType.IsTypeReferenceType(schema.InputModel)) + { + if (Configuration.IsBranded) + { + writer.UseNamespace("Azure.Core"); + } + writer.Line($"[{ReferenceClassFinder.TypeReferenceTypeAttribute}]"); + } + else if (MgmtReferenceType.IsReferenceType(schema.InputModel)) + { + if (Configuration.IsBranded) + { + writer.UseNamespace("Azure.Core"); + } + + // The hard-coded string input is needed for ReferenceTypeAttribute to work, and this only applies to ResourceData and TrackedResourceData now. + writer.Line($"[{ReferenceClassFinder.ReferenceTypeAttribute}(new string[]{{\"SystemData\"}})]"); + } + } + + protected override void AddCtorAttribute(CodeWriter writer, ObjectType schema, ObjectTypeConstructor constructor) + { + if (constructor == schema.InitializationConstructor) + { + writer.Line($"[{ReferenceClassFinder.InitializationCtorAttribute}]"); + } + else if (constructor == schema.SerializationConstructor) + { + writer.Line($"[{ReferenceClassFinder.SerializationCtorAttribute}]"); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/ResourceCollectionWriter.cs b/logger/autorest.csharp/mgmt/Generation/ResourceCollectionWriter.cs new file mode 100644 index 0000000..f8b6b29 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/ResourceCollectionWriter.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Requests; +using Azure; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + /// + /// Code writer for resource collection. + /// A resource collection should have 3 operations: + /// 1. CreateOrUpdate (2 variants) + /// 2. Get (2 variants) + /// 3. List (2 variants) + /// + internal class ResourceCollectionWriter : MgmtClientBaseWriter + { + private ResourceCollection This { get; } + + public ResourceCollectionWriter(ResourceCollection resourceCollection) : this(new CodeWriter(), resourceCollection) + { + } + + protected ResourceCollectionWriter(CodeWriter writer, ResourceCollection resourceCollection) + : base(writer, resourceCollection) + { + This = resourceCollection; + _customMethods.Add(nameof(WriteExistsBody), WriteExistsBody); + _customMethods.Add(nameof(WriteGetIfExistsBody), WriteGetIfExistsBody); + } + + protected override void WriteProperties() + { + // we put this method inside this method just because we are trying to preserve their existing sequence + var allPossibleTypes = This.ResourceTypes.SelectMany(p => p.Value).Distinct(); + + FormattableString validResourceType = allPossibleTypes.Count() == 1 + ? validResourceType = GetResourceTypeExpression(allPossibleTypes.First()) + : validResourceType = $"{typeof(Azure.Core.ResourceIdentifier)}.Root.ResourceType"; + _writer.Line(); + + if (allPossibleTypes.Count() == 1) + WriteStaticValidate(validResourceType); + } + + private void WriteExistsBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool async) + { + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(clientOperation.OperationMappings.Values.First()))) + { + var operation = clientOperation.OperationMappings.Values.First(); + var response = new CodeWriterDeclaration(Configuration.ApiTypes.ResponseParameterName); + _writer + .Append($"var {response:D} = {GetAwait(async)} ") + .Append($"{GetRestClientName(operation)}.{CreateMethodName(operation.Method.Name, async)}("); + WriteArguments(_writer, clientOperation.ParameterMappings.Values.First()); + _writer.Line($"cancellationToken: cancellationToken){GetConfigureAwait(async)};"); + _writer.Line($"return {Configuration.ApiTypes.ResponseType}.FromValue({Configuration.ApiTypes.ResponseParameterName}.Value != null, {Configuration.ApiTypes.ResponseParameterName}.{Configuration.ApiTypes.GetRawResponseName}());"); + } + } + + private void WriteGetIfExistsBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool async) + { + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(clientOperation.OperationMappings.Values.First()))) + { + // we need to write multiple branches for a normal method + if (clientOperation.OperationMappings.Count == 1) + { + // if we only have one branch, we would not need those if-else statements + var branch = clientOperation.OperationMappings.Keys.First(); + WriteGetMethodBranch(_writer, clientOperation.OperationMappings[branch], clientOperation.ParameterMappings[branch], async); + } + else + { + // branches go here + throw new NotImplementedException("multi-branch normal method not supported yet"); + } + } + } + + private void WriteGetMethodBranch(CodeWriter writer, MgmtRestOperation operation, IEnumerable parameterMappings, bool async) + { + var response = new CodeWriterDeclaration(Configuration.ApiTypes.ResponseParameterName); + writer + .Append($"var {response:D} = {GetAwait(async)} ") + .Append($"{GetRestClientName(operation)}.{CreateMethodName(operation.Method.Name, async)}("); + WriteArguments(writer, parameterMappings); + writer.Line($"cancellationToken: cancellationToken){GetConfigureAwait(async)};"); + + writer.Line($"if ({response}.Value == null)"); + writer.Line($"return new {new CSharpType(typeof(NoValueResponse<>), operation.MgmtReturnType!)}({response}.{Configuration.ApiTypes.GetRawResponseName}());"); + + if (This.Resource.ResourceData.ShouldSetResourceIdentifier) + { + writer.Line($"{response}.Value.Id = {CreateResourceIdentifierExpression(This.Resource, operation.RequestPath, parameterMappings, $"{response}.Value")};"); + } + + writer.Line($"return {Configuration.ApiTypes.ResponseType}.FromValue(new {operation.MgmtReturnType}({ArmClientReference}, {response}.Value), {response}.{Configuration.ApiTypes.GetRawResponseName}());"); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/ResourceDataWriter.cs b/logger/autorest.csharp/mgmt/Generation/ResourceDataWriter.cs new file mode 100644 index 0000000..95218f0 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/ResourceDataWriter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class ResourceDataWriter : ModelWriter + { + private ResourceData _resourceData; + public ResourceDataWriter(ResourceData resourceData) + { + _resourceData = resourceData; + } + + protected override void WriteProperties(CodeWriter writer, ObjectType schema) + { + base.WriteProperties(writer, schema); + + if (_resourceData.TypeOfId == null) + { + writer.WriteXmlDocumentationSummary($"The resource identifier"); + writer.Line($"public {typeof(Azure.Core.ResourceIdentifier)} Id {{ get; internal set; }}"); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/ResourceWriter.cs b/logger/autorest.csharp/mgmt/Generation/ResourceWriter.cs new file mode 100644 index 0000000..d4d0527 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/ResourceWriter.cs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure; +using Azure.Core; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; +using Resource = AutoRest.CSharp.Mgmt.Output.Resource; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class ResourceWriter : MgmtClientBaseWriter + { + public static ResourceWriter GetWriter(Resource resource) => resource switch + { + PartialResource partialResource => new PartialResourceWriter(partialResource), + _ => new ResourceWriter(resource) + }; + + private Resource This { get; } + + protected ResourceWriter(Resource resource) : this(new CodeWriter(), resource) + { } + + protected ResourceWriter(CodeWriter writer, Resource resource) : base(writer, resource) + { + This = resource; + _customMethods.Add(nameof(WriteAddTagBody), WriteAddTagBody); + _customMethods.Add(nameof(WriteSetTagsBody), WriteSetTagsBody); + _customMethods.Add(nameof(WriteRemoveTagBody), WriteRemoveTagBody); + } + + protected override void WriteStaticMethods() + { + WriteCreateResourceIdentifierMethods(); + _writer.Line(); + } + + private void WriteCreateResourceIdentifierMethods() + { + var method = This.CreateResourceIdentifierMethod; + _writer.WriteMethodDocumentation(method.Signature); + _writer.WriteMethod(method); + } + + protected override void WriteProperties() + { + foreach (var property in This.Properties) + { + _writer.WriteProperty(property); + _writer.Line(); + } + + _writer.Line(); + WriteStaticValidate($"ResourceType"); + } + + private void WriteAddTagBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync) + { + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(This.GetOperation.OperationMappings.Values.First()))) + { + using (_writer.Scope(GetTagResourceCheckString(isAsync))) + { + WriteGetOriginalFromTagResource(isAsync, "[key] = value"); + WriteTaggableCommonMethod(isAsync); + } + using (_writer.Scope($"else")) + { + WriteTaggableCommonMethodFromPutOrPatch(isAsync, "[key] = value"); + } + } + _writer.Line(); + } + + private void WriteSetTagsBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync) + { + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(This.GetOperation.OperationMappings.Values.First()))) + { + using (_writer.Scope(GetTagResourceCheckString(isAsync))) + { + if (isAsync) + { + _writer.Append($"await "); + } + _writer.Line($"GetTagResource().{CreateMethodName("Delete", isAsync)}({typeof(WaitUntil)}.Completed, cancellationToken: cancellationToken){GetConfigureAwait(isAsync)};"); + WriteGetOriginalFromTagResource(isAsync, ".ReplaceWith(tags)"); + WriteTaggableCommonMethod(isAsync); + } + using (_writer.Scope($"else")) + { + WriteTaggableCommonMethodFromPutOrPatch(isAsync, ".ReplaceWith(tags)", true); + } + } + _writer.Line(); + } + + private void WriteRemoveTagBody(MgmtClientOperation clientOperation, Diagnostic diagnostic, bool isAsync) + { + using (_writer.WriteDiagnosticScope(diagnostic, GetDiagnosticReference(This.GetOperation.OperationMappings.Values.First()))) + { + using (_writer.Scope(GetTagResourceCheckString(isAsync))) + { + WriteGetOriginalFromTagResource(isAsync, ".Remove(key)"); + WriteTaggableCommonMethod(isAsync); + } + using (_writer.Scope($"else")) + { + WriteTaggableCommonMethodFromPutOrPatch(isAsync, ".Remove(key)"); + } + } + _writer.Line(); + } + + private static FormattableString GetTagResourceCheckString(bool isAsync) + { + var awaitStr = isAsync ? "await " : String.Empty; + var configureStr = isAsync ? ".ConfigureAwait(false)" : String.Empty; + return $"if({awaitStr} {CreateMethodName("CanUseTagResource", isAsync)}(cancellationToken: cancellationToken){configureStr})"; + } + + private void WriteGetOriginalFromTagResource(bool isAsync, string setCode) + { + _writer.Append($"var originalTags = "); + if (isAsync) + { + _writer.Append($"await "); + } + _writer.Line($"GetTagResource().{CreateMethodName("Get", isAsync)}(cancellationToken){GetConfigureAwait(isAsync)};"); + _writer.Line($"originalTags.Value.Data.TagValues{setCode};"); + } + + private void WriteTaggableCommonMethodFromPutOrPatch(bool isAsync, string setCode, bool isSetTags = false) + { + if (This.UpdateOperation is null && This.CreateOperation is null) + throw new InvalidOperationException($"Unexpected null update method for resource {This.ResourceName} while its marked as taggable"); + var updateOperation = (This.UpdateOperation ?? This.CreateOperation)!; + var getOperation = This.GetOperation; + var updateMethodName = updateOperation.Name; + + var configureStr = isAsync ? ".ConfigureAwait(false)" : string.Empty; + var awaitStr = isAsync ? "await " : string.Empty; + _writer.Line($"var current = ({awaitStr}{CreateMethodName(getOperation.Name, isAsync)}(cancellationToken: cancellationToken){configureStr}).Value.Data;"); + + var lroParamStr = updateOperation.IsLongRunningOperation ? "WaitUntil.Completed, " : string.Empty; + + var parameters = updateOperation.IsLongRunningOperation ? updateOperation.MethodSignature.Parameters.Skip(1) : updateOperation.MethodSignature.Parameters; + var bodyParamType = parameters.First().Type; + string bodyParamName = "current"; + //if we are using PATCH always minimize what we pass in the body to what we actually want to change + if (bodyParamType.Name != This.ResourceData.Type.Name || updateOperation.OperationMappings.Values.First().Operation.HttpMethod == RequestMethod.Patch) + { + bodyParamName = "patch"; + if (bodyParamType is { IsFrameworkType: false, Implementation: ObjectType objectType }) + { + Configuration.MgmtConfiguration.PatchInitializerCustomization.TryGetValue(bodyParamType.Name, out var customizations); + customizations ??= new Dictionary(); + _writer.Append($"var patch = new {bodyParamType}("); + foreach (var parameter in objectType.InitializationConstructor.Signature.Parameters) + { + var varName = parameter.Name.FirstCharToUpperCase(); + if (customizations.TryGetValue(varName, out var customization)) + { + _writer.Append($"{customization}"); + } + else + { + _writer.Append($"current.{varName}, "); + } + } + _writer.RemoveTrailingComma(); + _writer.Line($");"); + } + else + { + _writer.Line($"var patch = new {bodyParamType}();"); + } + if (!isSetTags) + { + using (_writer.Scope($"foreach(var tag in current.Tags)")) + { + _writer.Line($"patch.Tags.Add(tag);"); + } + } + Configuration.MgmtConfiguration.UpdateRequiredCopy.TryGetValue(This.ResourceName, out var properties); + if (properties is not null) + { + foreach (var property in properties.Split(',')) + { + _writer.Line($"patch.{property} = current.{property};"); + } + } + } + + _writer.Line($"{bodyParamName}.Tags{setCode};"); + _writer.Line($"var result = {awaitStr}{CreateMethodName(updateMethodName, isAsync)}({lroParamStr}{bodyParamName}, cancellationToken: cancellationToken){configureStr};"); + if (updateOperation.IsLongRunningOperation) + { + if (updateOperation.MgmtReturnType == null) + { + _writer.Line($"return {awaitStr}{CreateMethodName(getOperation.Name, isAsync)}(cancellationToken: cancellationToken){configureStr};"); + } + else + { + _writer.Line($"return {Configuration.ApiTypes.ResponseType}.FromValue(result.Value, result.{Configuration.ApiTypes.GetRawResponseName}());"); + } + } + else + { + _writer.Line($"return result;"); + } + } + + private void WriteTaggableCommonMethod(bool isAsync) + { + _writer.Line($"{GetAwait(isAsync)} GetTagResource().{CreateMethodName("CreateOrUpdate", isAsync)}({typeof(WaitUntil)}.Completed, originalTags.Value.Data, cancellationToken: cancellationToken){GetConfigureAwait(isAsync)};"); + + var getOperation = This.GetOperation; + // we need to write multiple branches for a normal method + if (getOperation.OperationMappings.Count == 1) + { + // if we only have one branch, we would not need those if-else statements + var branch = getOperation.OperationMappings.Keys.First(); + WriteTaggableCommonMethodBranch(getOperation.OperationMappings[branch], getOperation.ParameterMappings[branch], isAsync); + } + else + { + // branches go here + throw new NotImplementedException("multi-branch normal method not supported yet"); + } + } + + private void WriteTaggableCommonMethodBranch(MgmtRestOperation getOperation, IEnumerable parameterMappings, bool isAsync) + { + var originalResponse = new CodeWriterDeclaration("originalResponse"); + _writer + .Append($"var {originalResponse:D} = {GetAwait(isAsync)} ") + .Append($"{GetRestClientName(getOperation)}.{CreateMethodName(getOperation.Method.Name, isAsync)}("); + + WriteArguments(_writer, parameterMappings, true); + _writer.Line($"cancellationToken){GetConfigureAwait(isAsync)};"); + + if (This.ResourceData.ShouldSetResourceIdentifier) + { + _writer.Line($"{originalResponse}.Value.Id = {CreateResourceIdentifierExpression(This, getOperation.RequestPath, parameterMappings, $"{originalResponse}.Value")};"); + } + + var valueConverter = getOperation.GetValueConverter($"{ArmClientReference}", $"{originalResponse}.Value", getOperation.MgmtReturnType); + if (valueConverter != null) + { + _writer.Line($"return {Configuration.ApiTypes.ResponseType}.FromValue({valueConverter}, {originalResponse}.{Configuration.ApiTypes.GetRawResponseName}());"); + } + else + { + _writer.Line($"return {originalResponse}"); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Generation/StaticUtilWriter.cs b/logger/autorest.csharp/mgmt/Generation/StaticUtilWriter.cs new file mode 100644 index 0000000..b3a1756 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Generation/StaticUtilWriter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using Azure.Core.Pipeline; + +namespace AutoRest.CSharp.Mgmt.Generation +{ + internal class StaticUtilWriter + { + private CodeWriter _writer; + + public StaticUtilWriter(CodeWriter writer) + { + _writer = writer; + } + + public void Write() + { + using (_writer.Namespace(MgmtContext.Context.DefaultNamespace)) + { + using (_writer.Scope($"internal static class ProviderConstants")) + { + WriteProviderDefaultNamespace(); + } + } + } + + protected void WriteProviderDefaultNamespace() + { + _writer.Line($"public static string DefaultProviderNamespace {{ get; }} = {typeof(ClientDiagnostics)}.GetResourceProviderNamespace(typeof(ProviderConstants).Assembly);"); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/MgmtClientOperation.cs b/logger/autorest.csharp/mgmt/Models/MgmtClientOperation.cs new file mode 100644 index 0000000..e5d1e25 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/MgmtClientOperation.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using Azure; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Mgmt.Models +{ + /// + /// A includes at least one . + /// This is a collection of multiple methods with the same purpose but belong to different parents. + /// For instance, one resource might how two different parents, and we can invoke the `CreateOrUpdate` under either of those parents. + /// To make the SDK more user-friendly and considering that our SDK has been built in the "context aware" way, + /// we group these methods together and invoke them by the current context + /// + internal class MgmtClientOperation : IReadOnlyList + { + private const int PropertyBagThreshold = 5; + private readonly Parameter? _extensionParameter; + public static MgmtClientOperation? FromOperations(IReadOnlyList operations, FormattableString idVariableName, Parameter? extensionParameter = null, bool isConvenientOperation = false) + { + if (operations.Count > 0) + { + return new MgmtClientOperation(operations.OrderBy(operation => operation.Name).ToArray(), idVariableName, extensionParameter, isConvenientOperation); + } + + return null; + } + + public static MgmtClientOperation FromOperation(MgmtRestOperation operation, FormattableString idVariableName, Parameter? extensionParameter = null, bool isConvenientOperation = false) + { + return new MgmtClientOperation(new List { operation }, idVariableName, extensionParameter, isConvenientOperation); + } + + public static MgmtClientOperation FromClientOperation(MgmtClientOperation other, FormattableString idVariableName, Parameter? extensionParameter = null, bool isConvenientOperation = false, IReadOnlyList? parameterOverride = null) + { + return new MgmtClientOperation(other._operations, idVariableName, extensionParameter, isConvenientOperation, parameterOverride); + } + + internal FormattableString IdVariableName { get; } + + public Func? ReturnsDescription => _operations.First().ReturnsDescription; + + private IReadOnlyDictionary? _operationMappings; + public IReadOnlyDictionary OperationMappings => _operationMappings ??= EnsureOperationMappings(); + + private IReadOnlyDictionary>? _parameterMappings; + public IReadOnlyDictionary> ParameterMappings => _parameterMappings ??= EnsureParameterMappings(); + + private IReadOnlyList? _methodParameters; + public IReadOnlyList MethodParameters => _methodParameters ??= EnsureMethodParameters(); + + public IReadOnlyList PropertyBagUnderlyingParameters => IsPropertyBagOperation ? _passThroughParams : Array.Empty(); + + private readonly IReadOnlyList _operations; + + private MgmtClientOperation(IReadOnlyList operations, FormattableString idVariableName, Parameter? extensionParameter, bool isConvenientOperation = false) + { + _operations = operations; + _extensionParameter = extensionParameter; + IdVariableName = idVariableName; + IsConvenientOperation = isConvenientOperation; + } + + private MgmtClientOperation(IReadOnlyList operations, FormattableString idVariableName, Parameter? extensionParameter, bool isConvenientOperation = false, IReadOnlyList? parameterOverride = null) : this(operations, idVariableName, extensionParameter, isConvenientOperation) + { + _methodParameters = parameterOverride; + } + + public bool IsConvenientOperation { get; } + + public MgmtRestOperation this[int index] => _operations[index]; + + private MethodSignature? _methodSignature; + public MethodSignature MethodSignature + { + get + { + if (_methodSignature != null) + return _methodSignature; + else + { + var attributes = _operations + .Where(op => Configuration.MgmtConfiguration.PrivilegedOperations.ContainsKey(op.OperationId)) + .Select(op => + { + var arg = Configuration.MgmtConfiguration.PrivilegedOperations[op.OperationId]; + MgmtReport.Instance.TransformSection.AddTransformLog( + new TransformItem(TransformTypeName.PrivilegedOperations, op.OperationId, arg), + op.Operation.GetFullSerializedName(), + $"Operation {op.OperationId} is marked as Privileged Operation"); + return new CSharpAttribute(typeof(Azure.Core.CallerShouldAuditAttribute), Literal(arg)); + }) + .ToList(); + + _methodSignature = new MethodSignature( + Name, + null, + Description, + Accessibility == Public + ? _extensionParameter != null + ? Public | Static | Extension + : Public | Virtual + : Accessibility, + IsPagingOperation + ? new CSharpType(typeof(Pageable<>), ReturnType) + : ReturnType, null, MethodParameters.ToArray(), + attributes); + return _methodSignature; + } + } + } + + // TODO -- we need a better way to get the name of this + public string Name => _operations.First().Name; + + // TODO -- we need a better way to get the description of this + private FormattableString? _description; + public FormattableString Description => _description ??= BuildDescription(); + + private FormattableString BuildDescription() + { + var pathInformation = _operations.Select(operation => + { + FormattableString resourceItem = $""; + if (operation.Resource != null) + { + resourceItem = $@" + +Resource +{operation.Resource.Type:C} +"; + } + FormattableString defaultApiVersion = $""; + if (operation.Operation.Parameters.Where(p => p.IsApiVersion).Any()) + { + defaultApiVersion = $@" + +Default Api Version +{string.Join(", ", operation.Operation.Parameters.Where(p => p.IsApiVersion).Select(x => x.DefaultValue?.Value))} +"; + } + return (FormattableString)$@" +Request Path +{operation.Operation.GetHttpPath()} + + +Operation Id +{operation.OperationId} +{defaultApiVersion}{resourceItem}"; + }).ToArray().Join(Environment.NewLine); + pathInformation = $@" +{pathInformation} +"; + FormattableString? mockingInformation; + if (_extensionParameter == null) + { + mockingInformation = null; + } + else + { + // find the corresponding extension of this method + var extendType = _extensionParameter.Type; + var mockingExtensionTypeName = MgmtMockableExtension.GetMockableExtensionDefaultName(extendType.Name); + + // construct the cref name + FormattableString methodSignature = $"{mockingExtensionTypeName}.{Name}({MethodParameters.Skip(1).GetTypesFormattable(MethodParameters.Count - 1)})"; + mockingInformation = $@" +Mocking +To mock this method, please mock {methodSignature:C} instead. +"; + } + + FormattableString extraInformation = mockingInformation != null ? $"{pathInformation}{Environment.NewLine}{mockingInformation}" : pathInformation; + var descriptionOfOperation = _operations.First().Description; + if (descriptionOfOperation != null) + return $"{descriptionOfOperation}{Environment.NewLine}{extraInformation}"; + return extraInformation; + } + + // TODO -- we need a better way to get this + public IEnumerable Parameters => _operations.First().Parameters; + + public CSharpType? MgmtReturnType => _operations.First().MgmtReturnType; + + public CSharpType ReturnType => _operations.First().ReturnType; + + public MethodSignatureModifiers Accessibility => _operations.First().Accessibility; + + public int Count => _operations.Count; + + public Resource? Resource => _operations.First().Resource; + + public IEnumerator GetEnumerator() => _operations.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _operations.GetEnumerator(); + + public MgmtRestClient RestClient => _operations.First().RestClient; + + public bool IsLongRunningOperation => _operations.First().IsLongRunningOperation; + + public bool IsPagingOperation => _operations.First().IsPagingOperation; + + public bool IsPropertyBagOperation => _passThroughParams.Count() > PropertyBagThreshold && _passThroughParams.All(p => p.IsPropertyBag); + + private IReadOnlyList _passThroughParams => ParameterMappings.Values.First().GetPassThroughParameters(); + + private IReadOnlyDictionary EnsureOperationMappings() + { + return this.ToDictionary( + operation => operation.ContextualPath, + operation => operation); + } + + private IReadOnlyDictionary> EnsureParameterMappings() + { + var contextParams = Resource?.ResourceCollection?.ExtraContextualParameterMapping ?? Enumerable.Empty(); + + var contextualParameterMappings = new Dictionary>(); + foreach (var contextualPath in OperationMappings.Keys) + { + var adjustedPath = Resource is not null ? contextualPath.ApplyHint(Resource.ResourceType) : contextualPath; + contextualParameterMappings.Add(contextualPath, adjustedPath.BuildContextualParameters(IdVariableName).Concat(contextParams)); + } + + var parameterMappings = new Dictionary>(); + foreach (var operationMappings in OperationMappings) + { + var parameterMapping = operationMappings.Value.BuildParameterMapping(contextualParameterMappings[operationMappings.Key]).ToList(); + if (parameterMapping.Where(p => p.IsPassThru).Count() > PropertyBagThreshold) + { + for (int i = 0; i < parameterMapping.Count; ++i) + { + if (parameterMapping[i].IsPassThru) + { + parameterMapping[i] = new ParameterMapping(parameterMapping[i].Parameter with { IsPropertyBag = true }, true, $"{parameterMapping[i].Parameter.GetPropertyBagValueExpression()}", Enumerable.Empty()); + } + } + } + parameterMappings.Add(operationMappings.Key, parameterMapping); + } + return parameterMappings; + } + + private IReadOnlyList EnsureMethodParameters() + { + List parameters = new List(); + if (_extensionParameter is not null) + parameters.Add(_extensionParameter); + if (IsLongRunningOperation) + parameters.Add(KnownParameters.WaitForCompletion); + var overrideParameters = OperationMappings.Values.First().OverrideParameters; + if (overrideParameters.Length > 0) + { + parameters.AddRange(overrideParameters); + } + else + { + if (IsPropertyBagOperation) + { + parameters.Add(_operations.First().GetPropertyBagParameter(_passThroughParams)); + } + else + { + parameters.AddRange(_passThroughParams); + } + } + parameters.Add(KnownParameters.CancellationTokenParameter); + return parameters; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/MgmtRestClientBuilder.cs b/logger/autorest.csharp/mgmt/Models/MgmtRestClientBuilder.cs new file mode 100644 index 0000000..5d91172 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/MgmtRestClientBuilder.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Generation.Writers; + +namespace AutoRest.CSharp.Mgmt.Models +{ + internal class MgmtRestClientBuilder : CmcRestClientBuilder + { + private class ParameterCompareer : IEqualityComparer + { + public bool Equals([AllowNull] InputParameter x, [AllowNull] InputParameter y) + { + if (x is null) + return y is null; + + if (y is null) + return false; + + return x.Name == y.Name && x.Kind == y.Kind; + } + + public int GetHashCode([DisallowNull] InputParameter obj) + { + return obj.Name.GetHashCode() ^ obj.Kind.GetHashCode(); + } + } + + public MgmtRestClientBuilder(InputClient inputClient) + : base(inputClient.Parameters.Concat(GetMgmtParametersFromOperations(inputClient.Operations)), MgmtContext.Context) + { + } + + private static IReadOnlyList GetMgmtParametersFromOperations(IReadOnlyList operations) + { + var parameters = new HashSet(new ParameterCompareer()); + foreach (var operation in operations) + { + var clientParameters = operation.Parameters.Where(p => p.Kind == InputOperationParameterKind.Client); + foreach (var parameter in clientParameters) + { + if (!parameter.IsEndpoint && !parameter.IsApiVersion) + { + throw new InvalidOperationException($"'{parameter.Name}' should be method parameter for operation '{operation.OperationId}'"); + } + parameters.Add(parameter); + } + } + return parameters.ToList(); + } + + public override Parameter BuildConstructorParameter(InputParameter requestParameter) + { + var parameter = base.BuildConstructorParameter(requestParameter); + return parameter.IsApiVersionParameter + ? parameter with { DefaultValue = Constant.Default(parameter.Type.WithNullable(true)), Initializer = parameter.DefaultValue?.GetConstantFormattable() } + : parameter; + } + + protected override Parameter[] BuildMethodParameters(IReadOnlyDictionary allParameters) + { + List requiredParameters = new(); + List optionalParameters = new(); + List bodyParameters = new(); + foreach (var (requestParameter, parameter) in allParameters) + { + // Grouped and flattened parameters shouldn't be added to methods + if (IsMethodParameter(requestParameter)) + { + // sort the parameters by the following sequence: + // 1. required parameters + // 2. body parameters (if exists), note that form data can generate multiple body parameters (e.g. "in": "formdata") + // see test project `body-formdata` for more details + // 3. optional parameters + if (parameter.RequestLocation == RequestLocation.Body) + { + bodyParameters.Add(parameter); + } + else if (parameter.IsOptionalInSignature) + { + optionalParameters.Add(parameter); + } + else + { + requiredParameters.Add(parameter); + } + } + } + + requiredParameters.AddRange(bodyParameters.OrderBy(p => p.IsOptionalInSignature)); // move required body parameters at the beginning + requiredParameters.AddRange(optionalParameters); + + return requiredParameters.ToArray(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/MgmtRestOperation.cs b/logger/autorest.csharp/mgmt/Models/MgmtRestOperation.cs new file mode 100644 index 0000000..94ad955 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/MgmtRestOperation.cs @@ -0,0 +1,563 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Input.InputTypes; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using AutoRest.CSharp.Mgmt.Output.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Azure.ResourceManager; +using Operation = AutoRest.CSharp.Input.Operation; + +namespace AutoRest.CSharp.Mgmt.Models +{ + /// + /// A includes some invocation information of a + /// We have the that will be invoked, also we have the "Contextual Path" of this method, + /// which records the context of this method invocation, + /// providing you the information that which part of the `Id` we should pass to the parameter of + /// + internal record MgmtRestOperation + { + private static readonly string[] NULLABLE_RESPONSE_METHOD_NAMES = { "GetIfExists" }; + + private bool? _isLongRunning; + + /// + /// The underlying object. + /// + public InputOperation Operation { get; } + + public string OperationId => Operation.OperationId; + + /// + /// The name of this operation + /// + public string Name { get; } + + private string? _description; + public string? Description => _description ??= Method.Description; + public IEnumerable Parameters => Method.Parameters; + + public OperationSource? OperationSource { get; } + + private Func? _returnsDescription; + public Func? ReturnsDescription => IsPagingOperation ? _returnsDescription ??= EnsureReturnsDescription() : null; + + public PagingMethodWrapper? PagingMethod { get; } + + private CSharpType? _mgmtReturnType; + public CSharpType? MgmtReturnType => _mgmtReturnType ??= GetMgmtReturnType(OriginalReturnType); + + public CSharpType? ListItemType => IsPagingOperation ? MgmtReturnType : null; + + private CSharpType? _wrappedMgmtReturnType; + public CSharpType ReturnType => _wrappedMgmtReturnType ??= GetWrappedMgmtReturnType(MgmtReturnType); + + public MethodSignatureModifiers Accessibility => Method.Accessibility; + public bool IsPagingOperation => Operation.Paging != null || IsListOperation; + + private string? _propertyBagName; + + private IEnumerable? _propertyBagSelectedParams; + + private Parameter? _propertyBagParameter; + + private bool IsListOperation => PagingMethod != null; + + public CSharpType? OriginalReturnType { get; } + + /// + /// The actual operation + /// + public RestClientMethod Method { get; } + /// + /// The request path of this operation + /// + public RequestPath RequestPath { get; } + /// + /// The contextual path of this operation + /// + public RequestPath ContextualPath { get; } + /// + /// From which RestClient is this operation invoked + /// + public MgmtRestClient RestClient { get; } + + public Resource? Resource { get; } + + public bool ThrowIfNull { get; } + + public bool IsLongRunningOperation => _isLongRunning.HasValue ? _isLongRunning.Value : Operation.IsLongRunning; + + public bool IsFakeLongRunningOperation => IsLongRunningOperation && !Operation.IsLongRunning; + + public Parameter[] OverrideParameters { get; } = Array.Empty(); + + public OperationFinalStateVia? FinalStateVia { get; } + + public InputType? FinalResponseSchema => Operation.LongRunning?.FinalResponse.BodyType; + + public MgmtRestOperation(InputOperation operation, RequestPath requestPath, RequestPath contextualPath, string methodName, bool? isLongRunning = null, bool throwIfNull = false, string? propertyBagName = null) + { + var method = MgmtContext.Library.GetRestClientMethod(operation); + var restClient = MgmtContext.Library.GetRestClient(operation); + + _propertyBagName = propertyBagName; + _isLongRunning = isLongRunning; + ThrowIfNull = throwIfNull; + Operation = operation; + Method = method; + PagingMethod = GetPagingMethodWrapper(method); + RestClient = restClient; + RequestPath = requestPath; + ContextualPath = contextualPath; + Name = methodName; + Resource = GetResourceMatch(restClient, method, requestPath); + FinalStateVia = operation.LongRunning?.FinalStateVia; + OriginalReturnType = operation.IsLongRunning ? GetFinalResponse() : Method.ReturnType; + OperationSource = GetOperationSource(); + } + + public MgmtRestOperation(MgmtRestOperation other, string nameOverride, CSharpType? overrideReturnType, string overrideDescription, params Parameter[] overrideParameters) + : this(other, nameOverride, overrideReturnType, overrideDescription, other.ContextualPath, overrideParameters) + { + } + + public MgmtRestOperation(MgmtRestOperation other, string nameOverride, CSharpType? overrideReturnType, string overrideDescription, RequestPath contextualPath, params Parameter[] overrideParameters) + { + //copy values from other method + _propertyBagName = other._propertyBagName; + _isLongRunning = other.IsLongRunningOperation; + ThrowIfNull = other.ThrowIfNull; + Operation = other.Operation; + Method = other.Method; + PagingMethod = other.PagingMethod; + RestClient = other.RestClient; + RequestPath = other.RequestPath; + ContextualPath = contextualPath; + Resource = other.Resource; + FinalStateVia = other.FinalStateVia; + OriginalReturnType = other.OriginalReturnType; + OperationSource = other.OperationSource; + + //modify some of the values + Name = nameOverride; + _mgmtReturnType = overrideReturnType; + _description = overrideDescription; + OverrideParameters = overrideParameters; + } + + private OperationSource? GetOperationSource() + { + if (!IsLongRunningOperation) + return null; + + if (MgmtReturnType is null) + return null; + + if (IsFakeLongRunningOperation) + return null; + + if (!MgmtContext.Library.CSharpTypeToOperationSource.TryGetValue(MgmtReturnType, out var operationSource)) + { + var resourceBeingReturned = MgmtReturnType is { IsFrameworkType: false, Implementation: Resource resource } ? resource : null; + operationSource = new OperationSource(MgmtReturnType, resourceBeingReturned, FinalResponseSchema!); + MgmtContext.Library.CSharpTypeToOperationSource.Add(MgmtReturnType, operationSource); + } + return operationSource; + } + + private CSharpType? GetFinalResponse() + { + var finalSchema = Operation.LongRunning?.FinalResponse.BodyType; + if (finalSchema is null) + return null; + try + { + return MgmtContext.TypeFactory.CreateType(finalSchema); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Final response for {RestClient.InputClient.Key}.{Method.Name} was not found it was of type {finalSchema.Name}", ex); + } + } + + /// + /// This method returns a value converter statement in from the underlying rest operation return type, to the desired + /// Note: MgmtReturnType always refers to the type that is not wrapped by Response, LRO or Pageable. + /// + /// + /// + /// + /// + public FormattableString? GetValueConverter(FormattableString clientVariable, FormattableString valueVariable, CSharpType? mgmtReturnType) + { + var restReturnType = IsPagingOperation ? PagingMethod!.ItemType : Method.ReturnType; + // when the method returns nothing, when this happens, the methodReturnType should either be Response, or ArmOperation + if (restReturnType == null && mgmtReturnType == null) + return null; + + Debug.Assert(restReturnType != null); + Debug.Assert(mgmtReturnType != null); + + // check if this operation need a response converter + if (mgmtReturnType.Equals(restReturnType)) + return null; + + var isRestReturnTypeResourceData = restReturnType is { IsFrameworkType: false, Implementation: ResourceData }; + + // second check: if the method is returning a Resource and the rest operation is returning a ResourceData + if (isRestReturnTypeResourceData && mgmtReturnType is { IsFrameworkType: false, Implementation: Resource returnResource }) + { + // in this case we should call the constructor of the resource to wrap it into a resource + return GetValueConverter(returnResource, clientVariable, valueVariable); + } + + // otherwise we return null + return null; + } + + public FormattableString? GetValueConverter(FormattableString clientVariable, FormattableString valueVariable) => GetValueConverter(clientVariable, valueVariable, MgmtReturnType); + + private FormattableString GetValueConverter(Resource resource, FormattableString clientVariable, FormattableString valueVariable) => $"new {resource.Type}({clientVariable}, {valueVariable})"; + + internal enum ResourceMatchType + { + Exact, + ParentList, // list of myself from my direct owner - ex. VirtualMachine list on resource group + AncestorList, // list of myself from my grand parent (or higher) - ex. VirtualMachine list on subscription + ChildList, // list something else on myself - ex. List skus on VirtualMachine (usually listing non-resource) GET + Context, // actions or behavior on myself - ex. Start on VirtualMachine POST + CheckName, + None + } + + private Resource? GetResourceMatch(MgmtRestClient restClient, RestClientMethod method, RequestPath requestPath) + { + if (restClient.Resources.Count == 1) + return restClient.Resources[0]; + + Dictionary> matches = new Dictionary>(); + foreach (var resource in restClient.Resources) + { + var match = GetMatchType(Operation.HttpMethod, resource.RequestPath, requestPath, method.IsListMethod(out var _)); + if (match == ResourceMatchType.None) + continue; + if (match == ResourceMatchType.Exact) + return resource; + if (!matches.TryGetValue(match, out var result)) + { + result = new HashSet(); + matches.Add(match, result); + } + result.Add(resource); + } + + FormattableString errorText = (FormattableString)$"{restClient.Type.Name}.{method.Name}"; + foreach (ResourceMatchType? matchType in Enum.GetValues(typeof(ResourceMatchType))) + { + var resource = GetMatch(matchType!.Value, matches, errorText); + + if (resource is not null) + return resource; + } + return null; + } + + private Resource? GetMatch(ResourceMatchType matchType, Dictionary> matches, FormattableString error) + { + if (!matches.TryGetValue(matchType, out var matchTypeMatches)) + return null; + + var first = matchTypeMatches.First(); + if (matchTypeMatches.Count == 1) + return first; + + // if we have multiple candidates, and the match is CheckName, we could return null because this check name could check name availibility of multiple resources + if (matchType == ResourceMatchType.CheckName) + return null; + + var parent = first.GetParents().First(); + if (parent is not null && AllMatchesSameParent(matchTypeMatches, parent, out bool areAllSingleton) && areAllSingleton) + return parent as Resource; + + //this throw catches anything we do not expect if it ever fires it means our logic is either incomplete or we need a directive to adjust the request paths + throw new InvalidOperationException($"Found more than 1 candidate for {error}, results were ({string.Join(',', matchTypeMatches.Select(r => r.Type.Name))})"); + } + + private bool AllMatchesSameParent(HashSet matches, MgmtTypeProvider parent, out bool areAllSingleton) + { + areAllSingleton = true; + foreach (var resource in matches) + { + areAllSingleton &= resource.IsSingleton; + var current = resource.GetParents().FirstOrDefault(); + if (current is null) + return false; + if (!current.Equals(parent)) + return false; + } + return true; + } + + /// + /// This method returns true if the first scope could cover the second scope + /// AKA operationScope >= resourceScope + /// For instance, if operationScope = subscription, resourceScope = resourceGroup, this should return true + /// if operationScope = resourceGroup, resourceGroup = subscription, this should return false + /// if operationScope = managementGroup, resourceGroup = subscription, this should return false + /// + /// + /// + /// + private static bool DoesScopeCover(RequestPath operationScope, RequestPath resourceScope, RequestPath operationPath, RequestPath resourcePath) + { + if (operationScope.Equals(resourceScope)) + return true; + // bigger scope should be shorter + if (operationScope.IsAncestorOf(resourceScope)) + return true; + + // if resource has a parameterized scope, operation does not, we need to check if the scope type of the operation is included by the scope of resource + if (resourceScope.IsParameterizedScope() && !operationScope.IsParameterizedScope()) + { + var resourceScopeTypes = resourceScope.GetParameterizedScopeResourceTypes(); + if (resourceScopeTypes is null) + return false; + var operationScopeType = operationScope.GetResourceType(); + if (resourceScopeTypes.Contains(ResourceTypeSegment.Any) || resourceScopeTypes.Contains(operationScopeType)) + return true; + } + return false; + } + + private static void SeparateScope(RequestPath original, out RequestPath scope, out RequestPath trimmed) + { + if (original.IndexOfLastProviders >= 0) + { + scope = original.GetScopePath(); + trimmed = original.TrimScope(); + } + else + { + scope = RequestPath.Empty; + trimmed = original; + } + } + + private static bool TryGetListMatch(RequestMethod method, RequestPath resourcePath, RequestPath operationPath, bool isList, out ResourceMatchType matchType) + { + matchType = ResourceMatchType.None; + if (!isList) + return false; + if (method != RequestMethod.Get) + return false; + var operationLastSegment = operationPath.Last(); + if (!operationLastSegment.IsConstant) + return false; + SeparateScope(resourcePath, out var resourceScopePath, out var trimmedResourcePath); + SeparateScope(operationPath, out var operationScopePath, out var trimmedOperationPath); + + if (trimmedResourcePath.Count > 0 && trimmedOperationPath.Equals(RequestPath.FromSegments(trimmedResourcePath.SkipLast(1))) && DoesScopeCover(operationScopePath, resourceScopePath, operationPath, resourcePath)) + { + matchType = (resourceScopePath.IsParameterizedScope(), operationScopePath.IsParameterizedScope()) switch + { + // if they are neither scope, we just check if the scope is the same. If it is the same, it is parent list, otherwise it is ancestor list. + (false, false) => resourceScopePath.GetResourceType() == operationScopePath.GetResourceType() ? ResourceMatchType.ParentList : ResourceMatchType.AncestorList, + // if resource has a scope, and the operation does not, and since we are here, the scope of resource covers the scope of the operation, this should always be a parent list (it should be a branch in the parent list operation + (true, false) => ResourceMatchType.ParentList, + _ => ResourceMatchType.AncestorList, + }; + return true; + } + + return false; + } + + internal static ResourceMatchType GetMatchType(RequestMethod httpMethod, RequestPath resourcePath, RequestPath requestPath, bool isList) + { + //check exact match + if (resourcePath == requestPath) + return ResourceMatchType.Exact; + + var requestLastSegment = requestPath.Last(); + //check for a list by a parent or an ancestor + if (TryGetListMatch(httpMethod, resourcePath, requestPath, isList, out var listMatch)) + return listMatch; + + //check for single value methods after the GET path which are typically POST methods + if (resourcePath.Count == requestPath.Count - 1 && requestLastSegment.IsConstant && AreEqualBackToProvider(resourcePath, requestPath, 0, 1)) + return isList ? ResourceMatchType.ChildList : ResourceMatchType.Context; + + if (httpMethod == RequestMethod.Get) + return ResourceMatchType.None; + + var resourceLastSegement = resourcePath.Last(); + //sometimes for singletons the POST methods show up at the same level + if (resourcePath.Count == requestPath.Count && requestLastSegment.IsConstant && resourceLastSegement.IsConstant && AreEqualBackToProvider(resourcePath, requestPath, 1, 1)) + return ResourceMatchType.Context; + + //catch check name availability where the provider ending matches + //this one catches a lot so we are narrowing it down to containing "name" dont know all the checknameavailability name types + if (requestLastSegment.IsConstant && + RequestPath.Subscription.IsAncestorOf(requestPath) && + requestLastSegment.ToString().Contains("name", StringComparison.OrdinalIgnoreCase) && + AreEqualBackToProvider(resourcePath, requestPath, 2, 1)) + return ResourceMatchType.CheckName; + + return ResourceMatchType.None; + } + + internal Parameter GetPropertyBagParameter(IEnumerable parameters) + { + // considering this method might be invoked several times in the future + // we use _propertyBagParameter to cache the last result + // and return it directly if the input parameter is the same as the previous one + if (_propertyBagSelectedParams != null && _propertyBagSelectedParams.SequenceEqual(parameters)) + { + return _propertyBagParameter!; + } + else + { + _propertyBagSelectedParams = parameters; + } + var clientName = _propertyBagName == null ? + MgmtContext.Context.DefaultNamespace.Equals(typeof(ArmClient).Namespace) ? "Arm" : $"{MgmtContext.Context.DefaultNamespace.Split('.').Last()}Extensions" : _propertyBagName; + + var propertyBagName = $"{clientName}{Name}"; + if (Configuration.MgmtConfiguration.RenamePropertyBag.TryGetValue(OperationId, out string? modelName)) + { + if (modelName.EndsWith("Options")) + { + propertyBagName = modelName.ReplaceLast("Options", string.Empty); + } + else + { + throw new InvalidOperationException($"The property bag model name for {OperationId} should end with Options."); + } + } + var propertyBag = ((MgmtPropertyBag)Method.PropertyBag!).WithUpdatedInfo(propertyBagName, parameters); + var schemaObject = propertyBag.PackModel; + var existingModels = MgmtContext.Library.PropertyBagModels.Where(m => m.Type.Name == schemaObject.Type.Name); + if (existingModels != null) + { + // sometimes we might have two or more property bag models with same name but different properties + // we will throw exception in this case to prompt the user to rename the property bag model + if (IsDuplicatedPropertyBag(existingModels, (ModelTypeProvider)schemaObject)) + { + throw new InvalidOperationException($"Another property bag model named {schemaObject.Type.Name} already exists, please use configuration `rename-property-bag` to rename the property bag model corresponding to the operation {OperationId}."); + } + } + MgmtContext.Library.PropertyBagModels.Add(schemaObject); + return _propertyBagParameter = propertyBag.PackParameter; + } + + private static bool IsDuplicatedPropertyBag(IEnumerable existingModels, ModelTypeProvider modelToAdd) + { + foreach (var model in existingModels) + { + if (model is not ModelTypeProvider mgmtModel) + continue; + if (mgmtModel.Properties.Count() != modelToAdd.Properties.Count()) + return true; + for (int i = 0; i < mgmtModel.Properties.Count(); i++) + { + if (mgmtModel.Properties[i].Declaration.Name != modelToAdd.Properties[i].Declaration.Name) + return true; + } + } + return false; + } + + private static bool AreEqualBackToProvider(RequestPath resourcePath, RequestPath requestPath, int resourceSkips, int requestSkips) + { + int resourceStart = resourcePath.Count - 1 - resourceSkips; + int requestStart = requestPath.Count - 1 - requestSkips; + //resourcePath will have an extra reference segment for the resource name. Skip this and walk back to the providers or beginning of array and everything must match to that point + for (int resourceIndex = resourceStart, requestIndex = requestStart; resourceIndex >= 0 && requestIndex >= 0; resourceIndex--, requestIndex--) + { + if (resourcePath[resourceIndex].IsReference && resourcePath[resourceIndex].Equals(requestPath[requestIndex], false)) + continue; //there are sometimes name differences in the path variable used but the rest of the context is the same + + if (resourcePath[resourceIndex] != requestPath[requestIndex]) + return false; + + if (resourcePath[resourceIndex] == Segment.Providers) + return true; + } + return true; + } + + private static bool AreEqualUpToProvider(RequestPath resourcePath, RequestPath requestPath) + { + for (int i = 0; i < Math.Min(resourcePath.Count, requestPath.Count); i++) + { + if (resourcePath[i] == Segment.Providers) + return true; + + if (resourcePath[i] != requestPath[i]) + return false; + } + return true; + } + + private CSharpType GetWrappedMgmtReturnType(CSharpType? originalType) + { + if (originalType is null) + return IsLongRunningOperation ? typeof(ArmOperation) : Configuration.ApiTypes.ResponseType; + + if (IsPagingOperation) + return originalType; + + return IsLongRunningOperation ? originalType.WrapOperation(false) : originalType.WrapResponse(isAsync: false, isNullable: NULLABLE_RESPONSE_METHOD_NAMES.Contains(this.Name)); + } + + private CSharpType? GetMgmtReturnType(CSharpType? originalType) + { + //try for list method + originalType = PagingMethod?.ItemType ?? originalType; + + if (originalType == null || originalType is not { IsFrameworkType: false, Implementation: ResourceData data }) + return originalType; + + if (Resource is not null && Resource.ResourceData.Type.Equals(originalType)) + return Resource.Type; + + var foundResources = MgmtContext.Library.FindResources(data).ToList(); + return foundResources.Count switch + { + 0 => throw new InvalidOperationException($"No resource corresponding to {originalType?.Name} is found"), + 1 => foundResources.Single().Type, + _ => originalType // we have multiple resource matched, we can only return the original type without wrapping it + }; + } + + private PagingMethodWrapper? GetPagingMethodWrapper(RestClientMethod method) + { + if (MgmtContext.Library.PagingMethods.Value.TryGetValue(method, out var pagingMethod)) + return new PagingMethodWrapper(pagingMethod); + + if (method.IsListMethod(out var itemType, out var valuePropertyName)) + return new PagingMethodWrapper(method, itemType, valuePropertyName); + + return null; + } + + private Func EnsureReturnsDescription() + => (isAsync) => $"{(isAsync ? "An async" : "A")} collection of {ListItemType!:C} that may take multiple service requests to iterate over."; + } +} diff --git a/logger/autorest.csharp/mgmt/Models/NameTransformer.cs b/logger/autorest.csharp/mgmt/Models/NameTransformer.cs new file mode 100644 index 0000000..b20b038 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/NameTransformer.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Utilities; +using Microsoft.CodeAnalysis.CSharp; +using static AutoRest.CSharp.Input.MgmtConfiguration; + +namespace AutoRest.CSharp.Mgmt.Models +{ + internal class NameTransformer + { + private record AppliedCache(NameInfo NewName, List AppliedDetailSteps); + public record ApplyDetailStep(string MappingKey, AcronymMappingTarget MappingValue, NameInfo NewName); + + private static NameTransformer? _instance; + public static NameTransformer Instance => _instance ??= new NameTransformer(Configuration.MgmtConfiguration.AcronymMapping); + + private IDictionary _acronymMapping = new Dictionary + { + { "CPU", new AcronymMappingTarget("Cpu") }, + { "CPUs", new AcronymMappingTarget("Cpus") }, + { "Os", new AcronymMappingTarget("OS") }, + { "Ip", new AcronymMappingTarget("IP") }, + { "Ips", new AcronymMappingTarget("IPs", "ips") }, + { "ID", new AcronymMappingTarget("Id") }, + { "IDs", new AcronymMappingTarget("Ids") }, + { "VMM", new AcronymMappingTarget("Vmm") }, + { "VM", new AcronymMappingTarget("Vm") }, + { "VMs", new AcronymMappingTarget("Vms") }, + { "VMScaleSet", new AcronymMappingTarget("VmScaleSet") }, + { "DNS", new AcronymMappingTarget("Dns") }, + { "VPN", new AcronymMappingTarget("Vpn") }, + { "NAT", new AcronymMappingTarget("Nat") }, + { "WAN", new AcronymMappingTarget("Wan") }, + { "Ipv4", new AcronymMappingTarget("IPv4", "ipv4") }, + { "Ipv6", new AcronymMappingTarget("IPv6", "ipv6") }, + { "Ipsec", new AcronymMappingTarget("IPsec", "ipsec") }, + { "URI", new AcronymMappingTarget("Uri") }, + // Need to set parameter value for ETag to "etag" as well + { "ETag", new AcronymMappingTarget("ETag", "etag") }, + { "Etag", new AcronymMappingTarget("ETag", "etag") }, + { "QoS", new AcronymMappingTarget("Qos") }, + }; + + private Regex _regex; + private ConcurrentDictionary _wordCache; + + /// + /// Instanciate a NameTransformer which uses the dictionary to transform the abbreviations in this word to correct casing + /// + /// + internal NameTransformer(IReadOnlyDictionary acronymMapping) + { + foreach (var (key, value) in acronymMapping) + { + _acronymMapping[key] = value; + } + _regex = BuildRegex(_acronymMapping.Keys); + _wordCache = new ConcurrentDictionary(); + } + + private static Regex BuildRegex(IEnumerable renameItems) + { + var regexRawString = string.Join("|", renameItems); + // we are building the regex that matches + // 1. it starts with a lower case letter or a digit which is considered as the end of its previous word + // or it is at the beginning of this string + // or we hit a word separator including \W, _, ., @, -, spaces + // 2. it either should match one of the candidates in the rename rules dictionary + // 3. it should followed by a upper case letter which is considered as the beginning of next word + // or it is the end of this string + // or we hit a word separator including \W, _, ., @, -, spaces + return new Regex(@$"([\W|_|\.|@|-|\s|\$\da-z]|^)({regexRawString})([\W|_|\.|@|-|\s|\$A-Z]|$)"); + } + + /// + /// Parse the input name and produce a name with correct casing + /// + /// + /// + public NameInfo EnsureNameCase(string name, Action? onMappingApplied = null) + { + if (_wordCache.TryGetValue(name, out var result)) + { + if (onMappingApplied != null) + result.AppliedDetailSteps.ForEach(record => onMappingApplied(record)); + return result.NewName; + } + + // escape the following logic if we do not have any rules + if (_acronymMapping.Count == 0) + { + result = new AppliedCache( + new NameInfo(name, name), + new List()); + _wordCache.TryAdd(name, result); + return result.NewName; + } + + var propertyNameBuilder = new StringBuilder(); + var parameterNameBuilder = new StringBuilder(); + var strToMatch = name.FirstCharToUpperCase(); + var match = _regex.Match(strToMatch); + var detailStep = new List(); + bool hasFirstWord = false; + while (match.Success) + { + // in our regular expression, the content we want to find is in the second group + var matchGroup = match.Groups[2]; + var replaceValue = _acronymMapping[matchGroup.Value]; + // append everything between the beginning and the index of this match + var everythingBeforeMatch = strToMatch.Substring(0, matchGroup.Index); + // append everything before myself + propertyNameBuilder.Append(everythingBeforeMatch); + parameterNameBuilder.Append(everythingBeforeMatch); + // append the replaced value + propertyNameBuilder.Append(replaceValue.Value); + // see if everything before myself is empty, or is all invalid character for an identifier which will be trimmed off, which makes the current word the first word + if (!hasFirstWord && IsEquivelantEmpty(everythingBeforeMatch)) + { + hasFirstWord = true; + parameterNameBuilder.Append(replaceValue.ParameterValue ?? replaceValue.Value); + } + else + parameterNameBuilder.Append(replaceValue.Value); + // move to whatever is left unmatched + strToMatch = strToMatch.Substring(matchGroup.Index + matchGroup.Length); + + string tempPropertyName = propertyNameBuilder.ToString() + strToMatch; + string tempParameterName = parameterNameBuilder.ToString() + strToMatch; + NameInfo tempNameInfo = new NameInfo(tempPropertyName, tempParameterName); + var step = new ApplyDetailStep(matchGroup.Value, replaceValue, tempNameInfo); + if (onMappingApplied != null) + onMappingApplied(step); + detailStep.Add(step); + match = _regex.Match(strToMatch); + } + if (strToMatch.Length > 0) + { + propertyNameBuilder.Append(strToMatch); + parameterNameBuilder.Append(strToMatch); + } + + result = new AppliedCache(new NameInfo(propertyNameBuilder.ToString(), parameterNameBuilder.ToString()), detailStep); + _wordCache.TryAdd(name, result); + _wordCache.TryAdd(result.NewName.Name, result); // in some other scenarios we might need to use the property name as keys + + return result.NewName; + } + + private static bool IsEquivelantEmpty(string s) => string.IsNullOrWhiteSpace(s) || s.All(c => !SyntaxFacts.IsIdentifierStartCharacter(c)); + + internal record NameInfo(string Name, string VariableName); + } +} diff --git a/logger/autorest.csharp/mgmt/Models/OperationSet.cs b/logger/autorest.csharp/mgmt/Models/OperationSet.cs new file mode 100644 index 0000000..d61eed1 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/OperationSet.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Common.Input; +using Azure.Core; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; + +namespace AutoRest.CSharp.Mgmt.Models +{ + /// + /// An represents a collection of with the same request path. + /// + internal class OperationSet : IReadOnlyCollection, IEquatable + { + private readonly InputClient? _inputClient; + + /// + /// The raw request path of string of the operations in this + /// + public string RequestPath { get; } + + /// + /// The operation set + /// + private HashSet Operations { get; } + + public int Count => Operations.Count; + + public OperationSet(string requestPath, InputClient? inputClient) + { + _inputClient = inputClient; + RequestPath = requestPath; + Operations = new HashSet(); + } + + /// + /// Add a new operation to this + /// + /// The operation to be added + /// when trying to add an operation with a different path from + public void Add(InputOperation operation) + { + var path = operation.GetHttpPath(); + if (path != RequestPath) + throw new InvalidOperationException($"Cannot add operation with path {path} to OperationSet with path {RequestPath}"); + Operations.Add(operation); + } + + public IEnumerator GetEnumerator() => Operations.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => Operations.GetEnumerator(); + + public override int GetHashCode() + { + return RequestPath.GetHashCode(); + } + + public bool Equals([AllowNull] OperationSet other) + { + if (other is null) + return false; + + return RequestPath == other.RequestPath; + } + + /// + /// Get the operation with the given verb. + /// We cannot have two operations with the same verb under the same request path, therefore this method is only returning one operation or null + /// + /// + /// + public InputOperation? GetOperation(RequestMethod method) + { + foreach (var operation in Operations) + { + if (operation.HttpMethod == method) + return operation; + } + + return null; + } + + public RequestPath GetRequestPath(ResourceTypeSegment? hint = null) + { + return hint.HasValue ? NonHintRequestPath.ApplyHint(hint.Value) : NonHintRequestPath; + } + + private RequestPath? _nonHintRequestPath; + public RequestPath NonHintRequestPath => _nonHintRequestPath ??= GetNonHintRequestPath(); + private RequestPath GetNonHintRequestPath() + { + var operation = FindBestOperation(); + if (_inputClient is not null && Operations.Any()) + { + if (operation != null) + { + return Models.RequestPath.FromOperation(operation, _inputClient, MgmtContext.TypeFactory); + } + } + + // we do not have an operation in this operation set to construct the RequestPath + // therefore this must be a request path for a virtual resource + // we find an operation with a prefix of this and take that many segment from its path as the request path of this operation set + OperationSet? hintOperationSet = null; + foreach (var operationSet in MgmtContext.Library.RawRequestPathToOperationSets.Values) + { + // skip myself + if (operationSet == this) + continue; + // also skip the sets that with zero operations, they are operation set of partial resources as well + if (operationSet.Count == 0) + continue; + + if (Models.RequestPath.IsPrefix(RequestPath, operationSet.RequestPath)) + { + hintOperationSet = operationSet; + break; + } + } + + if (hintOperationSet == null) + { + throw new InvalidOperationException($"cannot build request path for {RequestPath}. This usually happens when `partial-resource` is assigned but when there is no operation actually with this prefix, please double check"); + } + return BuildRequestPathFromHint(hintOperationSet); + } + + private RequestPath BuildRequestPathFromHint(OperationSet hint) + { + var hintPath = hint.GetRequestPath(); + var segmentsCount = RequestPath.Split('/', StringSplitOptions.RemoveEmptyEntries).Length; + var segments = hintPath.Take(segmentsCount); + return Models.RequestPath.FromSegments(segments); + } + + private InputOperation? FindBestOperation() + { + // first we try GET operation + var getOperation = FindOperation(RequestMethod.Get); + if (getOperation != null) + return getOperation; + // if no GET operation, we return PUT operation + var putOperation = FindOperation(RequestMethod.Put); + if (putOperation != null) + return putOperation; + + // if no PUT or GET, we just return the first one + return Operations.FirstOrDefault(); + } + + public InputOperation? FindOperation(RequestMethod method) + { + return this.FirstOrDefault(operation => operation.HttpMethod == method); + } + + public bool IsById => NonHintRequestPath.IsById; + + public override string? ToString() + { + return RequestPath; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/PagingMethodWrapper.cs b/logger/autorest.csharp/mgmt/Models/PagingMethodWrapper.cs new file mode 100644 index 0000000..adf9eec --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/PagingMethodWrapper.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Requests; + +namespace AutoRest.CSharp.Mgmt.Models; + +internal class PagingMethodWrapper +{ + public PagingMethodWrapper(PagingMethod pagingMethod) + { + Method = pagingMethod.Method; + NextPageMethod = pagingMethod.NextPageMethod; + NextLinkName = pagingMethod.PagingResponse.NextLinkPropertyName; + ItemName = pagingMethod.PagingResponse.ItemPropertyName; + ItemType = pagingMethod.PagingResponse.ItemType; + } + + public PagingMethodWrapper(RestClientMethod method, CSharpType itemType, string valuePropertyName) + { + Method = method; + NextPageMethod = null; + NextLinkName = null; + ItemName = valuePropertyName; + ItemType = itemType; + } + + public CSharpType ItemType { get; } + + /// + /// This is the underlying of this paging method + /// + public RestClientMethod Method { get; } + + /// + /// This is the REST method for getting next page if there is one + /// + public RestClientMethod? NextPageMethod { get; } + + /// + /// This is the property name in the response body, usually the value of this is `Value` + /// + public string ItemName { get; } + + /// + /// This is the name of the nextLink property if there is one. + /// + public string? NextLinkName { get; } +} diff --git a/logger/autorest.csharp/mgmt/Models/RequestPath.cs b/logger/autorest.csharp/mgmt/Models/RequestPath.cs new file mode 100644 index 0000000..0ba512c --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/RequestPath.cs @@ -0,0 +1,559 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Azure.ResourceManager; +using Azure.ResourceManager.ManagementGroups; +using Azure.ResourceManager.Resources; + +namespace AutoRest.CSharp.Mgmt.Models; + +/// +/// A represents a parsed request path in the swagger which corresponds to an operation. For instance, `/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines` +/// +internal readonly struct RequestPath : IEquatable, IReadOnlyList +{ + private const string _providerPath = "/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}"; + private const string _featurePath = "/subscriptions/{subscriptionId}/providers/Microsoft.Features/providers/{resourceProviderNamespace}/features"; + + internal const string ManagementGroupScopePrefix = "/providers/Microsoft.Management/managementGroups"; + internal const string ResourceGroupScopePrefix = "/subscriptions/{subscriptionId}/resourceGroups"; + internal const string SubscriptionScopePrefix = "/subscriptions"; + internal const string TenantScopePrefix = "/tenants"; + + public static readonly RequestPath Empty = new(Array.Empty()); + + public static readonly RequestPath Null = new(new[] { new Segment("") }); + + /// + /// This is a placeholder of request path for "any" resources in other RPs + /// + public static readonly RequestPath Any = new(new[] { new Segment("*") }); + + /// + /// The of a resource group resource + /// + public static readonly RequestPath ResourceGroup = new(new[] { + new Segment("subscriptions"), + new Segment(new Reference("subscriptionId", typeof(string)), true, true), + new Segment("resourceGroups"), + new Segment(new Reference("resourceGroupName", typeof(string)), true, false) + }); + + /// + /// The of a subscription resource + /// + public static readonly RequestPath Subscription = new(new[] { + new Segment("subscriptions"), + new Segment(new Reference("subscriptionId", typeof(string)), true, true) + }); + + /// + /// The of tenants + /// + public static readonly RequestPath Tenant = new(Enumerable.Empty()); + + /// + /// The of a management group resource + /// + public static readonly RequestPath ManagementGroup = new(new[] { + new Segment("providers"), + new Segment("Microsoft.Management"), + new Segment("managementGroups"), + // We use strict = false because we usually see the name of management group is different in different RPs. Some of them are groupId, some of them are groupName, etc + new Segment(new Reference("managementGroupId", typeof(string)), true, false) + }); + + private static Dictionary? _extensionChoices; + public static Dictionary ExtensionChoices => _extensionChoices ??= new() + { + [typeof(TenantResource)] = RequestPath.Tenant, + [typeof(ManagementGroupResource)] = RequestPath.ManagementGroup, + [typeof(SubscriptionResource)] = RequestPath.Subscription, + [typeof(ResourceGroupResource)] = RequestPath.ResourceGroup, + }; + + public static RequestPath GetContextualPath(Type armCoreType) + { + return ExtensionChoices[armCoreType]; + } + + private readonly IReadOnlyList _segments; + + public static RequestPath FromString(string rawPath) + { + var rawSegments = rawPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + + var segments = rawSegments.Select(GetSegmentFromString); + + return new RequestPath(segments); + } + + public static RequestPath FromSegments(IEnumerable segments) => new RequestPath(segments); + + public static RequestPath FromOperation(InputOperation operation, InputClient inputClient, TypeFactory typeFactory) + { + var references = new MgmtRestClientBuilder(inputClient).GetReferencesToOperationParameters(operation, operation.Parameters, inputClient.Parameters); + + var segments = new List(); + var segmentIndex = 0; + CreateSegments(operation.Uri, references, segments, ref segmentIndex); + CreateSegments(operation.Path, references, segments, ref segmentIndex); + + return new RequestPath(CheckByIdPath(segments), operation.GetHttpPath()); + + throw new ErrorHelpers.ErrorException($"We didn't find request path for {inputClient.Key}.{operation.CleanName}"); + } + + private static Segment GetSegmentFromString(string str) + { + var trimmed = TrimRawSegment(str); + var isScope = trimmed == "scope"; + return new Segment(trimmed, escape: !isScope, isConstant: !isScope && !str.Contains('{')); + } + + private static string TrimRawSegment(string segment) => segment.TrimStart('{').TrimEnd('}'); + + public int IndexOfLastProviders { get; } + + private RequestPath(IReadOnlyList segments, string httpPath) + { + _segments = segments; + SerializedPath = httpPath; + IsExpandable = GetIsExpandable(segments); + IndexOfLastProviders = _segments.ToList().LastIndexOf(Segment.Providers); + } + + private static bool GetIsExpandable(IEnumerable segments) + => segments + .Where((s, i) => i % 2 == 0 && s.IsReference && !s.Reference.Type.IsFrameworkType && s.Reference.Type.Implementation is EnumType) + .Any(); + + private static IReadOnlyList CheckByIdPath(IReadOnlyList segments) + { + // if this is a byId request path, we need to make it strict, since it might be accidentally to be any scope request path's parent + if (segments.Count != 1) + return segments; + var first = segments.First(); + if (first.IsConstant) + return segments; + if (!first.SkipUrlEncoding) + return segments; + + // this is a ById request path + return new List { new(first.Reference, first.Escape, true) }; + } + + public bool IsById => Count == 1 && this.First().SkipUrlEncoding; + + /// + /// Constructs the instance using a collection of + /// This is used for the request path that does not come from the swagger document, or an incomplete request path + /// + /// + private RequestPath(IEnumerable segments) : this(segments.ToArray(), Segment.BuildSerializedSegments(segments)) + { + } + + /// + /// The raw request path of this instance + /// + public string SerializedPath { get; } + + public bool IsExpandable { get; } + + /// + /// Check if this is a prefix path of + /// Note that this.IsAncestorOf(this) will return false which indicates that this method is testing the "proper ancestor" like a proper subset. + /// + /// + /// + public bool IsAncestorOf(RequestPath other) + { + // To be the parent of other, you must at least be shorter than other. + if (other.Count <= Count) + return false; + for (int i = 0; i < Count; i++) + { + // we need the segment to be identical when strict is true (which is the default value) + // when strict is false, we also need the segment to be identical if it is constant. + // but if it is a reference, we only require they have the same type, do not require they have the same variable name. + // This case happens a lot during the management group parent detection - different RP calls this different things + if (!this[i].Equals(other[i])) + return false; + } + return true; + } + + /// + /// Check if is a prefix path of + /// While comparing, we will ignore everything inside {} + /// For instance, if "/subs/{subsId}/rgs/{name}/foo" and "/subs/{subsId}/rgs/{name}/foo/bar/{something}", + /// we are effectively comparing /subs/{}/rgs/{}/foo and /subs/{}/rgs/{}/foo/bar/{} + /// + /// + /// + /// + public static bool IsPrefix(string requestPath, string candidate) + { + // Create spans for the candidate and request path + ReadOnlySpan candidateSpan = candidate.AsSpan(); + ReadOnlySpan requestPathSpan = requestPath.AsSpan(); + + int cIdx = 0, rIdx = 0; + + // iterate through everything on request path + while (rIdx < requestPathSpan.Length) + { + // if we run out of candidate, return false because request path here is effectively longer than candidate + if (cIdx >= candidateSpan.Length) + return false; + + // if we hit a { + char c = candidateSpan[cIdx]; + char r = requestPathSpan[rIdx]; + + if (c != r) + return false; + + if (c == '{') + { + // they both are {, skip everything until we have a } or we get to the last character of the string + while (cIdx < candidateSpan.Length - 1 && candidateSpan[cIdx] != '}') + cIdx++; + while (rIdx < requestPathSpan.Length - 1 && requestPathSpan[rIdx] != '}') + rIdx++; + } + else + { + // they are the same but not { + cIdx++; + rIdx++; + } + } + + return true; + } + + /// + /// Trim this from the other and return the that remain. + /// The result is "other - this" by removing this as a prefix of other. + /// If this == other, return empty request path + /// + /// + /// + /// if this.IsAncestorOf(other) is false + public RequestPath TrimAncestorFrom(RequestPath other) + { + if (TryTrimAncestorFrom(other, out var diff)) + return diff; + + throw new InvalidOperationException($"Request path {this} is not parent of {other}"); + } + + public bool TryTrimAncestorFrom(RequestPath other, [MaybeNullWhen(false)] out RequestPath diff) + { + diff = default; + if (this == other) + { + diff = RequestPath.Tenant; + return true; + } + if (this.IsAncestorOf(other)) + { + diff = new RequestPath(other._segments.Skip(this.Count)); + return true; + } + // Handle the special case of trim provider from feature + else if (this.SerializedPath == _providerPath && other.SerializedPath.StartsWith(_featurePath)) + { + diff = new RequestPath(other._segments.Skip(this.Count + 2)); + return true; + } + return false; + } + + /// + /// Trim the scope out of this request path. + /// If this is already a scope path, return the empty request path, aka the RequestPath.Tenant + /// + /// + public RequestPath TrimScope() + { + var scope = this.GetScopePath(); + // The scope for /subscriptions is /subscriptions/{subscriptionId}, we identify such case with scope.Count > this.Count. + if (scope == this || scope.Count > this.Count) + return Tenant; // if myself is a scope path, we return the empty path after the trim. + return scope.TrimAncestorFrom(this); + } + + public RequestPath Append(RequestPath other) + { + return new RequestPath(this._segments.Concat(other._segments)); + } + + public RequestPath ApplyHint(ResourceTypeSegment hint) + { + if (hint.Count == 0) + return this; + int hintIndex = 0; + List newPath = new List(); + int thisIndex = 0; + for (; thisIndex < _segments.Count; thisIndex++) + { + var segment = this[thisIndex]; + if (segment.IsExpandable) + { + newPath.Add(hint[hintIndex]); + hintIndex++; + } + else + { + if (segment.Equals(hint[hintIndex])) + { + hintIndex++; + } + newPath.Add(segment); + } + if (hintIndex >= hint.Count) + { + thisIndex++; + break; + } + } + + //copy remaining items in this + for (; thisIndex < _segments.Count; thisIndex++) + { + newPath.Add(_segments[thisIndex]); + } + return new RequestPath(newPath); + } + + public IEnumerable Expand() + { + // we first get the resource type + var resourceType = this.GetResourceType(); + + // if this resource type is a constant, we do not need to expand it + if (resourceType.IsConstant) + return this.AsIEnumerable(); + + // otherwise we need to expand them (the resource type is not a constant) + // first we get all the segment that is not a constant + var possibleValueMap = new Dictionary>(); + foreach (var segment in resourceType.Where(segment => segment.IsReference && !segment.Reference.Type.IsFrameworkType)) + { + var type = segment.Reference.Type.Implementation; + switch (type) + { + case EnumType enumType: + possibleValueMap.Add(segment, enumType.Values.Select(v => new Segment(v.Value, segment.Escape, segment.IsStrict, enumType.Type))); + break; + default: + throw new InvalidOperationException($"The resource type {this} contains variables in it, but it is not an enum type, therefore we cannot expand it. Please double check and/or override it in `request-path-to-resource-type` section."); + } + } + + // construct new resource types to make the resource types constant again + // here we are traversing the segments in this resource type as a tree: + // if the segment is constant, just add it into the result + // if the segment is not a constant, we need to add its all possible values (they are all constants) into the result + // first we build the levels + var levels = this.Select(segment => segment.IsConstant || !possibleValueMap.ContainsKey(segment) ? + segment.AsIEnumerable() : + possibleValueMap[segment]); + // now we traverse the tree to get the result + var queue = new Queue>(); + foreach (var level in levels) + { + // initialize + if (queue.Count == 0) + { + foreach (var _ in level) + queue.Enqueue(new List()); + } + // get every element in queue out, and push the new results back + int count = queue.Count; + for (int i = 0; i < count; i++) + { + var list = queue.Dequeue(); + foreach (var segment in level) + { + // push the results back with a new element on it + queue.Enqueue(new List(list) { segment }); + } + } + } + + return queue.Select(list => new RequestPath(list)); + } + + private static ISet GetScopeResourceTypes(RequestPath requestPath) + { + var scope = requestPath.GetScopePath(); + if (scope.IsParameterizedScope()) + { + return new HashSet(requestPath.GetParameterizedScopeResourceTypes()!); + } + + return new HashSet { scope.GetResourceType() }; + } + + /// + /// Return true if the scope resource types of the first path are a subset of the second path + /// + /// + /// + /// + public static bool IsScopeCompatible(RequestPath requestPath, RequestPath resourcePath) + { + // get scope types + var requestScopeTypes = GetScopeResourceTypes(requestPath); + var resourceScopeTypes = GetScopeResourceTypes(resourcePath); + if (resourceScopeTypes.Contains(ResourceTypeSegment.Any)) + return true; + return requestScopeTypes.IsSubsetOf(resourceScopeTypes); + } + + private static void CreateSegments(string path, IReadOnlyDictionary references, ICollection segments, ref int segmentIndex) + { + foreach ((ReadOnlySpan span, bool isLiteral) in Utilities.StringExtensions.GetPathParts(path)) + { + if (isLiteral) + { + // in this case, we have a constant in this path segment, which might contains slashes + // we need to split it into real segments + // For instance we might get "/providers/Microsoft.Storage/blobServices/default" here + // we will never get null in this constant since it comes from request path + var literalSpan = span; + while (!literalSpan.IsEmpty) + { + var separatorIndex = literalSpan.IndexOf('/'); + if (separatorIndex > 0) + { + segmentIndex++; + segments.Add(new Segment(literalSpan[..separatorIndex].ToString())); + } + + if (separatorIndex < 0) + { + segments.Add(new Segment(literalSpan.ToString())); + break; + } + + literalSpan = literalSpan[(separatorIndex + 1)..]; + } + } + else + { + if (references.TryGetValue(span.ToString(), out var parameterReference)) + { + segmentIndex++; + var (referenceOrConstant, skipUriEncoding) = parameterReference; + var valueType = referenceOrConstant.Type; + + // we explicitly skip the `uri` variable in the path (which should be `endpoint`) + if (valueType.Equals(typeof(Uri))) + { + continue; + } + + //for now we only assume expand variables are in the key slot which will be an odd slot + CSharpType? expandableType = segmentIndex % 2 == 0 && !valueType.IsFrameworkType && valueType.Implementation is EnumType ? valueType : null; + + // this is either a constant but not string type, or it is not a constant, we just keep the information in this path segment + segments.Add(new Segment(referenceOrConstant, !skipUriEncoding, expandableType: expandableType)); + } + else + { + ErrorHelpers.ThrowError($"\n\nError while processing request '{path}'\n\n '{span.ToString()}' in URI is missing a matching definition in the path parameters collection{ErrorHelpers.UpdateSwaggerOrFile}"); + } + } + } + } + + private static ReferenceOrConstant CreateReference(InputParameter requestParameter, CSharpType type, TypeFactory typeFactory) + { + if (requestParameter.Kind != InputOperationParameterKind.Method) + { + return new Reference(requestParameter.CSharpName(), type); + } + + if (requestParameter.Kind == InputOperationParameterKind.Constant) + { + return BuilderHelpers.ParseConstant(requestParameter.DefaultValue, typeFactory.CreateType(requestParameter.Type)); + } + + var groupedByParameter = requestParameter.GroupedBy; + if (groupedByParameter == null) + { + return new Reference(requestParameter.CSharpName(), type); + } + + var groupModel = (SchemaObjectType)typeFactory.CreateType(groupedByParameter.Type).Implementation; + var property = groupModel.GetPropertyBySerializedName(requestParameter.NameInRequest ?? requestParameter.Name); + + return new Reference($"{groupedByParameter.CSharpName()}.{property.Declaration.Name}", property.Declaration.Type); + } + + private static string GetRequestParameterName(InputParameter requestParameter) + { + return requestParameter.NameInRequest ?? requestParameter.Name; + } + + public int Count => _segments.Count; + + public Segment this[int index] => _segments[index]; + + public bool Equals(RequestPath other) + { + if (Count != other.Count) + return false; + for (int i = 0; i < Count; i++) + { + if (!this[i].Equals(other[i])) + return false; + } + return true; + } + + public override bool Equals(object? obj) => obj is RequestPath other && Equals(other); + + public IEnumerator GetEnumerator() => _segments.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _segments.GetEnumerator(); + + public override int GetHashCode() => SerializedPath.GetHashCode(); + + public override string ToString() => SerializedPath; + + public static bool operator ==(RequestPath left, RequestPath right) + { + return left.Equals(right); + } + + public static bool operator !=(RequestPath left, RequestPath right) + { + return !(left == right); + } + + public static implicit operator string(RequestPath requestPath) + { + return requestPath.SerializedPath; + } +} diff --git a/logger/autorest.csharp/mgmt/Models/ResourceObjectAssociation.cs b/logger/autorest.csharp/mgmt/Models/ResourceObjectAssociation.cs new file mode 100644 index 0000000..e7cc007 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/ResourceObjectAssociation.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Mgmt.Output; + +namespace AutoRest.CSharp.Mgmt.Models +{ + internal record ResourceObjectAssociation + { + public ResourceTypeSegment ResourceType { get; } + + public ResourceData ResourceData { get; } + + public Resource Resource { get; } + + public ResourceCollection? ResourceCollection { get; } + + public ResourceObjectAssociation(ResourceTypeSegment resourceType, ResourceData resourceData, Resource resource, ResourceCollection? resourceCollection) + { + ResourceType = resourceType; + ResourceData = resourceData; + Resource = resource; + ResourceCollection = resourceCollection; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/ResourceTypeSegment.cs b/logger/autorest.csharp/mgmt/Models/ResourceTypeSegment.cs new file mode 100644 index 0000000..b4a4557 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/ResourceTypeSegment.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; + +namespace AutoRest.CSharp.Mgmt.Models +{ + /// + /// A represents the resource type that derives from a . It can contain variables in it. + /// + internal struct ResourceTypeSegment : IEquatable, IReadOnlyList + { + public static readonly ResourceTypeSegment Scope = new(Array.Empty()); + + public static readonly ResourceTypeSegment Any = new(new[] { new Segment("*") }); + + /// + /// The of the resource group resource + /// + public static readonly ResourceTypeSegment ResourceGroup = new(new[] { + new Segment("Microsoft.Resources"), + new Segment("resourceGroups") + }); + + /// + /// The of the subscription resource + /// + public static readonly ResourceTypeSegment Subscription = new(new[] { + new Segment("Microsoft.Resources"), + new Segment("subscriptions") + }); + + /// + /// The of the tenant resource + /// + public static readonly ResourceTypeSegment Tenant = new(new Segment[] { + new Segment("Microsoft.Resources"), + new Segment("tenants") + }); + + /// + /// The of the management group resource + /// + public static readonly ResourceTypeSegment ManagementGroup = new(new[] { + new Segment("Microsoft.Management"), + new Segment("managementGroups") + }); + + private IReadOnlyList _segments; + + public static ResourceTypeSegment ParseRequestPath(RequestPath path) + { + // first try our built-in resources + if (path == RequestPath.Subscription) + return ResourceTypeSegment.Subscription; + if (path == RequestPath.ResourceGroup) + return ResourceTypeSegment.ResourceGroup; + if (path == RequestPath.ManagementGroup) + return ResourceTypeSegment.ManagementGroup; + if (path == RequestPath.Tenant) + return ResourceTypeSegment.Tenant; + if (path == RequestPath.Any) + return ResourceTypeSegment.Any; + + return Parse(path); + } + + public ResourceTypeSegment(string path) + : this(path.Split('/', StringSplitOptions.RemoveEmptyEntries).Select(segment => new Segment(segment)).ToList()) + { + } + + private ResourceTypeSegment(IReadOnlyList segments) + { + _segments = segments; + SerializedType = Segment.BuildSerializedSegments(segments, false); + IsConstant = _segments.All(segment => segment.IsConstant); + } + + private static ResourceTypeSegment Parse(RequestPath path) + { + var segment = new List(); + // find providers + var paths = path.ToList(); + int index = paths.LastIndexOf(Segment.Providers); + if (index < 0 || index == paths.Count - 1) + { + if (path.SerializedPath.StartsWith(RequestPath.ResourceGroupScopePrefix, StringComparison.InvariantCultureIgnoreCase)) + return ResourceTypeSegment.ResourceGroup; + if (path.SerializedPath.StartsWith(RequestPath.SubscriptionScopePrefix, StringComparison.InvariantCultureIgnoreCase)) + return ResourceTypeSegment.Subscription; + if (path.SerializedPath.Equals(RequestPath.TenantScopePrefix)) + return ResourceTypeSegment.Tenant; + throw new ErrorHelpers.ErrorException($"Could not set ResourceTypeSegment for request path {path}. No {Segment.Providers} string found in the URI. Please assign a valid resource type in `request-path-to-resource-type` configuration"); + } + segment.Add(path[index + 1]); + segment.AddRange(path.Skip(index + 1).Where((_, index) => index % 2 != 0)); + + return new ResourceTypeSegment(segment); + } + + /// + /// Returns true if every in this is constant + /// + public bool IsConstant { get; } + + public string SerializedType { get; } + + public Segment Namespace => this[0]; + + public IEnumerable Types => _segments.Skip(1); + + public Segment this[int index] => _segments[index]; + + public int Count => _segments.Count; + + public bool Equals(ResourceTypeSegment other) => SerializedType.Equals(other.SerializedType, StringComparison.InvariantCultureIgnoreCase); + + public IEnumerator GetEnumerator() => _segments.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _segments.GetEnumerator(); + + public override bool Equals(object? obj) + { + if (obj == null) + return false; + var other = (ResourceTypeSegment)obj; + return other.Equals(this); + } + + public override int GetHashCode() => SerializedType.GetHashCode(); + + public override string? ToString() => SerializedType; + + public static bool operator ==(ResourceTypeSegment left, ResourceTypeSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(ResourceTypeSegment left, ResourceTypeSegment right) + { + return !(left == right); + } + + internal bool DoesMatch(ResourceTypeSegment other) + { + if (Count == 0) + return other.Count == 0; + + if (Count != other.Count) + return false; + + if (this[Count - 1].IsConstant == other[Count - 1].IsConstant) + return this.Equals(other); + + return DoAllButLastItemMatch(other); //TODO: limit matching to the enum values + } + + private bool DoAllButLastItemMatch(ResourceTypeSegment other) + { + for (int i = 0; i < Count - 1; i++) + { + if (!this[i].Equals(other[i])) + return false; + } + return true; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/Segment.cs b/logger/autorest.csharp/mgmt/Models/Segment.cs new file mode 100644 index 0000000..054c93e --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/Segment.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Models +{ + /// + /// A represents a segment of a request path which could be either a or a + /// + internal readonly struct Segment : IEquatable + { + public static readonly Segment Providers = "providers"; + + private readonly ReferenceOrConstant _value; + private readonly string _stringValue; + private readonly CSharpType? _expandableType; + + public Segment(ReferenceOrConstant value, bool escape, bool strict = false, CSharpType? expandableType = null) + { + _value = value; + _stringValue = value.IsConstant ? value.Constant.Value?.ToString() ?? "null" + : $"({value.Reference.Type.Name}){value.Reference.Name}"; + IsStrict = strict; + Escape = escape; + _expandableType = expandableType; + } + + /// + /// Creates a new instance of . + /// + /// The string value for the segment. + /// Wether or not this segment is escaped. + /// Wether or not to use strict validate for this segment. + /// Whether this segment is a constant vs a reference. + public Segment(string value, bool escape = true, bool strict = false, bool isConstant = true) + : this(isConstant ? new Constant(value, typeof(string)) : new Reference(value, typeof(string)), escape, strict) + { + } + + /// + /// Represents the value of `x-ms-skip-url-encoding` if the corresponding path parameter has this extension + /// + public bool SkipUrlEncoding => !Escape; + + /// + /// If this is a constant, escape is guranteed to be true, since our segment has been split by slashes. + /// If this is a reference, escape is false when the corresponding parameter has x-ms-skip-url-encoding = true indicating this might be a scope variable + /// + public bool Escape { get; init; } + + /// + /// Mark if this segment is strict when comparing with each other. + /// IsStrict only works on Reference and does not work on Constant + /// If IsStrict is false, and this is a Reference, we will only compare the type is the same when comparing + /// But when IsStrict is true, or this is a Constant, we will always ensure we have the same value (for constant) or same reference name (for reference) and the same type + /// + public bool IsStrict { get; init; } + + public bool IsConstant => _value.IsConstant; + + public bool IsReference => !IsConstant; + + public bool IsExpandable => _expandableType is not null; + + public CSharpType Type => _expandableType ?? _value.Type; + + /// + /// Returns the of this segment + /// + /// if this.IsConstant is false + public Constant Constant => _value.Constant; + + /// + /// Returns the value of the of this segment in string + /// + /// if this.IsConstant is false + public string ConstantValue => _value.Constant.Value?.ToString() ?? "null"; + + /// + /// Returns the of this segment + /// + /// if this.IsReference is false + public Reference Reference => _value.Reference; + + /// + /// Returns the name of the of this segment + /// + /// if this.IsReference is false + public string ReferenceName => _value.Reference.Name; + + internal bool Equals(Segment other, bool strict) + { + if (strict) + return ExactEquals(this, other); + if (this.IsConstant) + return ExactEquals(this, other); + // this is a reference, we will only test if the Type is the same + if (other.IsConstant) + return ExactEquals(this, other); + // now other is also a reference + return this._value.Reference.Type.Equals(other._value.Reference.Type); + } + + private static bool ExactEquals(Segment left, Segment right) + { + return left._stringValue.Equals(right._stringValue, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Test if a segment is the same to the other. We will use strict mode if either of these two is strict. + /// + /// + /// + public bool Equals(Segment other) => this.Equals(other, IsStrict || other.IsStrict); + + public override bool Equals(object? obj) + { + if (obj == null) + return false; + var other = (Segment)obj; + return other.Equals(this); + } + + public override int GetHashCode() => _stringValue.GetHashCode(); + + public override string ToString() => _stringValue; + + internal static string BuildSerializedSegments(IEnumerable segments, bool slashPrefix = true) + { + var strings = segments.Select(segment => segment.IsConstant ? segment.ConstantValue : $"{{{segment.ReferenceName}}}"); + var prefix = slashPrefix ? "/" : string.Empty; + return $"{prefix}{string.Join('/', strings)}"; + } + + public static implicit operator Segment(string value) => new Segment(value); + + public static bool operator ==(Segment left, Segment right) + { + return left.Equals(right); + } + + public static bool operator !=(Segment left, Segment right) + { + return !(left == right); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Models/SingletonResourceSuffix.cs b/logger/autorest.csharp/mgmt/Models/SingletonResourceSuffix.cs new file mode 100644 index 0000000..6353ca8 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Models/SingletonResourceSuffix.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Generation.Writers; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Mgmt.Models +{ + internal class SingletonResourceSuffix + { + public static SingletonResourceSuffix Parse(string[] segments) + { + // put the segments in pairs + var pairs = new List<(string Key, string Value)>(); + for (int i = 0; i < segments.Length; i += 2) + { + pairs.Add((segments[i], segments[i + 1])); + } + + return new SingletonResourceSuffix(pairs); + } + + private IReadOnlyList<(string Key, string Value)> _pairs; + + private SingletonResourceSuffix(IReadOnlyList<(string Key, string Value)> pairs) + { + _pairs = pairs; + } + + public ValueExpression BuildResourceIdentifier(ValueExpression originalId) + { + var result = originalId; + for (int i = 0; i < _pairs.Count; i++) + { + var (key, value) = _pairs[i]; + if (key == Segment.Providers) + { + // when we have a providers segment, we must have a next pair + i++; + var (nextKey, nextValue) = _pairs[i]; + // build expression: => .AppendProviderResource(value, nextKey, nextValue) + result = new InvokeStaticMethodExpression( + typeof(ResourceIdentifier), + nameof(ResourceIdentifier.AppendProviderResource), + new[] { result, Literal(value), Literal(nextKey), Literal(nextValue) }, + CallAsExtension: true); + } + else + { + // if not, we just call the method to append this pair + result = new InvokeStaticMethodExpression( + typeof(ResourceIdentifier), + nameof(ResourceIdentifier.AppendChildResource), + new[] { result, Literal(key), Literal(value) }, + CallAsExtension: true); + } + } + + return result; + } + + public override string ToString() + { + return string.Join("/", this._pairs.Select(kv => $"{(kv.Key == Segment.Providers ? "" : $"{kv.Key}/")}{kv.Value}")); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/ArmClientExtension.cs b/logger/autorest.csharp/mgmt/Output/ArmClientExtension.cs new file mode 100644 index 0000000..9b8b63d --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/ArmClientExtension.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class ArmClientExtension : MgmtExtension + { + private readonly List _extensions; + private readonly ArmResourceExtension _armResourceExtensionForChildResources; + public ArmClientExtension(IReadOnlyDictionary> armResourceExtensionOperations, IEnumerable extensionClients, ArmResourceExtension armResourceExtensionForChildResources) + : base(Enumerable.Empty(), extensionClients, typeof(ArmClient), RequestPath.Tenant) + { + _armResourceExtensionForChildResources = armResourceExtensionForChildResources; + _extensions = new(); + foreach (var (parentRequestPath, operations) in armResourceExtensionOperations) + { + _extensions.Add(new(operations, extensionClients, typeof(ArmResource), parentRequestPath)); + } + } + + public override bool IsEmpty => !MgmtContext.Library.ArmResources.Any() && base.IsEmpty; + + protected override string VariableName => Configuration.MgmtConfiguration.IsArmCore ? "this" : "client"; + + public override FormattableString IdVariableName => $"scope"; + public override FormattableString BranchIdVariableName => $"scope"; + + protected override IEnumerable EnsureClientOperations() + { + var extensionParamToUse = Configuration.MgmtConfiguration.IsArmCore ? null : ExtensionParameter; + foreach (var extension in _extensions) + { + foreach (var clientOperation in extension.ClientOperations) + { + var requestPaths = clientOperation.Select(restOperation => restOperation.RequestPath); + var scopeResourceTypes = requestPaths.Select(requestPath => requestPath.GetParameterizedScopeResourceTypes() ?? Enumerable.Empty()).SelectMany(types => types).Distinct(); + var scopeTypes = ResourceTypeBuilder.GetScopeTypeStrings(scopeResourceTypes); + var parameterOverride = clientOperation.MethodParameters.Skip(1).Prepend(GetScopeParameter(scopeTypes)).Prepend(ExtensionParameter).ToArray(); + var newOp = MgmtClientOperation.FromClientOperation(clientOperation, IdVariableName, extensionParameter: extensionParamToUse, parameterOverride: parameterOverride); + yield return newOp; + } + } + } + + // only when in usual packages other than arm core, we need to generate the ArmClient, scope pattern for those scope resources + public override IEnumerable ChildResources => Configuration.MgmtConfiguration.IsArmCore ? Enumerable.Empty() : _armResourceExtensionForChildResources.AllChildResources; + + private readonly Parameter _scopeParameter = new Parameter( + Name: "scope", + Description: $"The scope that the resource will apply against.", + Type: typeof(ResourceIdentifier), + DefaultValue: null, + Validation: ValidationType.None, + Initializer: null); + + private Parameter GetScopeParameter(ICollection? types) + { + if (types == null) + return _scopeParameter; + + return _scopeParameter with + { + Description = $"{_scopeParameter.Description} Expected resource type includes the following: {types.Join(", ", " or ")}" + }; + } + + protected override Method BuildMockableExtensionFactoryMethod() + { + var signature = new MethodSignature( + MockableExtension.FactoryMethodName, + null, + null, + MethodSignatureModifiers.Private | MethodSignatureModifiers.Static, + MockableExtension.Type, + null, + new[] + { + KnownParameters.ArmClient with { Validation = ValidationType.AssertNotNull } + }); + + var extensionVariable = (ValueExpression)KnownParameters.ArmClient; + var clientVariable = new VariableReference(typeof(ArmClient), "client"); + var body = Snippets.Return( + extensionVariable.Invoke(nameof(ArmClient.GetCachedClient), + new FuncExpression(new[] { clientVariable.Declaration }, Snippets.New.Instance(MockableExtension.Type, clientVariable)) + )); + return new(signature, body); + } + + protected override Method BuildGetSingletonResourceMethod(Resource resource) + { + var originalMethod = base.BuildGetSingletonResourceMethod(resource); + if (IsArmCore) + return originalMethod; + + // we need to add a scope parameter inside the method signature + var originalSignature = (MethodSignature)originalMethod.Signature; + var scopeTypes = ResourceTypeBuilder.GetScopeTypeStrings(resource.RequestPath.GetParameterizedScopeResourceTypes()); + var parameters = new List() + { + // add the first parameter, which is the extension parameter + originalSignature.Parameters[0], + // then we add the scope parameter + GetScopeParameter(scopeTypes) + }; + parameters.AddRange(originalSignature.Parameters.Skip(1)); // add all remaining parameters + var signatureOnMockingExtension = originalSignature with + { + Modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + Parameters = parameters.Skip(1).ToArray() + }; + var signature = originalSignature with + { + Description = $"{BuildDescriptionForSingletonResource(resource)}{Environment.NewLine}{BuildMockingExtraDescription(signatureOnMockingExtension)}", + Parameters = parameters + }; + + return BuildRedirectCallToMockingExtension(signature, signatureOnMockingExtension); + } + + protected override Method BuildGetChildCollectionMethod(ResourceCollection collection) + { + var originalMethod = base.BuildGetChildCollectionMethod(collection); + if (IsArmCore) + return originalMethod; + + // we need to add a scope parameter inside the method signature + var originalSignature = (MethodSignature)originalMethod.Signature; + var scopeTypes = ResourceTypeBuilder.GetScopeTypeStrings(collection.RequestPath.GetParameterizedScopeResourceTypes()); + var parameters = new List() + { + // add the first parameter, which is the extension parameter + originalSignature.Parameters[0], + // then we add the scope parameter + GetScopeParameter(scopeTypes) + }; + parameters.AddRange(originalSignature.Parameters.Skip(1)); // add all remaining parameters + var signatureOnMockingExtension = originalSignature with + { + Modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + Parameters = parameters.Skip(1).ToArray() + }; + var signature = originalSignature with + { + Description = $"{BuildDescriptionForChildCollection(collection)}{Environment.NewLine}{BuildMockingExtraDescription(signatureOnMockingExtension)}", + Parameters = parameters + }; + + return BuildRedirectCallToMockingExtension(signature, signatureOnMockingExtension); + } + + private IEnumerable? _armResourceMethods; + public IEnumerable ArmResourceMethods => _armResourceMethods ??= BuildArmResourceMethods(); + + private IEnumerable BuildArmResourceMethods() + { + foreach (var resource in MgmtContext.Library.ArmResources) + { + yield return BuildArmResourceMethod(resource); + } + } + + private Method BuildArmResourceMethod(Resource resource) + { + var lines = new List(); + string an = resource.Type.Name.StartsWithVowel() ? "an" : "a"; + lines.Add($"Gets an object representing {an} along with the instance operations that can be performed on it but with no data."); + lines.Add($"You can use to create {an} from its components."); + var description = FormattableStringHelpers.Join(lines, Environment.NewLine); + + var parameters = new List + { + _resourceIdParameter + }; + + var signatureOnMockingExtension = new MethodSignature( + $"Get{resource.Type.Name}", + null, + description, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + resource.Type, + $"Returns a {resource.Type:C} object.", + parameters); + + if (IsArmCore) + { + var methodBody = new MethodBodyStatement[]{ + new InvokeStaticMethodStatement(resource.Type, "ValidateResourceId", _resourceIdParameter), + Snippets.Return(Snippets.New.Instance(resource.Type, Snippets.This, _resourceIdParameter)) + }; + + return new(signatureOnMockingExtension, methodBody); + } + else + { + var signature = signatureOnMockingExtension with + { + Description = $"{description}{Environment.NewLine}{BuildMockingExtraDescription(signatureOnMockingExtension)}", + Modifiers = MethodModifiers, + Parameters = parameters.Prepend(ExtensionParameter).ToArray() + }; + + return BuildRedirectCallToMockingExtension(signature, signatureOnMockingExtension); + } + } + + private readonly Parameter _resourceIdParameter = new( + Name: "id", + Description: $"The resource ID of the resource to get.", + Type: typeof(ResourceIdentifier), + DefaultValue: null, + Validation: ValidationType.None, + Initializer: null); + } +} diff --git a/logger/autorest.csharp/mgmt/Output/ArmResourceExtension.cs b/logger/autorest.csharp/mgmt/Output/ArmResourceExtension.cs new file mode 100644 index 0000000..27bf0a0 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/ArmResourceExtension.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.Models; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class ArmResourceExtension : MgmtExtension + { + private readonly List _extensions; + public ArmResourceExtension(IReadOnlyDictionary> armResourceExtensionOperations, IEnumerable extensionClients) : base(Enumerable.Empty(), extensionClients, typeof(ArmResource), RequestPath.Any) + { + _extensions = new(); + foreach (var (parentRequestPath, operations) in armResourceExtensionOperations) + { + _extensions.Add(new(operations, extensionClients, typeof(ArmResource), parentRequestPath)); + } + } + + protected override IEnumerable EnsureClientOperations() + { + foreach (var extension in _extensions) + { + foreach (var clientOperation in extension.ClientOperations) + { + var requestPaths = clientOperation.Select(restOperation => restOperation.RequestPath); + if (ShouldGenerateArmResourceExtensionMethod(requestPaths)) + yield return clientOperation; + } + } + } + + private static bool ShouldGenerateArmResourceExtensionMethod(IEnumerable requestPaths) + => requestPaths.Any(ShouldGenerateArmResourceExtensionMethod); + + private static bool ShouldGenerateArmResourceExtensionMethod(RequestPath requestPath) + => Configuration.MgmtConfiguration.GenerateArmResourceExtensions.Contains(requestPath); + + /// + /// This tracks all resources with a parent of ArmResource + /// We need this to keep the original data of all possible child resources of ArmResource and we will use it again in ArmClientExtension + /// + internal IEnumerable AllChildResources => base.ChildResources; + + private IEnumerable? _filteredArmResourceChildResource; + /// + /// We need to filter out some resources here + /// because in the design of generated code for ArmResourceExtension, we usually do not generate an extension method that extends ArmResource type + /// instead we generate an extension method of ArmClient with a scope instead to avoid that the extension method shows up on every resource + /// because in real life it is not quite possible. + /// + public override IEnumerable ChildResources => _filteredArmResourceChildResource ??= EnsureArmResourceExtensionChildResources(); + + private IEnumerable EnsureArmResourceExtensionChildResources() + { + foreach (var resource in AllChildResources) + { + if (ShouldGenerateArmResourceExtensionMethod(resource.RequestPath)) + yield return resource; + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtExtension.cs b/logger/autorest.csharp/mgmt/Output/MgmtExtension.cs new file mode 100644 index 0000000..1b2c18d --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtExtension.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownCodeBlocks; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtExtension : MgmtTypeProvider + { + public override bool IsStatic => IsArmCore ? false : true; // explicitly expand this for readability + + private readonly IEnumerable _allRawOperations; + + public MgmtExtension(IEnumerable allRawOperations, IEnumerable mockingExtensions, Type armCoreType, RequestPath? contextualPath = null) + : base(armCoreType.Name) + { + _allRawOperations = allRawOperations; + _mockingExtensions = mockingExtensions; // this property is populated later + ArmCoreType = armCoreType; + DefaultName = Configuration.MgmtConfiguration.IsArmCore ? ResourceName : $"{ResourceName}Extensions"; + DefaultNamespace = Configuration.MgmtConfiguration.IsArmCore ? ArmCoreType.Namespace! : base.DefaultNamespace; + Description = Configuration.MgmtConfiguration.IsArmCore ? (FormattableString)$"" : $"A class to add extension methods to {ResourceName}."; + ContextualPath = contextualPath ?? RequestPath.GetContextualPath(armCoreType); + ArmCoreNamespace = ArmCoreType.Namespace!; + ChildResources = !Configuration.MgmtConfiguration.IsArmCore || ArmCoreType.Namespace != MgmtContext.Context.DefaultNamespace ? base.ChildResources : Enumerable.Empty(); + ExtensionParameter = new Parameter( + VariableName, + $"The instance the method will execute against.", + ArmCoreType, + null, + ValidationType.AssertNotNull, + null); + + MethodModifiers = IsArmCore ? + MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual : + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension; + } + + protected override ConstructorSignature? EnsureMockingCtor() + { + return IsArmCore ? null : base.EnsureMockingCtor(); + } + + public override FormattableString BranchIdVariableName => $"{ExtensionParameter.Name}.Id"; + + public Parameter ExtensionParameter { get; } + + protected virtual string VariableName => Configuration.MgmtConfiguration.IsArmCore ? "this" : ArmCoreType.Name.ToVariableName(); + + public override CSharpType? BaseType => null; + + public override FormattableString Description { get; } + + public Type ArmCoreType { get; } + + public string ArmCoreNamespace { get; } + + protected override string DefaultName { get; } + + protected override string DefaultNamespace { get; } + + public RequestPath ContextualPath { get; } + + public override IEnumerable ChildResources { get; } + + public virtual bool IsEmpty => !ClientOperations.Any() && !ChildResources.Any(); + + protected internal override CSharpType TypeAsResource => ArmCoreType; + + protected override IEnumerable EnsureFieldDeclaration() + { + yield break; + } + + protected override IEnumerable EnsureClientOperations() + { + var extensionParamToUse = Configuration.MgmtConfiguration.IsArmCore ? null : ExtensionParameter; + return _allRawOperations.Select(operation => + { + var operationName = GetOperationName(operation, ResourceName); + // TODO -- these logic needs a thorough refactor -- the values MgmtRestOperation consumes here are actually coupled together + // some of the values are calculated multiple times (here and in writers). + // we just leave this implementation here since it could work for now + return MgmtClientOperation.FromOperation( + new MgmtRestOperation( + operation, + operation.GetRequestPath(), + ContextualPath, + operationName, + propertyBagName: ResourceName), + BranchIdVariableName, + extensionParamToUse); + }); + } + + public string GetOperationName(InputOperation operation) => GetOperationName(operation, ResourceName); + + protected override string CalculateOperationName(InputOperation operation, string clientResourceName) + { + var operationName = base.CalculateOperationName(operation, clientResourceName); + + if (MgmtContext.Library.GetRestClientMethod(operation).IsListMethod(out var itemType) && itemType is { IsFrameworkType: false, Implementation: ResourceData data }) + { + var requestPath = operation.GetRequestPath(); + // we need to find the correct resource type that links with this resource data + var resource = FindResourceFromResourceData(data, requestPath); + if (resource != null) + { + var extraLayers = GetExtraLayers(requestPath, resource); + if (!extraLayers.Any()) + return $"Get{resource.ResourceName.ResourceNameToPlural()}"; + var suffix = string.Join("", extraLayers.Select(segment => segment.ConstantValue.FirstCharToUpperCase().LastWordToSingular())); + return $"Get{resource.ResourceName.ResourceNameToPlural()}By{suffix}"; + } + } + + return operationName; + } + + private IEnumerable GetExtraLayers(RequestPath requestPath, Resource resource) + { + var rawType = ResourceTypeSegment.ParseRequestPath(requestPath); + var segmentsInResourceType = new HashSet(resource.ResourceType); + // compare and find the new segments in rawType + return rawType.Where(segment => segment.IsConstant && !segmentsInResourceType.Contains(segment)); + } + + // This piece of logic is duplicated in MgmtExtensionWriter, to be refactored + private Resource? FindResourceFromResourceData(ResourceData data, RequestPath requestPath) + { + // we need to find the correct resource type that links with this resource data + var candidates = MgmtContext.Library.FindResources(data); + + // return null when there is no match + if (!candidates.Any()) + return null; + + // when we only find one result, just return it. + if (candidates.Count() == 1) + return candidates.Single(); + + // if there is more candidates than one, we are going to some more matching to see if we could determine one + var resourceType = requestPath.GetResourceType(); + var filteredResources = candidates.Where(resource => resource.ResourceType == resourceType); + + if (filteredResources.Count() == 1) + return filteredResources.Single(); + + return null; + } + + private Method? _mockingExtensionFactoryMethod; + public Method MockingExtensionFactoryMethod => _mockingExtensionFactoryMethod ??= BuildMockableExtensionFactoryMethod(); + + protected virtual Method BuildMockableExtensionFactoryMethod() + { + var signature = new MethodSignature( + MockableExtension.FactoryMethodName, + null, + null, + MethodSignatureModifiers.Private | MethodSignatureModifiers.Static, + MockableExtension.Type, + null, + new[] { _generalExtensionParameter }); + + var extensionVariable = (ValueExpression)_generalExtensionParameter; + var clientVariable = new VariableReference(typeof(ArmClient), "client"); + var body = Snippets.Return( + extensionVariable.Invoke( + nameof(ArmResource.GetCachedClient), + new FuncExpression(new[] { clientVariable.Declaration }, Snippets.New.Instance(MockableExtension.Type, clientVariable, extensionVariable.Property(nameof(ArmResource.Id)))) + )); + + return new(signature, body); + } + + private readonly Parameter _generalExtensionParameter = new Parameter( + "resource", + $"The resource parameters to use in these operations.", + typeof(ArmResource), + null, + ValidationType.AssertNotNull, + null); + + public MgmtMockableExtension MockableExtension => Cache[ArmCoreType]; + + private readonly IEnumerable _mockingExtensions; + + private Dictionary? _cache; + private Dictionary Cache => _cache ??= _mockingExtensions.ToDictionary( + extensionClient => extensionClient.ExtendedResourceType, + extensionClient => extensionClient); + + /// + /// Build the implementation of methods in the extension. + /// It calls the factory method of the mocking extension first, and then calls the method wtih the same name and parameter list on the mocking extension + /// + /// + /// + /// + /// + protected Method BuildRedirectCallToMockingExtension(MethodSignature signature, MethodSignature signatureOnMockingExtension) + { + var callFactoryMethod = new InvokeInstanceMethodExpression(null, (MethodSignature)MockingExtensionFactoryMethod.Signature, new[] { (ValueExpression)signature.Parameters[0] }, false); + + var callMethodOnMockingExtension = callFactoryMethod.Invoke(signatureOnMockingExtension); + + var methodBody = new MethodBodyStatement[] + { + new ParameterValidationBlock(new[] { signature.Parameters[0] }), // we only write validation of the first extension parameter + Snippets.Return(callMethodOnMockingExtension) + }; + + return new(signature, methodBody); + } + + protected FormattableString BuildMockingExtraDescription(MethodSignature signatureOnMockingExtension) + { + FormattableString methodRef = $"{MockableExtension.Type}.{signatureOnMockingExtension.GetCRef()}"; + return $@" +Mocking +To mock this method, please mock {methodRef:C} instead. +"; + } + + protected override Method BuildGetSingletonResourceMethod(Resource resource) + { + var originalMethod = base.BuildGetSingletonResourceMethod(resource); + if (IsArmCore) + return originalMethod; + + // if it not arm core, we should generate these methods in a static extension way + var originalSignature = (MethodSignature)originalMethod.Signature; + var signatureOnMockingExtension = originalSignature with + { + Modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual + }; + var signature = originalSignature with + { + Description = $"{BuildDescriptionForSingletonResource(resource)}{Environment.NewLine}{BuildMockingExtraDescription(signatureOnMockingExtension)}", + Parameters = originalSignature.Parameters.Prepend(ExtensionParameter).ToArray() + }; + + return BuildRedirectCallToMockingExtension(signature, signatureOnMockingExtension); + } + + protected override Method BuildGetChildCollectionMethod(ResourceCollection collection) + { + var originalMethod = base.BuildGetChildCollectionMethod(collection); + if (IsArmCore) + return originalMethod; + + // if it not arm core, we should generate these methods in a static extension way + var originalSignature = (MethodSignature)originalMethod.Signature; + var signatureOnMockingExtension = originalSignature with + { + Modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual + }; + var signature = originalSignature with + { + Description = $"{BuildDescriptionForChildCollection(collection)}{Environment.NewLine}{BuildMockingExtraDescription(signatureOnMockingExtension)}", + Parameters = originalSignature.Parameters.Prepend(ExtensionParameter).ToArray() + }; + + return BuildRedirectCallToMockingExtension(signature, signatureOnMockingExtension); + } + + protected override Method BuildGetChildResourceMethod(ResourceCollection collection, MethodSignatureBase getCollectionMethodSignature, bool isAsync) + { + var originalMethod = base.BuildGetChildResourceMethod(collection, getCollectionMethodSignature, isAsync); + if (IsArmCore) + return originalMethod; + + var originalSignature = (MethodSignature)originalMethod.Signature; + var signatureOnMockingExtension = (originalSignature.WithAsync(false) with + { + Modifiers = MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + Parameters = originalSignature.Parameters.Skip(1).ToArray() + }).WithAsync(isAsync); + var signature = originalSignature with + { + Description = $"{BuildDescriptionForChildResource(collection)}{Environment.NewLine}{BuildMockingExtraDescription(signatureOnMockingExtension)}" + }; + + // if it not arm core, we should generate these methods in a static extension way + return BuildRedirectCallToMockingExtension(signature, signatureOnMockingExtension); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtExtensionBuilder.cs b/logger/autorest.csharp/mgmt/Output/MgmtExtensionBuilder.cs new file mode 100644 index 0000000..77f0e16 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtExtensionBuilder.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Utilities; +using Azure.ResourceManager; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtExtensionBuilder + { + private record MgmtExtensionInfo(IReadOnlyDictionary ExtensionDict, IEnumerable MockableExtensions) + { + private IEnumerable? _extensions; + public IEnumerable Extensions => _extensions ??= ExtensionDict.Values; + + private MgmtExtensionWrapper? _extensionWrapper; + public MgmtExtensionWrapper ExtensionWrapper => _extensionWrapper ??= new MgmtExtensionWrapper(Extensions, MockableExtensions); + } + + private readonly IReadOnlyDictionary> _extensionOperations; + private readonly IReadOnlyDictionary> _armResourceExtensionOperations; + + public MgmtExtensionBuilder(Dictionary> extensionOperations, Dictionary> armResourceExtensionOperations) + { + _extensionOperations = extensionOperations; + _armResourceExtensionOperations = armResourceExtensionOperations; + } + + public MgmtExtensionWrapper ExtensionWrapper => ExtensionInfo.ExtensionWrapper; + + public IEnumerable Extensions => ExtensionInfo.Extensions; + + public IEnumerable MockableExtensions => ExtensionInfo.MockableExtensions; + + public MgmtExtension GetExtension(Type armCoreType) + { + return ExtensionInfo.ExtensionDict[armCoreType]; + } + + private MgmtExtensionInfo? _info; + private MgmtExtensionInfo ExtensionInfo => _info ??= EnsureMgmtExtensionInfo(); + + private MgmtExtensionInfo EnsureMgmtExtensionInfo() + { + // we use a SortedDictionary or SortedSet here to make sure the order of extensions or extension clients is deterministic + var extensionDict = new SortedDictionary(new CSharpTypeNameComparer()); + var mockingExtensions = new SortedSet(new MgmtExtensionClientComparer()); + // create the extensions + foreach (var (type, operations) in _extensionOperations) + { + var extension = new MgmtExtension(operations, mockingExtensions, type); + extensionDict.Add(type, extension); + } + // add ArmResourceExtension methods + var armResourceExtension = new ArmResourceExtension(_armResourceExtensionOperations, mockingExtensions); + // add ArmClientExtension methods (which is also the TenantResource extension methods) + var armClientExtension = new ArmClientExtension(_armResourceExtensionOperations, mockingExtensions, armResourceExtension); + extensionDict.Add(typeof(ArmResource), armResourceExtension); + extensionDict.Add(typeof(ArmClient), armClientExtension); + + // construct all possible extension clients + // first we collection all possible combinations of the resource on operations + var resourceToOperationsDict = new Dictionary>(); + foreach (var (type, extension) in extensionDict) + { + // we add an empty list for the type to ensure that the corresponding extension client will always be constructed, even empty + resourceToOperationsDict.Add(type, new()); + foreach (var operation in extension.AllOperations) + { + resourceToOperationsDict.AddInList(type, operation); + } + } + // then we construct the extension clients + foreach (var (resourceType, operations) in resourceToOperationsDict) + { + // find the extension if the resource type here is a framework type (when it is ResourceGroupResource, SubscriptionResource, etc) to ensure the ExtensionClient could property have the child resources + extensionDict.TryGetValue(resourceType, out var extensionForChildResources); + var extensionClient = resourceType.Equals(typeof(ArmClient)) ? + new MgmtMockableArmClient(resourceType, operations, extensionForChildResources) : + new MgmtMockableExtension(resourceType, operations, extensionForChildResources); + mockingExtensions.Add(extensionClient); + } + + return new(extensionDict, mockingExtensions); + } + + private struct CSharpTypeNameComparer : IComparer + { + public int Compare(CSharpType? x, CSharpType? y) + { + return string.Compare(x?.Name, y?.Name); + } + } + + private struct MgmtExtensionClientComparer : IComparer + { + public int Compare(MgmtMockableExtension? x, MgmtMockableExtension? y) + { + return string.Compare(x?.Declaration.Name, y?.Declaration.Name); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtExtensionWrapper.cs b/logger/autorest.csharp/mgmt/Output/MgmtExtensionWrapper.cs new file mode 100644 index 0000000..cc1cd42 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtExtensionWrapper.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Models; + +namespace AutoRest.CSharp.Mgmt.Output +{ + /// + /// MgmtExtensionsWrapper is a wrapper of all the , despite the is inheriting from , currently it will not produce a class in the generated code. + /// In ArmCore, we will not use this class because Azure.ResourceManager does not need to generate extension classes, we just need to generate partial classes to extend them because those "to be extended" types are defined in Azure.ResourceManager. + /// In other packages, we need this TypeProvider to generate one big extension class that contains all the extension methods. + /// + internal class MgmtExtensionWrapper : MgmtTypeProvider + { + public IEnumerable Extensions { get; } + + public IEnumerable MockingExtensions { get; } + + public override bool IsStatic => true; + + public bool IsEmpty => Extensions.All(extension => extension.IsEmpty); + + public MgmtExtensionWrapper(IEnumerable extensions, IEnumerable mockingExtensions) : base(MgmtContext.RPName) + { + DefaultName = $"{ResourceName}Extensions"; + Description = Configuration.MgmtConfiguration.IsArmCore ? (FormattableString)$"" : $"A class to add extension methods to {MgmtContext.Context.DefaultNamespace}."; + Extensions = extensions; + MockingExtensions = mockingExtensions; + } + + public override CSharpType? BaseType => null; + + public override FormattableString Description { get; } + + protected override string DefaultName { get; } + + protected override string DefaultAccessibility => "public"; + + protected override IEnumerable EnsureClientOperations() + { + foreach (var extension in Extensions) + { + foreach (var operation in extension.ClientOperations) + { + yield return operation; + } + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtMockableArmClient.cs b/logger/autorest.csharp/mgmt/Output/MgmtMockableArmClient.cs new file mode 100644 index 0000000..a0e70cc --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtMockableArmClient.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions.Azure; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using Azure.Core; +using static AutoRest.CSharp.Common.Output.Models.Snippets; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtMockableArmClient : MgmtMockableExtension + { + public MgmtMockableArmClient(CSharpType resourceType, IEnumerable operations, MgmtExtension? extensionForChildResources) : base(resourceType, operations, extensionForChildResources) + { + } + + public override FormattableString IdVariableName => $"scope"; + + public override FormattableString BranchIdVariableName => $"scope"; + + public override bool IsEmpty => !ClientOperations.Any() && !MgmtContext.Library.ArmResources.Any(); + + protected override Method BuildGetSingletonResourceMethod(Resource resource) + { + var originalMethod = base.BuildGetSingletonResourceMethod(resource); + if (IsArmCore) + return originalMethod; + + // we need to add a scope parameter inside the method signature + var originalSignature = (MethodSignature)originalMethod.Signature; + var signature = originalSignature with + { + Parameters = originalSignature.Parameters.Prepend(_scopeParameter).ToArray() + }; + + var scopeTypes = resource.RequestPath.GetParameterizedScopeResourceTypes(); + var methodBody = new MethodBodyStatement[] + { + BuildScopeResourceTypeValidations(scopeTypes), + Snippets.Return( + Snippets.New.Instance(resource.Type, ArmClientProperty, resource.SingletonResourceIdSuffix!.BuildResourceIdentifier(_scopeParameter))) + }; + + return new(signature, methodBody); + } + + protected override Method BuildGetChildCollectionMethod(ResourceCollection collection) + { + var originalMethod = base.BuildGetChildCollectionMethod(collection); + if (IsArmCore) + return originalMethod; + + // we need to add a scope parameter inside the method signature + var originalSignature = (MethodSignature)originalMethod.Signature; + var signature = originalSignature with + { + Parameters = originalSignature.Parameters.Prepend(_scopeParameter).ToArray() + }; + + var scopeTypes = collection.RequestPath.GetParameterizedScopeResourceTypes(); + var ctorArguments = new List + { + ArmClientProperty, + _scopeParameter + }; + ctorArguments.AddRange(originalSignature.Parameters.Select(p => (ValueExpression)p)); + var methodBody = new MethodBodyStatement[] + { + BuildScopeResourceTypeValidations(scopeTypes), + Snippets.Return( + Snippets.New.Instance(collection.Type, ctorArguments.ToArray())) + }; + + return new(signature, methodBody); + } + + private MethodBodyStatement BuildScopeResourceTypeValidations(ResourceTypeSegment[]? scopeTypes) + { + if (scopeTypes is null || !scopeTypes.Any() || scopeTypes.Contains(ResourceTypeSegment.Any)) + { + return EmptyStatement; + } + + var resourceTypeVariable = new ResourceIdentifierExpression(_scopeParameter).ResourceType; + var conditions = new List(); + var resourceTypeStrings = new List(); + foreach (var type in scopeTypes) + { + // here we have an expression of `!scope.ResourceType.Equals("")` + conditions.Add(Not(resourceTypeVariable.InvokeEquals(Literal(type.ToString())))); + resourceTypeStrings.Add($"{type}"); + } + + // here we aggregate them together using or operator || and get: scope.ResourceType.Equals("") || scope.ResourceType.Equals("") + var condition = conditions.Aggregate((a, b) => a.Or(b)); + var errorMessageFormat = $"Invalid resource type {{0}}, expected {resourceTypeStrings.Join(", ", " or ")}"; + return new IfStatement(condition) + { + Throw(New.Instance(typeof(ArgumentException), new InvokeStaticMethodExpression(typeof(string), nameof(string.Format), new ValueExpression[] { Literal(errorMessageFormat), resourceTypeVariable }))) + }; + } + + private IEnumerable? _armResourceMethods; + public IEnumerable ArmResourceMethods => _armResourceMethods ??= BuildArmResourceMethods(); + + private IEnumerable BuildArmResourceMethods() + { + foreach (var resource in MgmtContext.Library.ArmResources) + { + yield return BuildArmResourceMethod(resource); + } + } + + private Method BuildArmResourceMethod(Resource resource) + { + var lines = new List(); + string an = resource.Type.Name.StartsWithVowel() ? "an" : "a"; + lines.Add($"Gets an object representing {an} {resource.Type:C} along with the instance operations that can be performed on it but with no data."); + lines.Add($"You can use to create {an} {resource.Type:C} {typeof(ResourceIdentifier):C} from its components."); + var description = FormattableStringHelpers.Join(lines, Environment.NewLine); + + var parameters = new List + { + _resourceIdParameter + }; + + var signature = new MethodSignature( + $"Get{resource.Type.Name}", + null, + description, + MethodModifiers, + resource.Type, + $"Returns a {resource.Type:C} object.", + parameters); + + var methodBody = new MethodBodyStatement[]{ + new InvokeStaticMethodStatement(resource.Type, "ValidateResourceId", _resourceIdParameter), + Snippets.Return(Snippets.New.Instance(resource.Type, ArmClientProperty, _resourceIdParameter)) + }; + + return new(signature, methodBody); + } + + private readonly Parameter _resourceIdParameter = new( + Name: "id", + Description: $"The resource ID of the resource to get.", + Type: typeof(ResourceIdentifier), + DefaultValue: null, + Validation: ValidationType.None, + Initializer: null); + + private readonly Parameter _scopeParameter = new Parameter( + Name: "scope", + Description: $"The scope that the resource will apply against.", + Type: typeof(ResourceIdentifier), + DefaultValue: null, + Validation: ValidationType.None, + Initializer: null); + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtMockableExtension.cs b/logger/autorest.csharp/mgmt/Output/MgmtMockableExtension.cs new file mode 100644 index 0000000..b7a57b4 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtMockableExtension.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.ResourceManager; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtMockableExtension : MgmtTypeProvider + { + private readonly IEnumerable _operations; + private readonly MgmtExtension? _extensionForChildResources; + + public MgmtMockableExtension(CSharpType resourceType, IEnumerable operations, MgmtExtension? extensionForChildResources) + : base(resourceType.Name) + { + _operations = operations; + _extensionForChildResources = extensionForChildResources; + ExtendedResourceType = resourceType; + // name of this class is like "MockableComputeResourceGroupResource" + DefaultName = GetMockableExtensionDefaultName(resourceType.Name); + } + + internal static string GetMockableExtensionDefaultName(string resourceName) + => $"Mockable{MgmtContext.RPName}{resourceName}"; + + public override bool IsInitializedByProperties => true; + + protected override ConstructorSignature? EnsureArmClientCtor() + { + return new ConstructorSignature( + Type, + Summary: null, + Description: $"Initializes a new instance of the {Type:C} class.", + Modifiers: Internal, + Parameters: new[] { KnownParameters.ArmClient, ResourceIdentifierParameter }, + Initializer: new( + isBase: true, + arguments: new[] { KnownParameters.ArmClient, ResourceIdentifierParameter })); + } + + private string? _factoryMethodName; + public string FactoryMethodName => _factoryMethodName ??= $"Get{Declaration.Name}"; + + protected override IEnumerable EnsureClientOperations() + { + // here we have to capsulate the MgmtClientOperation again to remove the extra "extension parameter" we added when constructing them in MgmtExtension.EnsureClientOperations + // and here we need to regroup these MgmtClientOperations when they cannot be overloard of each other + var operationDict = new Dictionary>(); + foreach (var operation in _operations) + { + operationDict.AddInList(GetMgmtClientOperationKey(operation.MethodSignature), operation); + } + + foreach (var (_, operations) in operationDict) + { + yield return MgmtClientOperation.FromOperations(operations.SelectMany(clientOperation => clientOperation).ToList(), IdVariableName)!; + } + } + + public override ResourceTypeSegment GetBranchResourceType(RequestPath branch) + { + return branch.GetResourceType(); + } + + public CSharpType ExtendedResourceType { get; } + + public override CSharpType? BaseType => typeof(ArmResource); + + protected override string DefaultName { get; } + protected override string DefaultNamespace => $"{base.DefaultNamespace}.Mocking"; + + public override string DiagnosticNamespace => base.DefaultNamespace; + + public virtual bool IsEmpty => !ClientOperations.Any() && !ChildResources.Any(); + + public override IEnumerable ChildResources => _extensionForChildResources?.ChildResources ?? Enumerable.Empty(); + + private FormattableString? _description; + public override FormattableString Description => _description ??= $"A class to add extension methods to {ResourceName}."; + + protected override string DefaultAccessibility => "public"; + + /// + /// Construct a key for overload of this method signature. + /// The format of this key is like "MethodName(TypeOfParameter1,TypeOfParameter2,...,TypeOfLastParameter)" + /// + /// + /// + private static string GetMgmtClientOperationKey(MethodSignature methodSignature) + { + var builder = new StringBuilder(); + builder.Append(methodSignature.Name).Append("("); + // all methods here should be extension methods, therefore we skip the first parameter which is the extension method parameter "this" and in this context, it is actually myself + builder.AppendJoin(',', methodSignature.Parameters.Skip(1).Select(p => p.Type.Name)); + builder.Append(")"); + + return builder.ToString(); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtObjectType.cs b/logger/autorest.csharp/mgmt/Output/MgmtObjectType.cs new file mode 100644 index 0000000..1fdcb24 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtObjectType.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtObjectType : SchemaObjectType + { + private IReadOnlyList? _myProperties; + + public MgmtObjectType(InputModelType inputModel, SerializableObjectType? defaultDerivedType = null) + : base(inputModel, Configuration.Namespace, MgmtContext.TypeFactory, MgmtContext.Context.SourceInputModel, defaultDerivedType) + { + } + + protected virtual bool IsResourceType => false; + private string? _defaultName; + protected override string DefaultName => _defaultName ??= GetDefaultName(InputModel, IsResourceType); + private string? _defaultNamespace; + protected override string DefaultNamespace => _defaultNamespace ??= GetDefaultNamespace(MgmtContext.Context, IsResourceType); + + internal IReadOnlyList MyProperties => _myProperties ??= BuildMyProperties().ToArray(); + + private static string GetDefaultName(InputModelType inputModel, bool isResourceType) + { + var name = inputModel.CSharpName(); + return isResourceType ? name + "Data" : name; + } + + private static string GetDefaultNamespace(BuildContext context, bool isResourceType) + { + return isResourceType ? context.DefaultNamespace : GetDefaultModelNamespace(null, context.DefaultNamespace); + } + + private HashSet GetParentPropertyNames() + { + return EnumerateHierarchy() + .Skip(1) + .SelectMany(type => type.Properties) + .Select(p => p.Declaration.Name) + .ToHashSet(); + } + + protected override IEnumerable BuildProperties() + { + var parentProperties = GetParentPropertyNames(); + foreach (var property in base.BuildProperties()) + { + if (!parentProperties.Contains(property.Declaration.Name)) + { + var propertyType = CreatePropertyType(property); + + // check if the type of this property is "single property type" + if (IsSinglePropertyObject(propertyType)) + { + propertyType = propertyType.MarkFlatten(); + } + yield return propertyType; + } + } + } + + private static bool IsSinglePropertyObject(ObjectTypeProperty property) + { + if (property.Declaration.Type is not { IsFrameworkType: false, Implementation: ObjectType objType }) + return false; + + return objType switch + { + SystemObjectType systemObjectType => HandleSystemObjectType(systemObjectType), + SchemaObjectType schemaObjectType => HandleMgmtObjectType(schemaObjectType), + _ => throw new InvalidOperationException($"Unhandled case {objType.GetType()} for property {property.Declaration.Type} {property.Declaration.Name}") + }; + } + + private static bool HandleMgmtObjectType(SchemaObjectType objType) + { + if (objType.Discriminator != null) + return false; + + if (objType.AdditionalPropertiesProperty != null) + return false; + + // we cannot use the EnumerateHierarchy method because we are calling this when we are building that + var properties = objType.InputModel.GetSelfAndBaseModels().SelectMany(obj => obj.Properties).Select(x => x.Name).Distinct(); + return properties.Count() == 1; + } + + private static bool HandleSystemObjectType(SystemObjectType objType) + { + var properties = objType.EnumerateHierarchy().SelectMany(obj => obj.Properties).ToArray(); + + // only bother flattening if the single property is public + return properties.Length == 1 && properties[0].Declaration.Accessibility == "public"; + } + + private IEnumerable BuildMyProperties() + { + foreach (var model in InputModel.GetSelfAndBaseModels()) + { + foreach (var property in model.Properties) + { + yield return CreateProperty(property); + } + } + } + + protected virtual ObjectTypeProperty CreatePropertyType(ObjectTypeProperty objectTypeProperty) + { + var type = objectTypeProperty.ValueType; + if (type is { IsFrameworkType: true, FrameworkType: { } frameworkType } && frameworkType.IsGenericType) + { + var arguments = new CSharpType[type.Arguments.Count]; + bool shouldReplace = false; + for (int i = 0; i < type.Arguments.Count; i++) + { + var argType = type.Arguments[i]; + arguments[i] = argType; + if (argType is { IsFrameworkType: false, Implementation: MgmtObjectType typeToReplace }) + { + var match = ReferenceTypePropertyChooser.GetExactMatch(typeToReplace); + if (match != null) + { + shouldReplace = true; + string fullSerializedName = this.GetFullSerializedName(objectTypeProperty, i); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.ReplacePropertyType, fullSerializedName), + fullSerializedName, + "ReplacePropertyType", typeToReplace.Declaration.FullName, $"{match.Namespace}.{match.Name}"); + arguments[i] = match; + } + } + } + if (shouldReplace) + { + var newType = new CSharpType(frameworkType.GetGenericTypeDefinition(), type.IsNullable, arguments); + return new ObjectTypeProperty( + new MemberDeclarationOptions(objectTypeProperty.Declaration.Accessibility, objectTypeProperty.Declaration.Name, newType), + objectTypeProperty.Description, + objectTypeProperty.IsReadOnly, + objectTypeProperty.InputModelProperty + ); + } + return objectTypeProperty; + } + else + { + ObjectTypeProperty property = objectTypeProperty; + if (type is { IsFrameworkType: false, Implementation: MgmtObjectType typeToReplace }) + { + var match = ReferenceTypePropertyChooser.GetExactMatch(typeToReplace); + if (match != null) + { + property = ReferenceTypePropertyChooser.GetObjectTypeProperty(objectTypeProperty, match); + string fullSerializedName = this.GetFullSerializedName(objectTypeProperty); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.ReplacePropertyType, fullSerializedName), + fullSerializedName, + "ReplacePropertyType", typeToReplace.Declaration.FullName, $"{match.Namespace}.{match.Name}"); + } + } + return property; + } + } + + /// + /// Check whether this type should be replaced when used as property type. + /// + /// true if this type should NOT be replaced when used as property type; false elsewise + public bool ShouldNotReplaceForProperty() + { + return Configuration.MgmtConfiguration.NoPropertyTypeReplacement.Contains(this.Type.Name); + } + + private bool IsDescendantOf(SchemaObjectType schemaObjectType) + { + if (schemaObjectType.Discriminator == null) + return false; + var descendantTypes = schemaObjectType.Discriminator.Implementations.Select(implementation => implementation.Type).ToHashSet(); + + // We need this redundant check as the internal backing schema will not be a part of the discriminator implementations of its base type. + if (InputModel.IsUnknownDiscriminatorModel && InputModel.BaseModel == schemaObjectType.InputModel) + { + descendantTypes.Add(Type); + } + + return descendantTypes.Contains(Type); + } + + private static bool ShouldIncludeArmCoreType(Type type) + { + return SystemObjectType.TryGetCtor(type, ReferenceClassFinder.InitializationCtorAttributeName, out _); + } + + protected override CSharpType? CreateInheritedType() + { + // find from the customized code to see if we already have this type defined with a base class + if (ExistingType != null && ExistingType.BaseType != null) + { + // if this type is defined with a base class, we have to use the same base class here + // otherwise the compiler will throw an error + if (MgmtContext.Context.TypeFactory.TryCreateType(ExistingType.BaseType, ShouldIncludeArmCoreType, out var existingBaseType)) + { + // if we could find a type and it is not a framework type meaning that it is a TypeProvider, return that + if (!existingBaseType.IsFrameworkType) + return existingBaseType; + // if it is a framework type, first we check if it is System.Object. Since it is base type for everything, we would not want it to override anything in our code + if (!existingBaseType.Equals(typeof(object))) + { + // we cannot directly return the FrameworkType here, we need to wrap it inside the SystemObjectType + // in order to let the constructor builder have the ability to get base constructor + return CSharpType.FromSystemType(MgmtContext.Context, existingBaseType.FrameworkType); + } + } + // if we did not find that type, this means the customization code is referencing something unrecognized + // or the customization code is not specifying a base type + } + + CSharpType? inheritedType = base.CreateInheritedType(); + if (inheritedType != null) + { + if (inheritedType.IsFrameworkType) + return inheritedType; + else + { + // if the base type is a TypeProvider, we need to make sure if it is a discriminator provider + // by checking if this type is one of its descendants + if (inheritedType is { IsFrameworkType: false, Implementation: SchemaObjectType schemaObjectType } && IsDescendantOf(schemaObjectType)) + { + // if the base type has a discriminator and this type is one of them + return inheritedType; + } + } + } + + // try to replace the base type if this is not a type from discriminator + // try exact match first + var typeToReplace = inheritedType?.Implementation as MgmtObjectType; + if (typeToReplace != null) + { + var match = InheritanceChooser.GetExactMatch(typeToReplace, typeToReplace.MyProperties); + if (match != null) + { + string fullSerializedName = this.GetFullSerializedName(); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.ReplaceBaseType, fullSerializedName), + fullSerializedName, + "ReplaceBaseTypeByExactMatch", inheritedType?.GetNameForReport() ?? "", match.GetNameForReport()); + inheritedType = match; + } + } + + // try superset match because our superset match is checking the proper superset + var supersetBaseType = InheritanceChooser.GetSupersetMatch(this, MyProperties); + if (supersetBaseType != null) + { + string fullSerializedName = this.GetFullSerializedName(); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.ReplaceBaseType, fullSerializedName), + fullSerializedName, + "ReplaceBaseTypeBySupersetMatch", inheritedType?.GetNameForReport() ?? "", supersetBaseType.GetNameForReport()); + inheritedType = supersetBaseType; + } + + return inheritedType; + } + + protected CSharpType? CreateInheritedTypeWithNoExtraMatch() + { + return base.CreateInheritedType(); + } + + public override ObjectTypeProperty GetPropertyForSchemaProperty(InputModelProperty property, bool includeParents = false) + { + if (!TryGetPropertyForSchemaProperty(p => p.InputModelProperty == property, out ObjectTypeProperty? objectProperty, includeParents)) + { + if (Inherits?.Implementation is SystemObjectType) + { + return GetPropertyBySerializedName(property.SerializedName, includeParents); + } + throw new InvalidOperationException($"Unable to find object property for schema property '{property.SerializedName}' in schema {DefaultName}"); + } + + return objectProperty; + } + + protected override FormattableString CreateDescription() + { + return $"{InputModel.Description}"; + } + + internal string GetFullSerializedName() + { + return this.InputModel.GetFullSerializedName(); + } + + internal string GetFullSerializedName(InputModelProperty property) + { + var parentSchema = InputModel.GetSelfAndBaseModels().FirstOrDefault(s => s.Properties.Contains(property)); + if (parentSchema == null) + { + throw new InvalidOperationException($"Can't find parent object schema for property schema: '{this.Declaration.Name}.{property.CSharpName()}'"); + } + else + { + return parentSchema.GetFullSerializedName(property); + } + } + + internal string GetFullSerializedName(ObjectTypeProperty otProperty) + { + if (otProperty.InputModelProperty != null) + return this.GetFullSerializedName(otProperty.InputModelProperty); + else + return $"{this.GetFullSerializedName()}.{otProperty.Declaration.Name}"; + } + + internal string GetFullSerializedName(ObjectTypeProperty otProperty, int argumentIndex) + { + if (otProperty.ValueType.Arguments == null || otProperty.ValueType.Arguments.Count <= argumentIndex) + throw new ArgumentException("argumentIndex out of range"); + var argType = otProperty.ValueType.Arguments[argumentIndex]; + return $"{this.GetFullSerializedName(otProperty)}.Arguments[{argumentIndex}-{argType.Namespace}.{argType.Name}]"; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtReferenceType.cs b/logger/autorest.csharp/mgmt/Output/MgmtReferenceType.cs new file mode 100644 index 0000000..7133d94 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtReferenceType.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtReferenceType : MgmtObjectType + { + private static HashSet PropertyReferenceTypeModels = new HashSet + { + "Azure.ResourceManager.Models.ArmPlan", + "Azure.ResourceManager.Models.ArmSku", + "Azure.ResourceManager.Models.EncryptionProperties", + "Azure.ResourceManager.Resources.Models.ExtendedLocation", + "Azure.ResourceManager.Models.ManagedServiceIdentity", + "Azure.ResourceManager.Models.KeyVaultProperties", + "Azure.ResourceManager.Resources.ResourceProviderData", + "Azure.ResourceManager.Models.SystemAssignedServiceIdentity", + "Azure.ResourceManager.Models.SystemData", + "Azure.ResourceManager.Models.UserAssignedIdentity", + "Azure.ResourceManager.Resources.Models.WritableSubResource" + }; + + private static HashSet TypeReferenceTypeModels = new HashSet + { + "Azure.ResourceManager.Models.OperationStatusResult" + }; + + private static HashSet ReferenceTypeModels = new HashSet + { + "Azure.ResourceManager.Models.ResourceData", + "Azure.ResourceManager.Models.TrackedResourceData" + }; + + public static bool IsPropertyReferenceType(InputModelType schema) + => PropertyReferenceTypeModels.Contains(schema.CrossLanguageDefinitionId); + + public static bool IsTypeReferenceType(InputModelType schema) + => TypeReferenceTypeModels.Contains(schema.CrossLanguageDefinitionId); + + public static bool IsReferenceType(ObjectType schema) + => ReferenceTypeModels.Contains($"{schema.Declaration.Namespace}.{schema.Declaration.Name}"); + + public static bool IsReferenceType(InputModelType schema) + => ReferenceTypeModels.Contains(schema.CrossLanguageDefinitionId); + + public MgmtReferenceType(InputModelType inputModel) : base(inputModel) + { + JsonConverter = (IsPropertyReferenceType(inputModel) || IsTypeReferenceType(InputModel)) + ? new JsonConverterProvider(this, _sourceInputModel) + : null; + } + + protected override ObjectTypeProperty CreatePropertyType(ObjectTypeProperty objectTypeProperty) + { + if (objectTypeProperty.ValueType != null && objectTypeProperty.ValueType.IsFrameworkType) + { + var newProperty = ReferenceTypePropertyChooser.GetExactMatchForReferenceType(objectTypeProperty, objectTypeProperty.ValueType.FrameworkType); + if (newProperty != null) + { + string fullSerializedName = GetFullSerializedName(objectTypeProperty); + MgmtReport.Instance.TransformSection.AddTransformLogForApplyChange( + new TransformItem(TransformTypeName.ReplacePropertyType, fullSerializedName), + fullSerializedName, + "ReplacePropertyType", objectTypeProperty.Declaration.Type.ToString(), newProperty.Declaration.Type.ToString()); + return newProperty; + } + } + + return objectTypeProperty; + } + + protected override CSharpType? CreateInheritedType() => base.CreateInheritedType(); + + // the reference types do not need raw data field + public override ObjectTypeProperty? RawDataField => null; + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtRestClient.cs b/logger/autorest.csharp/mgmt/Output/MgmtRestClient.cs new file mode 100644 index 0000000..d872763 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtRestClient.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class MgmtRestClient : CmcRestClient + { + public static readonly Parameter ApplicationIdParameter = new("applicationId", $"The application id to use for user agent", new CSharpType(typeof(string)), null, ValidationType.None, null); + + private readonly MgmtRestClientBuilder _clientBuilder; + private IReadOnlyList? _resources; + + public ClientFields Fields { get; } + + public MgmtRestClient(InputClient inputClient, MgmtRestClientBuilder clientBuilder) + : base(inputClient, MgmtContext.Context, inputClient.Name, GetOrderedParameters(clientBuilder)) + { + _clientBuilder = clientBuilder; + Fields = ClientFields.CreateForRestClient(new[] { KnownParameters.Pipeline }.Union(clientBuilder.GetOrderedParametersByRequired())); + } + + protected override Dictionary EnsureNormalMethods() + { + var requestMethods = new Dictionary(ReferenceEqualityComparer.Instance); + + foreach (var operation in InputClient.Operations) + { + requestMethods.Add(operation, _clientBuilder.BuildMethod(operation, operation.Parameters, InputClient.Parameters, null, "public", ShouldReturnNullOn404(operation))); + + } + + return requestMethods; + } + + private static Func ShouldReturnNullOn404(InputOperation operation) + { + Func f = delegate (string? responseBodyType) + { + if (!MgmtContext.Library.TryGetResourceData(operation.GetHttpPath(), out var resourceData)) + return false; + if (!operation.IsGetResourceOperation(responseBodyType, resourceData)) + return false; + + return operation.Responses.Any(r => r.BodyType == resourceData.InputModel); + }; + return f; + } + + public IReadOnlyList Resources => _resources ??= GetResources(); + + private IReadOnlyList GetResources() + { + HashSet candidates = new HashSet(); + foreach (var operation in InputClient.Operations) + { + foreach (var resource in operation.GetResourceFromResourceType()) + { + candidates.Add(resource); + } + } + return candidates.ToList(); + } + + private static IReadOnlyList GetOrderedParameters(CmcRestClientBuilder clientBuilder) + => new[] {KnownParameters.Pipeline, ApplicationIdParameter}.Union(clientBuilder.GetOrderedParametersByRequired()).ToArray(); + } +} diff --git a/logger/autorest.csharp/mgmt/Output/MgmtTypeProvider.cs b/logger/autorest.csharp/mgmt/Output/MgmtTypeProvider.cs new file mode 100644 index 0000000..47138c6 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/MgmtTypeProvider.cs @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Output.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using AutoRest.CSharp.Utilities; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.ResourceManager; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Mgmt.Output +{ + /// + /// MgmtTypeProvider represents the information that corresponds to the generated class in the SDK that contains operations in it. + /// This includes , , , , and + /// + internal abstract class MgmtTypeProvider : TypeProvider + { + protected bool IsArmCore { get; } + + protected MgmtTypeProvider(string resourceName) : base(MgmtContext.Context) + { + ResourceName = resourceName; + IsArmCore = Configuration.MgmtConfiguration.IsArmCore; + IsStatic = !IsArmCore && BaseType is null; + ArmClientProperty = new MemberExpression(null, "Client"); // this refers to ArmResource.Client which is protected internal therefore we have to hardcode in plain string here instead of using nameof + IdProperty = new MemberExpression(null, nameof(ArmResource.Id)); + MethodModifiers = Public | Virtual; + } + + protected ValueExpression ArmClientProperty { get; init; } + + protected ValueExpression IdProperty { get; init; } + + protected MethodSignatureModifiers MethodModifiers { get; init; } + + protected virtual FormattableString IdParamDescription => $"The identifier of the resource that is the target of operations."; + public Parameter ResourceIdentifierParameter => new(Name: "id", Description: IdParamDescription, Type: typeof(ResourceIdentifier), DefaultValue: null, ValidationType.None, null); + + public string Accessibility => DefaultAccessibility; + protected override string DefaultAccessibility => "public"; + + public virtual bool CanValidateResourceType => true; + + /// + /// If this is false, all the RestOperation instances will be initialized in the constructor + /// If this is true, all the RestOperation instances will be initialized when the property is invoked for the first time + /// + public virtual bool IsInitializedByProperties => false; + + public virtual bool HasChildResourceGetMethods => true; + + public virtual FormattableString IdVariableName => $"Id"; + + public virtual FormattableString BranchIdVariableName => $"Id"; + + public string Namespace => DefaultNamespace; + + public virtual string DiagnosticNamespace => Namespace; + + public abstract CSharpType? BaseType { get; } + + protected internal virtual CSharpType TypeAsResource => Type; + + private IReadOnlyList? _enumerableInterfaces; + public IEnumerable EnumerableInterfaces => _enumerableInterfaces ??= EnsureGetInterfaces(); + protected virtual IReadOnlyList EnsureGetInterfaces() + { + return Array.Empty(); + } + + public IEnumerable GetImplements() + { + if (BaseType is not null) + yield return BaseType; + + foreach (var type in EnumerableInterfaces) + { + yield return type; + } + } + public virtual bool IsStatic { get; } + + public abstract FormattableString Description { get; } + + private HashSet? _uniqueSets; + public HashSet UniqueSets => _uniqueSets ??= EnsureUniqueSets(); + + public virtual Resource? DefaultResource { get; } = null; + + private IEnumerable? _extraConstructorParameters; + public IEnumerable ExtraConstructorParameters => _extraConstructorParameters ??= EnsureExtraCtorParameters(); + protected virtual IEnumerable EnsureExtraCtorParameters() => new List(); + + protected virtual FieldModifiers FieldModifiers { get; } = FieldModifiers.Private; + + private IEnumerable? _fields; + public IEnumerable Fields => _fields ??= EnsureFieldDeclaration(); + protected virtual IEnumerable EnsureFieldDeclaration() + { + foreach (var set in UniqueSets) + { + var nameSet = GetRestDiagNames(set); + yield return new FieldDeclaration( + FieldModifiers, + typeof(ClientDiagnostics), + nameSet.DiagnosticField); + yield return new FieldDeclaration( + FieldModifiers, + set.RestClient.Type, + nameSet.RestField); + } + + var additionalFields = GetAdditionalFields(); + foreach (var field in additionalFields) + { + yield return field; + } + } + + protected virtual IEnumerable GetAdditionalFields() => Enumerable.Empty(); + + private ConstructorSignature? _mockingCtor; + public ConstructorSignature? MockingCtor => _mockingCtor ??= EnsureMockingCtor(); + protected virtual ConstructorSignature? EnsureMockingCtor() + { + return new ConstructorSignature( + Type, + null, + Description: $"Initializes a new instance of the {Type:C} class for mocking.", + Modifiers: Protected, + Parameters: Array.Empty()); + } + + private ConstructorSignature? _armClientCtor; + public ConstructorSignature? ArmClientCtor => _armClientCtor ??= EnsureArmClientCtor(); + protected virtual ConstructorSignature? EnsureArmClientCtor() => null; + private ConstructorSignature? _resourceDataCtor; + public ConstructorSignature? ResourceDataCtor => _resourceDataCtor ??= EnsureResourceDataCtor(); + protected virtual ConstructorSignature? EnsureResourceDataCtor() => null; + + private Dictionary _nameCache = new Dictionary(); + public NameSet GetRestDiagNames(NameSetKey set) + { + if (_nameCache.TryGetValue(set, out NameSet names)) + return names; + + var resource = set.Resource; + var client = set.RestClient; + string? resourceName = resource is not null ? resource.ResourceName : client.Resources.Contains(DefaultResource) ? DefaultResource?.ResourceName : null; + + string uniqueName = GetUniqueName(resourceName, client.InputClient.Key); + + string uniqueVariable = uniqueName.ToVariableName(); + var result = new NameSet( + $"_{uniqueVariable}ClientDiagnostics", + $"{uniqueName}ClientDiagnostics", + $"_{uniqueVariable}RestClient", + $"{uniqueName}RestClient", + $"{uniqueVariable}ApiVersion"); + _nameCache.Add(set, result); + + return result; + } + + private string GetUniqueName(string? resourceName, string clientName) + { + if (resourceName is not null) + { + if (string.IsNullOrEmpty(clientName)) + { + return resourceName; + } + else + { + var singularClientName = clientName.ToSingular(true); + return resourceName.Equals(singularClientName, StringComparison.Ordinal) + ? resourceName + : $"{resourceName}{clientName}"; + } + } + else + { + return string.IsNullOrEmpty(clientName) ? "Default" : clientName; + } + } + + /// + /// This is the display name for this TypeProvider. + /// If this TypeProvider generates an extension class, this will be the resource name of whatever it extends from. + /// + public string ResourceName { get; } + + private IEnumerable? _clientOperations; + /// + /// The collection of operations that will be included in this generated class. + /// + public IEnumerable ClientOperations => _clientOperations ??= EnsureClientOperations(); + protected abstract IEnumerable EnsureClientOperations(); + + private IEnumerable? _allOperations; + public IEnumerable AllOperations => _allOperations ??= EnsureAllOperations(); + protected virtual IEnumerable EnsureAllOperations() => ClientOperations; + + public virtual ResourceTypeSegment GetBranchResourceType(RequestPath branch) + { + throw new InvalidOperationException($"Tried to get a branch resource type from a type provider that doesn't support in {GetType().Name}."); + } + + private IEnumerable? _childResources; + /// + /// The collection of that is a child of this generated class. + /// + public virtual IEnumerable ChildResources => _childResources ??= MgmtContext.Library.ArmResources.Where(resource => resource.GetParents().Contains(this)); + + protected string GetOperationName(InputOperation operation, string clientResourceName) + { + // search the configuration for a override of this operation + if (operation.TryGetConfigOperationName(out var name)) + return name; + + return CalculateOperationName(operation, clientResourceName); + } + + /// + /// If the operationGroup of this operation is the same as the resource operation group, we just use operation.CSharpName() + /// If it is not, we append the operation group key after operation.CSharpName() to make sure this operation has an unique name. + /// + /// + /// For extension classes, use the ResourceName. For resources, use its operation group name + /// + protected virtual string CalculateOperationName(InputOperation operation, string clientResourceName) + { + // search the configuration for a override of this operation + if (operation.TryGetConfigOperationName(out var name)) + return name; + + var operationGroup = MgmtContext.Library.GetRestClient(operation).InputClient; + var ogKey = operationGroup.Key; + var singularOGKey = ogKey.LastWordToSingular(); + if (ogKey == clientResourceName || singularOGKey == clientResourceName) + { + return operation.MgmtCSharpName(false); + } + + var resourceName = string.Empty; + if (MgmtContext.Library.GetRestClientMethod(operation).IsListMethod(out _)) + { + resourceName = ogKey.IsNullOrEmpty() ? string.Empty : singularOGKey.ResourceNameToPlural(); + var opName = operation.MgmtCSharpName(!resourceName.IsNullOrEmpty()); + // Remove 'By[Resource]' if the method is put in the [Resource] class. For instance, GetByDatabaseDatabaseColumns now becomes GetDatabaseColumns under Database resource class. + if (opName.EndsWith($"By{clientResourceName.LastWordToSingular()}")) + { + opName = opName.Substring(0, opName.IndexOf($"By{clientResourceName.LastWordToSingular()}")); + } + // For other variants, move By[Resource] to the end. For instance, GetByInstanceServerTrustGroups becomes GetServerTrustGroupsByInstance. + else if (opName.StartsWith("GetBy") && opName.SplitByCamelCase().ToList()[1] == "By") + { + return $"Get{resourceName}{opName.Substring(opName.IndexOf("By"))}"; + } + return $"{opName}{resourceName}"; + } + resourceName = ogKey.IsNullOrEmpty() ? string.Empty : singularOGKey; + return $"{operation.MgmtCSharpName(!resourceName.IsNullOrEmpty())}{resourceName}"; + } + + private HashSet EnsureUniqueSets() + { + HashSet uniqueSets = new HashSet(); + foreach (var operation in AllOperations) + { + Resource? resource = operation.Resource; + if (resource is null && operation.RestClient.Resources.Contains(DefaultResource)) + resource = DefaultResource; + + NameSetKey key = new NameSetKey(operation.RestClient, resource); + if (uniqueSets.Contains(key)) + continue; + uniqueSets.Add(key); + } + return uniqueSets; + } + + private IEnumerable? _childResourceEntryMethods; + public IEnumerable ChildResourceEntryMethods => _childResourceEntryMethods ??= BuildChildResourceEntries(); + + protected virtual IEnumerable BuildChildResourceEntries() + { + foreach (var resource in ChildResources) + { + if (resource.IsSingleton) + yield return BuildGetSingletonResourceMethod(resource); + else if (resource.ResourceCollection is { } collection) + { + var getCollectionMethod = BuildGetChildCollectionMethod(collection); + yield return getCollectionMethod; + + if (HasChildResourceGetMethods) + { + yield return BuildGetChildResourceMethod(collection, getCollectionMethod.Signature, true); + yield return BuildGetChildResourceMethod(collection, getCollectionMethod.Signature, false); + } + } + } + } + + protected FormattableString BuildDescriptionForSingletonResource(Resource resource) + => $"Gets an object representing a {resource.Type.Name} along with the instance operations that can be performed on it in the {ResourceName}."; + + protected virtual Method BuildGetSingletonResourceMethod(Resource resource) + { + var signature = new MethodSignature( + $"Get{resource.ResourceName}", + null, + BuildDescriptionForSingletonResource(resource), + MethodModifiers, + resource.Type, + $"Returns a {resource.Type:C} object.", + Array.Empty()); + var methodBody = Snippets.Return( + Snippets.New.Instance( + resource.Type, + ArmClientProperty, + resource.SingletonResourceIdSuffix!.BuildResourceIdentifier(IdProperty))); + return new(signature, methodBody); + } + + protected FormattableString BuildDescriptionForChildCollection(ResourceCollection collection) + => $"Gets a collection of {collection.Resource.Type.Name.LastWordToPlural()} in the {ResourceName}."; + + protected virtual Method BuildGetChildCollectionMethod(ResourceCollection collection) + { + var resource = collection.Resource; + var signature = new MethodSignature( + $"Get{resource.ResourceName.ResourceNameToPlural()}", + null, + BuildDescriptionForChildCollection(collection), + MethodModifiers, + collection.Type, + $"An object representing collection of {resource.Type.Name.LastWordToPlural()} and their operations over a {resource.Type.Name}.", + collection.ExtraConstructorParameters.ToArray()); + var methodBody = BuildGetResourceCollectionMethodBody(collection); + return new(signature, methodBody); + } + private MethodBodyStatement BuildGetResourceCollectionMethodBody(ResourceCollection collection) + { + // when there are extra ctor parameters, we cannot use the GetCachedClient method to cache the collection instance + if (collection.ExtraConstructorParameters.Any()) + { + var parameters = new List + { + ArmClientProperty, + IdProperty + }; + parameters.AddRange(collection.ExtraConstructorParameters.Select(p => (ValueExpression)p)); + return Snippets.Return( + Snippets.New.Instance( + collection.Type, + parameters.ToArray())); + } + else + { + var clientArgument = new VariableReference(typeof(ArmClient), "client"); + return Snippets.Return( + new InvokeInstanceMethodExpression( + null, + nameof(ArmResource.GetCachedClient), + new[] { + new FuncExpression(new[] { clientArgument.Declaration }, Snippets.New.Instance(collection.Type, clientArgument, IdProperty)) + }, + null, + false)); + } + } + + protected FormattableString BuildDescriptionForChildResource(ResourceCollection collection) + => collection.GetOperation.Description; + + protected virtual Method BuildGetChildResourceMethod(ResourceCollection collection, MethodSignatureBase getCollectionMethodSignature, bool isAsync) + { + var getOperation = collection.GetOperation; + // construct the method signature + var signature = getOperation.MethodSignature with + { + // name after `Get{ResourceName}` + Name = $"Get{collection.Resource.ResourceName}", + Description = BuildDescriptionForChildResource(collection), + Modifiers = MethodModifiers, + // the parameter of this method should be the parameters on the collection's ctor + parameters on the Get method + // also the validations of parameters will be skipped + Parameters = getCollectionMethodSignature.Parameters.Concat(getOperation.MethodSignature.Parameters).ToArray(), + Attributes = getOperation.MethodSignature.Attributes.Append(new CSharpAttribute(typeof(ForwardsClientCallsAttribute))).ToList() + }; + + var callGetCollection = new InvokeInstanceMethodExpression( + null, + getCollectionMethodSignature.Name, + getCollectionMethodSignature.Parameters.Select(p => (ValueExpression)p).ToArray(), + null, + getCollectionMethodSignature.Modifiers.HasFlag(Async)); + var callGetOperation = callGetCollection.Invoke(getOperation.MethodSignature.WithAsync(isAsync)); + var methodBody = Snippets.Return(callGetOperation); + + return new(signature.WithAsync(isAsync), methodBody); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/Models/MgmtPropertyBag.cs b/logger/autorest.csharp/mgmt/Output/Models/MgmtPropertyBag.cs new file mode 100644 index 0000000..58ac678 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/Models/MgmtPropertyBag.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; + +namespace AutoRest.CSharp.Mgmt.Output.Models +{ + internal class MgmtPropertyBag : PropertyBag + { + public MgmtPropertyBag(string name, InputOperation operation) + : base(name) + { + _operation = operation; + _paramsToKeep = Array.Empty(); + } + + private MgmtPropertyBag(string name, InputOperation operation, IEnumerable paramsToKeep) + : this(name, operation) + { + _paramsToKeep = paramsToKeep; + } + + public MgmtPropertyBag WithUpdatedInfo(string name, IEnumerable paramsToKeep) => + new MgmtPropertyBag(name, _operation, paramsToKeep); + + private InputOperation _operation; + + private IEnumerable _paramsToKeep; + + protected override TypeProvider EnsurePackModel() + { + var packModelName = string.IsNullOrEmpty(Name) ? + throw new InvalidOperationException("Not enough information is provided for constructing management plane property bag, please make sure you first call the WithUpdatedInfo method of MgmtPropertyBag to update the property bag before using it.") : + $"{Name}Options"; + var properties = new List(); + foreach (var parameter in _paramsToKeep) + { + var inputParameter = _operation.Parameters.First(p => string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); + var description = !string.IsNullOrEmpty(inputParameter.Description) && parameter.Description is not null ? parameter.Description.ToString() : $"The {parameter.Name}"; + var property = new InputModelProperty(parameter.Name, parameter.Name, description, inputParameter!.Type, parameter.DefaultValue == null ? null : inputParameter.DefaultValue, parameter.DefaultValue == null, false, false); + properties.Add(property); + } + var defaultNamespace = $"{MgmtContext.Context.DefaultNamespace}.Models"; + var propertyBagModel = new InputModelType( + packModelName, + defaultNamespace, + "public", + null, + $"The {packModelName}.", + InputModelTypeUsage.Input, + properties, + null, + Array.Empty(), + null, + null, + new Dictionary(), + null) + { + IsPropertyBag = true + }; + return new ModelTypeProvider(propertyBagModel, defaultNamespace, MgmtContext.Context.SourceInputModel, MgmtContext.Context.TypeFactory); + } + + protected override bool EnsureShouldValidateParameter() + { + if (PackModel is ModelTypeProvider mgmtPackModel) + { + return mgmtPackModel.Properties.Any(p => p.IsRequired); + } + return false; + } + + private FormattableString? GetDefaultValue(Parameter parameter) + { + if (parameter.DefaultValue is { } defaultValue && defaultValue.Value != null) + { + return defaultValue.GetConstantFormattable(); + } + return null; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/Models/NameSet.cs b/logger/autorest.csharp/mgmt/Output/Models/NameSet.cs new file mode 100644 index 0000000..3a575e1 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/Models/NameSet.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.CSharp.Mgmt.Output.Models; + +internal readonly struct NameSet +{ + public string DiagnosticField { get; } + public string DiagnosticProperty { get; } + public string RestField { get; } + public string RestProperty { get; } + public string ApiVersionVariable { get; } + + public NameSet(string diagField, string diagProperty, string restField, string restProperty, string apiVariable) + { + DiagnosticField = diagField; + DiagnosticProperty = diagProperty; + RestField = restField; + RestProperty = restProperty; + ApiVersionVariable = apiVariable; + } +} diff --git a/logger/autorest.csharp/mgmt/Output/Models/NameSetKey.cs b/logger/autorest.csharp/mgmt/Output/Models/NameSetKey.cs new file mode 100644 index 0000000..9d88062 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/Models/NameSetKey.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +namespace AutoRest.CSharp.Mgmt.Output.Models; + +internal readonly struct NameSetKey +{ + public MgmtRestClient RestClient { get; } + public Resource? Resource { get; } + + public NameSetKey(MgmtRestClient client, Resource? resource) + { + RestClient = client; + Resource = resource; + } + + public override int GetHashCode() + { + int hc = RestClient.GetHashCode(); + if (Resource is not null) + hc ^= Resource.GetHashCode(); + return hc; + } + public override bool Equals(object? obj) + { + if (obj is null) + return false; + + if (obj is not NameSetKey other) + return false; + + bool eq = RestClient.Equals(other.RestClient); + if (Resource is not null) + eq &= Resource.Equals(other.Resource); + + return eq; + } +} diff --git a/logger/autorest.csharp/mgmt/Output/OperationSource.cs b/logger/autorest.csharp/mgmt/Output/OperationSource.cs new file mode 100644 index 0000000..242e221 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/OperationSource.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Output.Builders; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Serialization.Json; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; +using Azure.ResourceManager; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class OperationSource : TypeProvider + { + public OperationSource(CSharpType returnType, Resource? resource, InputType inputType) : base(MgmtContext.Context) + { + ReturnType = returnType; + DefaultName = $"{(resource != null ? resource.ResourceName : returnType.Name)}OperationSource"; + Interface = new CSharpType(typeof(IOperationSource<>), returnType); + Resource = resource; + ArmClientField = new FieldDeclaration(FieldModifiers.Private | FieldModifiers.ReadOnly, typeof(ArmClient), "_client"); + ArmClientCtor = new ConstructorSignature(Type, null, null, Internal, new[] { KnownParameters.ArmClient }); + ResponseSerialization = SerializationBuilder.BuildJsonSerialization(inputType, resource?.ResourceData.Type ?? returnType, false, SerializationBuilder.GetSerializationFormat(inputType, resource?.ResourceData.Type ?? returnType)); + } + + public bool IsReturningResource => !ReturnType.IsFrameworkType && ReturnType.Implementation is Resource; + + public CSharpType ReturnType { get; } + public CSharpType Interface { get; } + public Resource? Resource { get; } + public FieldDeclaration ArmClientField { get; } + public ConstructorSignature ArmClientCtor { get; } + public JsonSerialization ResponseSerialization { get; } + + protected override string DefaultName { get; } + + protected override string DefaultAccessibility => "internal"; + } +} diff --git a/logger/autorest.csharp/mgmt/Output/PartialResource.cs b/logger/autorest.csharp/mgmt/Output/PartialResource.cs new file mode 100644 index 0000000..9c92ff8 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/PartialResource.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using Azure.Core; + +namespace AutoRest.CSharp.Mgmt.Output; + +/// +/// A virtual resource stands for a resource from another SDK, and it plays a role of anchor of some operations that belong to this resource in another SDK +/// +internal class PartialResource : Resource +{ + protected internal PartialResource(OperationSet operationSet, IEnumerable operations, string defaultName, string originalResourceName, ResourceTypeSegment resourceType) : base(operationSet, operations, defaultName, resourceType, ResourceData.Empty, ResourcePosition) + { + OriginalResourceName = originalResourceName; + } + + /// + /// This is the resource name of its original resource, the resource that this partial resource is extending + /// + public string OriginalResourceName { get; } + + protected override FormattableString CreateDescription() + { + var an = ResourceName.StartsWithVowel() ? "an" : "a"; + List lines = new List(); + + lines.Add($"A class extending from the {OriginalResourceName.AddResourceSuffixToResourceName()} in {MgmtContext.DefaultNamespace} along with the instance operations that can be performed on it."); + lines.Add($"You can only construct {an} {Type:C} from a {typeof(ResourceIdentifier):C} with a resource type of {ResourceType}."); + + return FormattableStringHelpers.Join(lines, "\r\n"); + } + + protected override ConstructorSignature? EnsureResourceDataCtor() + { + // virtual resource does not have this constructor + return null; + } + + protected override Method BuildCreateResourceIdentifierMethod() + { + var original = base.BuildCreateResourceIdentifierMethod(); + + return new( + original.Signature with + { + Modifiers = MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static + }, original.Body!); + } + + protected override IEnumerable GetAdditionalFields() + { + yield break; + } +} diff --git a/logger/autorest.csharp/mgmt/Output/Resource.cs b/logger/autorest.csharp/mgmt/Output/Resource.cs new file mode 100644 index 0000000..f04eb37 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/Resource.cs @@ -0,0 +1,626 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Expressions.KnownValueExpressions; +using AutoRest.CSharp.Common.Output.Expressions.Statements; +using AutoRest.CSharp.Common.Output.Expressions.ValueExpressions; +using AutoRest.CSharp.Common.Output.Models; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Mgmt.Report; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Output.Models.Types; +using Azure.Core; +using Azure.ResourceManager; +using Microsoft.CodeAnalysis; +using static AutoRest.CSharp.Common.Output.Models.Snippets; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class Resource : MgmtTypeProvider + { + protected static readonly string ResourcePosition = "resource"; + protected static readonly string CollectionPosition = "collection"; + private const string DataFieldName = "_data"; + protected readonly Parameter[] _armClientCtorParameters; + + private static readonly RequestMethod[] MethodToExclude = new[] { RequestMethod.Put, RequestMethod.Get, RequestMethod.Delete, RequestMethod.Patch }; + + private static readonly Parameter TagKeyParameter = new Parameter( + "key", + $"The key for the tag.", + typeof(string), + null, + ValidationType.AssertNotNull, + null); + + private static readonly Parameter TagValueParameter = new Parameter( + "value", + $"The value for the tag.", + typeof(string), + null, + ValidationType.AssertNotNull, + null); + + private static readonly Parameter TagSetParameter = new Parameter( + "tags", + $"The set of tags to use as replacement.", + typeof(IDictionary), + null, + ValidationType.AssertNotNull, + null); + + /// + /// The position means which class an operation should go. Possible value of this property is `resource` or `collection`. + /// There is a configuration in which assign values to operations. + /// + protected string Position { get; } + + public OperationSet OperationSet { get; } + + protected IEnumerable _clientOperations; + + private RequestPath? _requestPath; + public RequestPath RequestPath => _requestPath ??= OperationSet.GetRequestPath(ResourceType); + + /// + /// + /// The map that contains all possible operations in this resource and its corresponding resource collection class (if any) + /// The name of the corresponding resource data model + /// The type of this resource instance represents + /// The corresponding resource data model + /// The build context of this resource instance + /// The position of operations of this class. for more information + protected internal Resource(OperationSet operationSet, IEnumerable operations, string resourceName, ResourceTypeSegment resourceType, ResourceData resourceData, string position) + : base(resourceName) + { + _armClientCtorParameters = new[] { KnownParameters.ArmClient, ResourceIdentifierParameter }; + OperationSet = operationSet; + ResourceType = resourceType; + ResourceData = resourceData; + + if (OperationSet.TryGetSingletonResourceSuffix(out var singletonResourceIdSuffix)) + SingletonResourceIdSuffix = singletonResourceIdSuffix; + + _clientOperations = GetClientOperations(operationSet, operations); + + IsById = OperationSet.IsById; + Position = position; + } + + protected override ConstructorSignature? EnsureArmClientCtor() + { + return new ConstructorSignature( + Type, + null, + Description: $"Initializes a new instance of the {Type:C} class.", + Modifiers: Internal, + Parameters: _armClientCtorParameters, + Initializer: new( + isBase: true, + arguments: _armClientCtorParameters)); + } + + protected override ConstructorSignature? EnsureResourceDataCtor() + { + return new ConstructorSignature( + Type, + null, + Description: $"Initializes a new instance of the {Type:C} class.", + Modifiers: Internal, + Parameters: new[] { KnownParameters.ArmClient, ResourceDataParameter }, + Initializer: new( + IsBase: false, + Arguments: new ValueExpression[] { KnownParameters.ArmClient, ResourceDataIdExpression(ResourceDataParameter) })); + } + + public override CSharpType? BaseType => typeof(ArmResource); + + public override Resource? DefaultResource => this; + + protected override FieldModifiers FieldModifiers => base.FieldModifiers | FieldModifiers.ReadOnly; + + protected override IEnumerable GetAdditionalFields() + { + // the resource data private field + yield return new FieldDeclaration(FieldModifiers, ResourceData.Type, DataFieldName); + + // the resource type public field + yield return new FieldDeclaration( + $"Gets the resource type for the operations", + FieldModifiers.Public | FieldModifiers.Static | FieldModifiers.ReadOnly, + typeof(ResourceType), + "ResourceType", + Literal(ResourceType.ToString())); + } + + private IReadOnlyList? _properties; + public IReadOnlyList Properties => _properties ??= BuildProperties().ToArray(); + + protected virtual IEnumerable BuildProperties() + { + // HasData property + var hasDataProperty = new PropertyDeclaration( + description: $"Gets whether or not the current instance has data.", + modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + propertyType: typeof(bool), + name: "HasData", + propertyBody: new AutoPropertyBody(false)); + + yield return hasDataProperty; + + var dataProperty = new PropertyDeclaration( + description: $"Gets the data representing this Feature.", + modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + propertyType: ResourceData.Type, + name: "Data", + propertyBody: new MethodPropertyBody(new MethodBodyStatement[] + { + new IfStatement(Not(new BoolExpression(new VariableReference(hasDataProperty.PropertyType, hasDataProperty.Declaration))), AddBraces: false) + { + Throw(Snippets.New.Instance(typeof(InvalidOperationException), Literal("The current instance does not have data, you must call Get first."))) + }, + Return(new MemberExpression(null, DataFieldName)) + }), + exceptions: new Dictionary() + { + [typeof(InvalidOperationException)] = $"Throws if there is no data loaded in the current instance." + }); + + yield return dataProperty; + } + + public Resource(OperationSet operationSet, IEnumerable operations, string resourceName, ResourceTypeSegment resourceType, ResourceData resourceData) + : this(operationSet, operations, resourceName, resourceType, resourceData, ResourcePosition) + { } + + private static IEnumerable GetClientOperations(OperationSet operationSet, IEnumerable operations) + => operations.Concat(operationSet.Where(operation => !MethodToExclude.Contains(operation.HttpMethod))); + + protected bool IsById { get; } + + protected MgmtClientOperation? GetOperationWithVerb(RequestMethod method, string operationName, bool? isLongRunning = null, bool throwIfNull = false) + { + var result = new List(); + var operation = OperationSet.GetOperation(method); + if (operation is not null) + { + var requestPath = operation.GetRequestPath(ResourceType); + var contextualPath = GetContextualPath(OperationSet, requestPath); + var restOperation = new MgmtRestOperation( + operation, + requestPath, + contextualPath, + operationName, + isLongRunning, + throwIfNull, + Type.Name); + result.Add(restOperation); + } + + return MgmtClientOperation.FromOperations(result, IdVariableName); + } + + public virtual Resource GetResource() => this; + + private bool loggedForDefaultName = false; + // name after `{ResourceName}Resource`, unless the `ResourceName` already ends with `Resource` + protected override string DefaultName + { + get + { + if (Configuration.MgmtConfiguration.NoResourceSuffix.Contains(ResourceName)) + { + if (!loggedForDefaultName) + { + MgmtReport.Instance.TransformSection.AddTransformLog(new TransformItem(TransformTypeName.NoResourceSuffix, ResourceName), ResourceName, $"NoResourceSuffix for {ResourceName}"); + loggedForDefaultName = true; + } + return ResourceName; + } + else + return ResourceName.AddResourceSuffixToResourceName(); + } + } + + public override FormattableString Description => CreateDescription(); + + public bool IsSingleton => SingletonResourceIdSuffix != null; + + public SingletonResourceSuffix? SingletonResourceIdSuffix { get; } + + private bool? _isTaggable; + public bool IsTaggable => GetIsTaggable(); + + private bool GetIsTaggable() + { + if (_isTaggable is not null) + return _isTaggable.Value; + + var bodyParameter = GetBodyParameter(); + if (ResourceData.IsTaggable && bodyParameter is not null) + { + var bodyParamType = bodyParameter.Type; + _isTaggable = bodyParamType.Equals(ResourceData.Type) ? ResourceData.IsTaggable : DoesUpdateSchemaHaveTags(bodyParamType); + } + else + { + _isTaggable = false; + } + + return _isTaggable.Value; + } + + private bool DoesUpdateSchemaHaveTags(CSharpType bodyParamType) + { + if (bodyParamType.IsFrameworkType) + return false; + if (bodyParamType.Implementation is null) + return false; + if (bodyParamType.Implementation is not SchemaObjectType schemaObject) + return false; + return schemaObject.InputModel.HasTags(); + } + + private Parameter? GetBodyParameter() + { + //found a case in logic where there is a patch with only a cancellation token + //I think this is a bug in there swagger but this works around that since generation + //will fail if the update doesn't have a body param + var op = UpdateOperation ?? CreateOperation; + if (op is null) + return null; + + return op.MethodParameters.FirstOrDefault(p => p.RequestLocation == Common.Input.RequestLocation.Body); + } + + /// + /// Finds the corresponding of this + /// Return null when this resource is a singleton. + /// + public ResourceCollection? ResourceCollection { get; internal set; } + + /// + /// Finds the corresponding of this + /// + public ResourceData ResourceData { get; } + private MgmtClientOperation? _createOperation; + private MgmtClientOperation? _getOperation; + private MgmtClientOperation? _deleteOperation; + private MgmtClientOperation? _updateOperation; + public virtual MgmtClientOperation? CreateOperation => _createOperation ??= GetOperationWithVerb(RequestMethod.Put, "CreateOrUpdate", true); + public virtual MgmtClientOperation GetOperation => _getOperation ??= GetOperationWithVerb(RequestMethod.Get, "Get", throwIfNull: true)!; + public virtual MgmtClientOperation? DeleteOperation => _deleteOperation ??= GetOperationWithVerb(RequestMethod.Delete, "Delete", true); + public virtual MgmtClientOperation? UpdateOperation => _updateOperation ??= EnsureUpdateOperation(); + + private MgmtClientOperation? EnsureUpdateOperation() + { + var updateOperation = GetOperationWithVerb(RequestMethod.Patch, "Update"); + + if (updateOperation != null) + return updateOperation; + + if (ResourceCollection?.CreateOperation is not null) + { + var createOrUpdateOperation = ResourceCollection.CreateOperation.OperationMappings.Values.First(); + return MgmtClientOperation.FromOperation( + new MgmtRestOperation( + createOrUpdateOperation, + "Update", + createOrUpdateOperation.MgmtReturnType, + createOrUpdateOperation.Description ?? $"Update this {ResourceName}.", + createOrUpdateOperation.RequestPath), + IdVariableName); + } + + return null; + } + + protected virtual bool ShouldIncludeOperation(InputOperation operation) + { + if (Configuration.MgmtConfiguration.OperationPositions.TryGetValue(operation.OperationId!, out var positions)) + { + return positions.Contains(Position); + } + // In the resource class, we need to exclude the List operations + if (MgmtContext.Library.GetRestClientMethod(operation).IsListMethod(out var valueType)) + return !valueType.AreNamesEqual(ResourceData.Type); + return true; + } + + protected override IEnumerable EnsureAllOperations() + { + var result = new List(); + if (GetOperation != null) + result.Add(GetOperation); + if (DeleteOperation != null) + result.Add(DeleteOperation); + if (UpdateOperation != null) + result.Add(UpdateOperation); + if (IsSingleton && CreateOperation != null) + result.Add(CreateOperation); + result.AddRange(ClientOperations); + if (GetOperation != null && IsTaggable) + { + var getOperation = GetOperation.OperationMappings.Values.First(); + result.Add(MgmtClientOperation.FromOperation( + new MgmtRestOperation( + getOperation, + "AddTag", + getOperation.MgmtReturnType, + "Add a tag to the current resource.", + TagKeyParameter, + TagValueParameter), + IdVariableName, + isConvenientOperation: true)); + + result.Add(MgmtClientOperation.FromOperation( + new MgmtRestOperation( + getOperation, + "SetTags", + getOperation.MgmtReturnType, + "Replace the tags on the resource with the given set.", + TagSetParameter), + IdVariableName, + isConvenientOperation: true)); + + result.Add(MgmtClientOperation.FromOperation( + new MgmtRestOperation( + getOperation, + "RemoveTag", + getOperation.MgmtReturnType, + "Removes a tag by key from the resource.", + TagKeyParameter), + IdVariableName, + isConvenientOperation: true)); + } + return result; + } + + public override FormattableString BranchIdVariableName => $"Id.Parent"; + + public override ResourceTypeSegment GetBranchResourceType(RequestPath branch) + { + return branch.ParentRequestPath().GetResourceType(); + } + + private IEnumerable? _extraContextualParameterMapping; + public IEnumerable ExtraContextualParameterMapping => _extraContextualParameterMapping ??= EnsureExtraContextualParameterMapping(); + protected virtual IEnumerable EnsureExtraContextualParameterMapping() => Enumerable.Empty(); + + /// + /// A collection of ClientOperations. + /// The List of represents a set of the same operations under different parent (OperationSet) + /// + protected override IEnumerable EnsureClientOperations() => EnsureClientOperationMap().Values; + + /// + /// This is a map from the diff request path between the operation and the contextual path to the actual operations + /// + private IDictionary? _clientOperationMap; + private IDictionary EnsureClientOperationMap() + { + if (_clientOperationMap != null) + return _clientOperationMap; + + var result = new Dictionary>(); + var resourceRequestPath = OperationSet.GetRequestPath(); + var resourceRestClient = OperationSet.Any() ? MgmtContext.Library.GetRestClient(OperationSet.First()) : null; + // iterate over all the operations under this operationSet to get their diff between the corresponding contextual path + foreach (var operation in _clientOperations) + { + if (!ShouldIncludeOperation(operation)) + continue; // meaning this operation will be included in the collection + var method = operation.HttpMethod; + // we use the "unique" part of this operation's request path comparing with its containing resource's path as the key to categorize the operations + var requestPath = operation.GetRequestPath(ResourceType); + var key = $"{method}{resourceRequestPath.Minus(requestPath)}"; + var contextualPath = GetContextualPath(OperationSet, requestPath); + var methodName = IsListOperation(operation, OperationSet) ? + "GetAll" :// hard-code the name of a resource collection operation to "GetAll" + GetOperationName(operation, resourceRestClient?.InputClient.Key ?? string.Empty); + // get the MgmtRestOperation with a proper name + var restOperation = new MgmtRestOperation( + operation, + requestPath, + contextualPath, + methodName, + propertyBagName: Type.Name); + + if (result.TryGetValue(key, out var list)) + { + list.Add(restOperation); + } + else + { + result.Add(key, new List { restOperation }); + } + } + + // now the operations should be properly categarized into the dictionary in the key of diff between contextual request path and the operation + // TODO -- what if the response type is not the same? Also we need to verify they have the same parameters before we could union those together + _clientOperationMap = result.Where(pair => pair.Value.Count > 0).ToDictionary( + pair => pair.Key, + pair => MgmtClientOperation.FromOperations(pair.Value, IdVariableName)!); // We first filtered the ones with at least one operation, therefore this will never be null + return _clientOperationMap; + } + + /// + /// This method returns the contextual path from one resource + /// In the class, we just use the RequestPath of the OperationSet as its contextual path + /// Also we need to replace the parameterized scope if there is any with the actual scope value. + /// + /// + /// + /// + protected virtual RequestPath GetContextualPath(OperationSet operationSet, RequestPath operationRequestPath) + { + var contextualPath = RequestPath; + // we need to replace the scope in this contextual path with the actual scope in the operation + var scope = contextualPath.GetScopePath(); + if (!scope.IsParameterizedScope()) + return contextualPath; + + return operationRequestPath.GetScopePath().Append(contextualPath.TrimScope()); + } + + protected bool IsListOperation(InputOperation operation, OperationSet operationSet) + { + return operation.IsResourceCollectionOperation(out var resourceOperationSet) && resourceOperationSet == operationSet; + } + + public ResourceTypeSegment ResourceType { get; } + + protected virtual FormattableString CreateDescription() + { + var an = ResourceName.StartsWithVowel() ? "an" : "a"; + List lines = new List(); + var parents = this.GetParents(); + var parentTypes = parents.Select(parent => parent.TypeAsResource).ToList(); + var parentDescription = CreateParentDescription(parentTypes); + + lines.Add($"A Class representing {an} {ResourceName} along with the instance operations that can be performed on it."); + lines.Add($"If you have a {typeof(ResourceIdentifier):C} you can construct {an} {Type:C}"); + lines.Add($"from an instance of {typeof(ArmClient):C} using the Get{DefaultName} method."); + // only append the following information when the parent of me is not myself, aka TenantResource + if (parentDescription != null && !parents.Contains(this)) + { + lines.Add($"Otherwise you can get one from its parent resource {parentDescription} using the Get{ResourceName} method."); + } + + return FormattableStringHelpers.Join(lines, "\r\n"); + } + + protected static FormattableString? CreateParentDescription(IReadOnlyList parentTypes) => parentTypes.Count switch + { + 0 => null, + _ => FormattableStringHelpers.Join(parentTypes.Select(type => (FormattableString)$"{type:C}").ToList(), ", ", " or "), + }; + + private static CSharpType GetReferenceType(Reference reference) + => reference.Name switch + { + "location" when reference.Type.EqualsIgnoreNullable(typeof(string)) => typeof(AzureLocation), + _ => reference.Type + }; + + private Parameter CreateResourceIdentifierParameter(Segment segment) + => new Parameter(segment.Reference.Name, $"The {segment.Reference.Name}", GetReferenceType(segment.Reference), null, ValidationType.None, null); + + public Method? _createResourceIdentifierMethod; + public Method CreateResourceIdentifierMethod => _createResourceIdentifierMethod ??= BuildCreateResourceIdentifierMethod(); + + protected virtual Method BuildCreateResourceIdentifierMethod() + { + var signature = new MethodSignature( + Name: "CreateResourceIdentifier", + null, + Description: $"Generate the resource identifier of a {Type:C} instance.", + Modifiers: Public | Static, + ReturnType: typeof(ResourceIdentifier), + ReturnDescription: null, + Parameters: RequestPath.Where(segment => segment.IsReference).Select(CreateResourceIdentifierParameter).ToArray()); + + // build the format string of the id + var formatBuilder = new StringBuilder(); + var first = true; + var refCount = 0; + foreach (var segment in RequestPath) + { + if (first) + { + first = false; + // If first segment is "{var}", then we should not add leading "/". Instead, we should let callers to specify, e.g. "{scope}/providers/Microsoft.Resources/..." v.s. "/subscriptions/{subscriptionId}/..." + if (RequestPath.Count == 0 || RequestPath[0].IsConstant) + formatBuilder.Append('/'); + } + else + formatBuilder.Append('/'); + if (segment.IsConstant) + formatBuilder.Append(segment.ConstantValue); + else + { + formatBuilder.Append('{') + .Append(refCount) + .Append('}'); + refCount++; + } + } + + var resourceId = new VariableReference(typeof(ResourceIdentifier), "resourceId"); + var methodBody = new MethodBodyStatement[] + { + Var(resourceId, new FormattableStringExpression(formatBuilder.ToString(), signature.Parameters.Select(p => (ValueExpression)p).ToArray())), + Return(Snippets.New.Instance(typeof(ResourceIdentifier), resourceId)) + }; + return new Method(signature, methodBody); + } + + public ValueExpression ResourceDataIdExpression(ValueExpression dataExpression) + { + var id = dataExpression.Property("Id"); + var typeOfId = ResourceData.TypeOfId; + if (typeOfId != null && typeOfId.Equals(typeof(string))) + { + return Snippets.New.Instance(typeof(ResourceIdentifier), id); + } + else + { + // we have ensured other cases we would have an Id of Azure.Core.ResourceIdentifier type + return id; + } + } + + public Parameter ResourceParameter => new(Name: "resource", Description: $"The client parameters to use in these operations.", Type: typeof(ArmResource), DefaultValue: null, ValidationType.None, null); + public Parameter ResourceDataParameter => new(Name: "data", Description: $"The resource that is the target of operations.", Type: ResourceData.Type, DefaultValue: null, ValidationType.None, null); + + private FormattableString? _createResourceIdentifierReference; + public FormattableString CreateResourceIdentifierReference => _createResourceIdentifierReference ??= BuildCreateResourceIdentifierMethodReference(); + + private FormattableString BuildCreateResourceIdentifierMethodReference() + { + // see if there is method naming "CreateResourceIdentifier" in customization code + var methods = ExistingType?.GetMembers().OfType().Where(m => m.MethodKind is MethodKind.Ordinary); + var createResourceIdentifierMethod = methods?.FirstOrDefault(m => m.Name == CreateResourceIdentifierMethod.Signature.Name); + + if (createResourceIdentifierMethod == null) + { + return $"{Type}.{CreateResourceIdentifierMethod.Signature.Name}"; + } + + var parameters = CreateResourceIdentifierMethod.Signature.Parameters; + var formatBuilder = new StringBuilder("{0}.") + .Append(CreateResourceIdentifierMethod.Signature.Name) + .Append("("); + var arguments = new List(parameters.Count + 1) + { + Type + }; + for (int i = 0; i < parameters.Count; i++) + { + formatBuilder.Append($"{{{i + 1}}}"); // +1 because the first argument is set to Resource Type + if (i != parameters.Count - 1) + { + formatBuilder.Append(","); + } + arguments.Add(parameters[i].Type); + } + formatBuilder.Append(")"); + return FormattableStringFactory.Create(formatBuilder.ToString(), [.. arguments]); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Output/ResourceCollection.cs b/logger/autorest.csharp/mgmt/Output/ResourceCollection.cs new file mode 100644 index 0000000..7716088 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/ResourceCollection.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Generation.Writers; +using AutoRest.CSharp.Input; +using AutoRest.CSharp.Mgmt.AutoRest; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Models; +using AutoRest.CSharp.Output.Models.Requests; +using AutoRest.CSharp.Output.Models.Shared; +using AutoRest.CSharp.Utilities; +using Azure.ResourceManager; +using static AutoRest.CSharp.Mgmt.Decorator.ParameterMappingBuilder; +using static AutoRest.CSharp.Output.Models.MethodSignatureModifiers; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class ResourceCollection : Resource + { + private const string _suffixValue = "Collection"; + + public ResourceCollection(OperationSet operationSet, IEnumerable operations, Resource resource) + : base(operationSet, operations, resource.ResourceName, resource.ResourceType, resource.ResourceData, CollectionPosition) + { + Resource = resource; + } + + public override CSharpType? BaseType => typeof(ArmCollection); + protected override IReadOnlyList EnsureGetInterfaces() + { + if (GetAllOperation is null || GetAllOperation.MethodParameters.Any(p => !p.IsOptionalInSignature && + (!p.IsPropertyBag || p.Validation != ValidationType.None))) + return base.EnsureGetInterfaces(); + + var getRestOperation = GetAllOperation.OperationMappings.Values.First(); + return new CSharpType[] + { + new CSharpType(typeof(IEnumerable<>), getRestOperation.MgmtReturnType!), + new CSharpType(typeof(IAsyncEnumerable<>), getRestOperation.MgmtReturnType!) + }; + } + public Resource Resource { get; } + + public override Resource GetResource() => Resource; + + public override bool CanValidateResourceType => ResourceTypes.SelectMany(p => p.Value).Distinct().Count() == 1; + + public override FormattableString BranchIdVariableName => $"Id"; + + private MgmtClientOperation? _getAllOperation; + public MgmtClientOperation? GetAllOperation => _getAllOperation ??= EnsureGetAllOperation(); + + private Dictionary _extraConstructorParameters = new(); + + protected override IEnumerable EnsureExtraCtorParameters() + { + _ = ExtraContextualParameterMapping; + return _extraConstructorParameters.Keys; + } + + protected override ConstructorSignature? EnsureArmClientCtor() + { + return new ConstructorSignature( + Type, + null, + Description: $"Initializes a new instance of the {Type:C} class.", + Modifiers: Internal, + Parameters: _armClientCtorParameters.Concat(ExtraConstructorParameters).ToArray(), + Initializer: new( + isBase: true, + arguments: _armClientCtorParameters)); + } + protected override ConstructorSignature? EnsureResourceDataCtor() => null; + + protected override IEnumerable EnsureExtraContextualParameterMapping() + { + var result = new List(); + InputOperation? op = null; + foreach (var operation in _clientOperations) + { + if (IsListOperation(operation, OperationSet)) + { + op = operation; + break; + } + } + + if (op is null) + return result; + + RestClientMethod method = MgmtContext.Library.GetRestClientMethod(op); + // calculate the ResourceType from the RequestPath of this resource + var resourceTypeSegments = ResourceType.Select((segment, index) => (segment, index)).Where(tuple => tuple.segment.IsReference).ToList(); + // iterate over all the reference segments in the diff of this GetAll operation + var candidatesOfParameters = new List(method.Parameters); + + var opRequestPath = op.GetRequestPath(ResourceType); + foreach (var segment in GetDiffFromRequestPath(opRequestPath, GetContextualPath(OperationSet, opRequestPath))) + { + var index = resourceTypeSegments.FindIndex(tuple => tuple.segment == segment); + if (index < 0) + { + var parameter = candidatesOfParameters.First(p => p.Name == segment.ReferenceName && p.Type.Equals(segment.Type)); + candidatesOfParameters.Remove(parameter); + // this reference is not in the resource type, therefore this parameter goes to the constructor + _extraConstructorParameters.Add(parameter, $"_{segment.ReferenceName}"); + // there is a key for this parameter, get the key and add this one to contextual parameter mapping + var key = ParameterMappingBuilder.FindKeyOfParameter(parameter, opRequestPath); + result.Add(new ContextualParameterMapping(key, segment, GetFieldName(parameter))); + } + else + { + var candidate = resourceTypeSegments[index]; + var value = ResourceType[candidate.index]; + try + { + result.Add(new ContextualParameterMapping("", segment, $"\"{value.ConstantValue}\"")); + } + catch (InvalidOperationException) + { + throw new InvalidOperationException($"Expected enum type for the parameter '{segment.ReferenceName}' in method '{method.Operation.Path}'"); + } + } + } + return result; + } + + private MgmtClientOperation? EnsureGetAllOperation() + { + // if this resource was listed in list-exception section, we suppress the exception here + // or if the debug flag `--mgmt-debug.suppress-list-exception` is on, we suppress the exception here + var suppressListException = Configuration.MgmtConfiguration.ListException.Contains(RequestPath) + || Configuration.MgmtConfiguration.MgmtDebug.SuppressListException; + var getAllOperation = ClientOperations.Where(operation => operation.Name == "GetAll").OrderBy(operation => ReferenceSegments(operation).Count()).FirstOrDefault(); + + if (getAllOperation == null) + return getAllOperation; + + // skip the following transformations for `ById` resources. + // In ById resources, the required parameters of the GetAll operation is usually a scope, doing the following transform will require the constructor to accept a scope variable + // which is not reasonable and causes problems + if (IsById) + { + return ReferenceSegments(getAllOperation).Any() ? null : getAllOperation; + } + + return getAllOperation; + } + + public FormattableString GetFieldName(Parameter parameter) + { + return _extraConstructorParameters[parameter]; + } + + private static IEnumerable ReferenceSegments(MgmtClientOperation clientOperation) + { + var operation = clientOperation.First(); + return GetDiffFromRequestPath(operation.RequestPath, operation.ContextualPath); + } + + private static IEnumerable GetDiffFromRequestPath(RequestPath requestPath, RequestPath contextPath) + { + RequestPath diff; + if (requestPath.IsAncestorOf(contextPath)) + { + diff = requestPath.TrimAncestorFrom(contextPath); + } + else + { + diff = contextPath.TrimAncestorFrom(requestPath); + } + return diff.Where(segment => segment.IsReference); + } + + protected override bool ShouldIncludeOperation(InputOperation operation) + { + if (Configuration.MgmtConfiguration.OperationPositions.TryGetValue(operation.OperationId!, out var positions)) + { + return positions.Contains(Position); + } + // if the position of this operation is not set in the configuration, we just include those are excluded in the resource class + return !base.ShouldIncludeOperation(operation); + } + + /// + /// This method returns the contextual path from one resource + /// In the class, we need to use the parent RequestPath of the OperationSet as its contextual path + /// + /// + /// + /// + protected override RequestPath GetContextualPath(OperationSet operationSet, RequestPath operationRequestPath) + { + var contextualPath = operationSet.ParentRequestPath(ResourceType); + // we need to replace the scope in this contextual path with the actual scope in the operation + var scope = contextualPath.GetScopePath(); + if (!scope.IsParameterizedScope()) + return contextualPath; + + return operationRequestPath.GetScopePath().Append(contextualPath.TrimScope()); + } + + // name after `{ResourceName}Collection` + protected override string DefaultName => ResourceName + _suffixValue; + + protected override FormattableString CreateDescription() + { + var an = ResourceName.StartsWithVowel() ? "an" : "a"; + List lines = new List(); + var parents = Resource.GetParents(); + var parentTypes = parents.Select(parent => parent.TypeAsResource).ToList(); + var parentDescription = CreateParentDescription(parentTypes); + + lines.Add($"A class representing a collection of {Resource.Type:C} and their operations."); + // only append the following information when the parent of me is not myself, aka TenantResource + if (parentDescription != null && !parents.Contains(Resource)) + { + lines.Add($"Each {Resource.Type:C} in the collection will belong to the same instance of {parentDescription}."); + lines.Add($"To get {an} {Type:C} instance call the Get{ResourceName.LastWordToPlural()} method from an instance of {parentDescription}."); + } + + return FormattableStringHelpers.Join(lines, "\r\n"); + } + + protected override IEnumerable EnsureAllOperations() + { + var result = new List(); + if (CreateOperation != null) + result.Add(CreateOperation); + if (GetOperation != null) + result.Add(GetOperation); + result.AddRange(ClientOperations); + if (GetOperation != null) + { + var getMgmtRestOperation = GetOperation.OperationMappings.Values.First(); + result.Add(MgmtClientOperation.FromOperation( + new MgmtRestOperation( + getMgmtRestOperation, + "Exists", + typeof(bool), + $"Checks to see if the resource exists in azure."), + IdVariableName)); + result.Add(MgmtClientOperation.FromOperation( + new MgmtRestOperation( + getMgmtRestOperation, + "GetIfExists", + getMgmtRestOperation.MgmtReturnType, + $"Tries to get details for this resource from the service."), + IdVariableName)); + } + + return result; + } + + public override ResourceTypeSegment GetBranchResourceType(RequestPath branch) + { + return branch.GetResourceType(); + } + + protected override IEnumerable GetAdditionalFields() + { + foreach (var reference in ExtraConstructorParameters) + { + yield return new FieldDeclaration(FieldModifiers, reference.Type, GetFieldName(reference).ToString()); + } + } + + private IDictionary>? _resourceTypes; + public IDictionary> ResourceTypes => _resourceTypes ??= EnsureResourceTypes(); + + private IDictionary> EnsureResourceTypes() + { + var result = new Dictionary>(); + foreach (var operation in AllOperations.SelectMany(o => o)) + { + var resourceTypes = GetResourceTypes(operation.RequestPath, operation.ContextualPath); + if (result.TryGetValue(operation.ContextualPath, out var set)) + { + set.UnionWith(resourceTypes); + } + else + { + set = new HashSet(); + set.UnionWith(resourceTypes); + result.Add(operation.ContextualPath, set); + } + } + return result; + } + + private IEnumerable GetResourceTypes(RequestPath requestPath, RequestPath contextualPath) + { + var type = contextualPath.GetResourceType(); + if (type == ResourceTypeSegment.Scope) + return requestPath.GetParameterizedScopeResourceTypes()!; + + return type.AsIEnumerable(); + } + + public Parameter ParentParameter => ResourceParameter with + { + Name = "parent", + Description = $"The resource representing the parent resource." + }; + + protected override FormattableString IdParamDescription => $"The identifier of the parent resource that is the target of operations."; + } +} diff --git a/logger/autorest.csharp/mgmt/Output/ResourceData.cs b/logger/autorest.csharp/mgmt/Output/ResourceData.cs new file mode 100644 index 0000000..16a3443 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Output/ResourceData.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AutoRest.CSharp.Common.Input; +using AutoRest.CSharp.Common.Output.Models.Types; +using AutoRest.CSharp.Generation.Types; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Output.Builders; + +namespace AutoRest.CSharp.Mgmt.Output +{ + internal class ResourceData : MgmtObjectType + { + public ResourceData(InputModelType inputModel, SerializableObjectType? defaultDerivedType = null) + : base(inputModel, defaultDerivedType) + { + _clientPrefix = inputModel.Name; + } + + public static ResourceData Empty = new ResourceData(new InputModelType(string.Empty, string.Empty, null, null, null, InputModelTypeUsage.None, Array.Empty(), null, Array.Empty(), null, null, new Dictionary(), null)); + + protected override bool IsResourceType => true; + + protected override FormattableString CreateDescription() + { + FormattableString baseDescription = $"{BuilderHelpers.EscapeXmlDocDescription($"A class representing the {_clientPrefix} data model.")}"; + FormattableString extraDescription = string.IsNullOrWhiteSpace(InputModel.Description) ? + (FormattableString)$"" : + $"{Environment.NewLine}{BuilderHelpers.EscapeXmlDocDescription(InputModel.Description)}"; + return $"{baseDescription}{extraDescription}"; + } + + private string _clientPrefix; + + private bool? _isTaggable; + public bool IsTaggable => _isTaggable ??= EnsureIsTaggable(); + private bool EnsureIsTaggable() + { + return InputModel.HasTags(); + } + + private CSharpType? typeOfId; + internal CSharpType? TypeOfId => typeOfId ??= GetTypeOfId(); + + /// + /// Get the of the `Id` property of this ResourceData. + /// Return null if this resource data does not have an Id property. + /// + /// + private CSharpType? GetTypeOfId() + { + var baseTypes = EnumerateHierarchy().TakeLast(2).ToArray(); + var baseType = baseTypes.Length == 1 || baseTypes[1].Declaration.Name == "Object" ? baseTypes[0] : baseTypes[1]; + var idProperty = baseType.Properties.Where(p => p.Declaration.Name == "Id").FirstOrDefault(); + return idProperty?.Declaration.Type; + } + + internal CSharpType? GetTypeOfName() + { + var baseTypes = EnumerateHierarchy().TakeLast(2).ToArray(); + var baseType = baseTypes.Length == 1 || baseTypes[1].Declaration.Name == "Object" ? baseTypes[0] : baseTypes[1]; + var nameProperty = baseType.Properties.Where(p => p.Declaration.Name == "Name").FirstOrDefault(); + return nameProperty?.Declaration.Type; + } + + internal virtual bool ShouldSetResourceIdentifier => TypeOfId == null; + } +} diff --git a/logger/autorest.csharp/mgmt/Report/DictionaryReportSection.cs b/logger/autorest.csharp/mgmt/Report/DictionaryReportSection.cs new file mode 100644 index 0000000..c0008a0 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/DictionaryReportSection.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class DictionaryReportSection : ReportSection + { + public Func> _getInitialDict; + public Dictionary _dict = new Dictionary(); + + public DictionaryReportSection(string name, Func>? getInitialDict = null) + : base(name) + { + this._getInitialDict = getInitialDict ?? (() => new Dictionary()); + this._dict = this._getInitialDict(); + } + + public override Dictionary GenerateSection() + { + return this._dict.ToDictionary(kv => kv.Key, kv => (object?)kv.Value); + } + + public override void Reset() + { + this._dict = this._getInitialDict(); + } + + public void Add(string key, T item) + { + this._dict[key] = item; + } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/EnumItem.cs b/logger/autorest.csharp/mgmt/Report/EnumItem.cs new file mode 100644 index 0000000..d426462 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/EnumItem.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class EnumItem : TransformableItem + { + public EnumItem(string @namespace, string name, string serializedName, TransformSection transformSection) + :base(serializedName, transformSection) + { + FullName = string.IsNullOrEmpty(@namespace) ? name : $"{@namespace}.{name}"; + } + + [YamlIgnore] + [JsonIgnore] + public string FullName { get; set; } + + public Dictionary Values { get; set; } = new Dictionary(); + } +} diff --git a/logger/autorest.csharp/mgmt/Report/EnumValueItem.cs b/logger/autorest.csharp/mgmt/Report/EnumValueItem.cs new file mode 100644 index 0000000..229fe68 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/EnumValueItem.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class EnumValueItem : TransformableItem + { + public EnumValueItem(string name, string serializedName, TransformSection transformSection) + :base(serializedName, transformSection) + { + Name = name; + } + + [YamlIgnore] + [JsonIgnore] + public string Name { get; set; } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/ExtensionItem.cs b/logger/autorest.csharp/mgmt/Report/ExtensionItem.cs new file mode 100644 index 0000000..e0970d3 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/ExtensionItem.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class ExtensionItem + { + public ExtensionItem(MgmtExtension extension, TransformSection transformSection) + { + this.Name = extension.ResourceName; + this.ContextPaths = + extension.AllOperations.SelectMany(cop => cop.Select(rop => rop.ContextualPath.ToString())).Distinct().ToList(); + this.Operations = extension.AllOperations + .GroupBy(op => op.MethodSignature.Name) + .OrderBy(g => g.Key) + .ToDictionary( + g => g.Key, + g => g.SelectMany(op => op.Select(mrop => new OperationItem(mrop, transformSection))).Distinct().ToList()); + } + + public ExtensionItem(MgmtMockableExtension mockableExtension, TransformSection transformSection) + { + this.Name = mockableExtension.ResourceName; + this.ContextPaths = + mockableExtension.AllOperations.SelectMany(cop => cop.Select(rop => rop.ContextualPath.ToString())).Distinct().ToList(); + this.Operations = mockableExtension.AllOperations + .GroupBy(op => op.MethodSignature.Name) + .ToDictionary( + g => g.Key, + g => g.SelectMany(op => op.Select(mrop => new OperationItem(mrop, transformSection))).Distinct().ToList()); + } + + [YamlIgnore] + [JsonIgnore] + public string Name { get; set; } + public List ContextPaths { get; set; } = new List(); + public Dictionary> Operations { get; set; } = new Dictionary>(); + } +} diff --git a/logger/autorest.csharp/mgmt/Report/MgmtReport.cs b/logger/autorest.csharp/mgmt/Report/MgmtReport.cs new file mode 100644 index 0000000..0dc8db8 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/MgmtReport.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class MgmtReport + { + public static MgmtReport Instance { get; } = new MgmtReport(); + + public MgmtReport() + { + _sections.Add(ModelSection); + _sections.Add(EnumSection); + _sections.Add(ResourceSection); + _sections.Add(ResourceCollectionSection); + _sections.Add(ExtensionSection); + _sections.Add(TransformSection); + _sections.Add(HelpMessage); + } + + private List _sections = new List(); + + public DictionaryReportSection ModelSection { get; } = new DictionaryReportSection("ObjectModels"); + public DictionaryReportSection EnumSection { get; } = new DictionaryReportSection("EnumModels"); + public DictionaryReportSection ResourceSection { get; } = new DictionaryReportSection("Resources"); + public DictionaryReportSection ResourceCollectionSection { get; } = new DictionaryReportSection("ResourceCollections"); + public DictionaryReportSection ExtensionSection { get; } = new DictionaryReportSection("Extensions"); + public TransformSection TransformSection { get; } = new TransformSection("Transforms"); + private DictionaryReportSection HelpMessage { get; } = new DictionaryReportSection("_help_message_", () => + new Dictionary() + { + { "1", "If the transform configuration has a '!' postfix (i.e. 'ProxyResource!'), it means the config is not from *.md config file (i.e. it's built-in through hard code)" }, + { "2", "[{num}] in transform log is the index the transform is applied" }, + { "3", "[=] in transform log means the value (target FullSerializedName) is the same as the key (transform configuration)" }, + { "4", "Supported configuration: mgmt-debug.generate-report=true|false, mgmt-debug.report-only=true|false, mgmt-debug.report-format=yaml|json"} + }); + + public string GenerateReport(string format) + { + var reportObj = _sections.ToDictionary(s => s.Name, s => s.GenerateSection()); + + switch (format.ToLower()) + { + case "json": + return JsonSerializer.Serialize(reportObj, new JsonSerializerOptions() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }); + case "yaml": + case "": + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + return serializer.Serialize(reportObj); + default: + throw new ArgumentException($"Unknown Report Format '{format}'. Only 'json' and 'yaml' is supported now"); + } + } + + public void Reset() + { + foreach (var item in _sections) + { + item.Reset(); + } + } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/ModelItem.cs b/logger/autorest.csharp/mgmt/Report/ModelItem.cs new file mode 100644 index 0000000..130c34c --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/ModelItem.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class ModelItem : TransformableItem + { + public ModelItem(string @namespace, string name, string serializedName, TransformSection transformSection) + : base(serializedName, transformSection) + { + FullName = string.IsNullOrEmpty(@namespace) ? name : $"{@namespace}.{name}"; + } + + [YamlIgnore] + [JsonIgnore] + public string FullName { get; set; } + + public Dictionary Properties { get; set; } = new Dictionary(); + } +} diff --git a/logger/autorest.csharp/mgmt/Report/OperationItem.cs b/logger/autorest.csharp/mgmt/Report/OperationItem.cs new file mode 100644 index 0000000..11a7a3e --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/OperationItem.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Common.Utilities; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Models; +using AutoRest.CSharp.Output.Builders; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class OperationItem : TransformableItem + { + public OperationItem(MgmtRestOperation operation, TransformSection transformSection) + : base(operation.OperationId, transformSection) + { + this.OperationId = operation.OperationId; + this.IsLongRunningOperation = operation.IsLongRunningOperation; + this.IsPageableOperation = operation.IsPagingOperation; + this.Resource = operation.Resource?.ResourceName; + + var bodyParam = operation.Parameters.FirstOrDefault(p => p.RequestLocation == Common.Input.RequestLocation.Body); + if (bodyParam != null) + { + var bodyRequestParam = operation.Operation.GetBodyParameter(); + string? paramFullSerializedName; + if (bodyRequestParam == null) + { + paramFullSerializedName = $"{operation.Operation.GetFullSerializedName()}.{bodyParam.Name}"; + string warning = $"Can't find corresponding RequestParameter for Parameter {operation.Name}.{bodyParam.Name}. OperationId = {operation.OperationId}. Try to use Parameter.Name to parse fullSerializedName as {paramFullSerializedName}"; + AutoRestLogger.Warning(warning).Wait(); + } + else if (bodyRequestParam.CSharpName() != bodyParam.Name) + { + paramFullSerializedName = operation.Operation.GetFullSerializedName(bodyRequestParam); + string warning = $"Name mismatch between Parameter and RequestParameter. OperationId = {operation.OperationId}. Parameter.Name = {bodyParam.Name}, RequestParameter.CSharpName = {bodyRequestParam.CSharpName()}. Try to use RequestParameter to parse fullSerializedName as {paramFullSerializedName}"; + AutoRestLogger.Warning(warning).Wait(); + } + else + { + paramFullSerializedName = operation.Operation.GetFullSerializedName(bodyRequestParam); + } + this.BodyParameter = new ParameterItem(bodyParam, paramFullSerializedName, transformSection); + } + } + + public string OperationId { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ParameterItem? BodyParameter { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsLongRunningOperation { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsPageableOperation { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Resource { get; set; } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/ParameterItem.cs b/logger/autorest.csharp/mgmt/Report/ParameterItem.cs new file mode 100644 index 0000000..cdaa633 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/ParameterItem.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AutoRest.CSharp.Output.Models.Shared; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class ParameterItem : TransformableItem + { + public ParameterItem(Parameter parameter, string paramFullSerializedName, TransformSection transformSection) + : base(paramFullSerializedName, transformSection) + { + this.Name = parameter.Name; + this.Type = parameter.Type.GetNameForReport(); + } + + public string Name { get; set; } + public string Type { get; set; } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/PropertyItem.cs b/logger/autorest.csharp/mgmt/Report/PropertyItem.cs new file mode 100644 index 0000000..a6a30aa --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/PropertyItem.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class PropertyItem : TransformableItem + { + public PropertyItem(string name, string type, string serializedName, TransformSection transformSection) + :base(serializedName, transformSection) + { + Name = name; + Type = type; + } + + [YamlIgnore] + [JsonIgnore] + public string Name { get; set; } + public string Type { get; set; } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/ReportExtension.cs b/logger/autorest.csharp/mgmt/Report/ReportExtension.cs new file mode 100644 index 0000000..1127cd6 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/ReportExtension.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AutoRest.CSharp.Generation.Types; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal static class ReportExtension + { + public static T AddToTransformerStore(this T items, string transformerType, bool fromConfig) where T : IEnumerable + { + foreach (var item in items) + MgmtReport.Instance.TransformSection.AddTransformer(transformerType, item, fromConfig); + return items; + } + + public static IReadOnlyDictionary AddToTransformerStore(this IReadOnlyDictionary dict, string transformerType, bool fromConfig) + { + return AddToTransformerStore(dict, transformerType, (arg) => new TransformItem[] { new TransformItem(arg.Type, arg.Key, fromConfig, arg.Value) }); + } + + public static IReadOnlyDictionary AddToTransformerStore(this IReadOnlyDictionary dict, string transformerType, Func<(string Type, string Key, T Value), IEnumerable> toTransformerArguments) + { + foreach (var kv in dict) + { + var items = toTransformerArguments((transformerType, kv.Key, kv.Value)); + MgmtReport.Instance.TransformSection.AddTransformers(items); + } + return dict; + } + + public static string GetNameForReport(this CSharpType type) + { + return type.ToString().Trim().Replace("global::", ""); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/ReportSection.cs b/logger/autorest.csharp/mgmt/Report/ReportSection.cs new file mode 100644 index 0000000..fc6be14 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/ReportSection.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal abstract class ReportSection + { + private string _name; + public ReportSection(string name) + { + this._name = name; + } + + public string Name => this._name; + public abstract void Reset(); + public abstract Dictionary GenerateSection(); + } +} diff --git a/logger/autorest.csharp/mgmt/Report/ResourceItem.cs b/logger/autorest.csharp/mgmt/Report/ResourceItem.cs new file mode 100644 index 0000000..aacb2aa --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/ResourceItem.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using AutoRest.CSharp.Mgmt.Decorator; +using AutoRest.CSharp.Mgmt.Output; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class ResourceItem : TransformableItem + { + public ResourceItem(Resource resource, TransformSection transformSection) + :base(resource.ResourceName, transformSection) + { + this.Name = resource.ResourceName; + this.ContextPaths = + resource.AllOperations.SelectMany(cop => cop.Select(rop => rop.ContextualPath.ToString())).Distinct().ToList(); + this.RequestPath = resource.RequestPath.ToString(); + this.isScopedResource = resource.RequestPath.GetScopePath().IsParameterizedScope(); + if (isScopedResource) + { + var scopeTypes = resource.RequestPath.GetParameterizedScopeResourceTypes(); + if (scopeTypes != null && scopeTypes.Length > 0) + this.ScopeResourceTypes = scopeTypes.Select(st => st.ToString() ?? "").ToList(); + } + this.ResourceType = resource.ResourceType.ToString() ?? ""; + this.IsSingleton = resource.IsSingleton; + if (resource.SingletonResourceIdSuffix != null) + this.SingletonSuffix = resource.SingletonResourceIdSuffix.ToString(); + this.Operations = resource.AllOperations + .GroupBy(op => op.MethodSignature.Name) + .OrderBy(g => g.Key) + .ToDictionary( + g => g.Key, + g => g.SelectMany(op => op.Select(mrop => new OperationItem(mrop, transformSection))).Distinct().ToList()); + // assume there is no circle in resource hirachy. TODO: handle it if it's not true + this.ChildResources = resource.ChildResources.Select(r => r.ResourceName).ToList(); + this.ParentResources = resource.GetParents().Select(r => r.ResourceName).ToList(); + } + + [YamlIgnore] + [JsonIgnore] + public string Name { get; set; } + public List ContextPaths { get; set; } = new List(); + public string RequestPath { get; set; } = string.Empty; + public string ResourceType { get; set; } = ""; + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool isScopedResource { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? ScopeResourceTypes { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsSingleton { get; set; } = false; + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SingletonSuffix { get; set; } + public Dictionary> Operations { get; set; } = new Dictionary>(); + public List ChildResources { get; set; } = new List(); + public List ParentResources { get; set; } = new List(); + } +} diff --git a/logger/autorest.csharp/mgmt/Report/TransformItem.cs b/logger/autorest.csharp/mgmt/Report/TransformItem.cs new file mode 100644 index 0000000..dcc46d4 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/TransformItem.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class TransformItem : IEquatable + { + private const string SEP = ">>"; + + public TransformItem(string transformType, string key, bool isFromConfig, params string[] arguments) + { + TransformType = transformType; + Key = key; + Arguments = arguments; + IsFromConfig = isFromConfig; + } + + public TransformItem(string transformType, string key, params string[] arguments) + : this(transformType, key, false, arguments) + { + } + + public string TransformType { get; set; } + public string Key { get; set; } + public string[] Arguments { get; set; } + [YamlIgnore] + [JsonIgnore] + public string ArgumentsAsString + { + get { return string.Join(SEP, this.Arguments); } + } + public bool IsFromConfig { get; set; } + + public override string ToString() + { + return $"<{TransformType}> {Key}{(this.Arguments.Length > 0 ? $":{this.ArgumentsAsString}" : "")}{(this.IsFromConfig ? "" : "!")}"; + } + + public override bool Equals(object? obj) + { + return Equals(obj as TransformItem); + } + + public bool Equals(TransformItem? other) + { + if (other == null) + return false; + if (TransformType != other.TransformType || Key != other.Key || Arguments.Length != other.Arguments.Length) + return false; + for (int i = 0; i < Arguments.Length; i++) + if (Arguments[i] != other.Arguments[i]) + return false; + return true; + } + + public override int GetHashCode() + { + return HashCode.Combine(TransformType, Key, ArgumentsAsString); + } + + public static bool operator ==(TransformItem? left, TransformItem? right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(TransformItem? left, TransformItem? right) + { + return !(left == right); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/TransformLog.cs b/logger/autorest.csharp/mgmt/Report/TransformLog.cs new file mode 100644 index 0000000..b3f394d --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/TransformLog.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class TransformLog + { + public TransformLog(int index, string targetFullSerializedName, string logMessage) + { + Index = index; + LogMessage = logMessage; + TargetFullSerializedName = targetFullSerializedName; + } + + public int Index { get; } + public string LogMessage { get; } + public string TargetFullSerializedName { get; } + + public override string ToString() + { + return $"[{Index}][{TargetFullSerializedName}]: {LogMessage}"; + } + + public TransformLog Clone() + { + return new TransformLog(Index, TargetFullSerializedName, LogMessage); + } + } +} diff --git a/logger/autorest.csharp/mgmt/Report/TransformSection.cs b/logger/autorest.csharp/mgmt/Report/TransformSection.cs new file mode 100644 index 0000000..6804f30 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/TransformSection.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using AutoRest.CSharp.Common.Utilities; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class TransformSection : ReportSection + { + private Dictionary> _transformItemDict = new Dictionary>(); + private int _logIndex = 0; + public TransformSection(string name) + : base(name) + { + } + + public IEnumerable<(TransformItem Transform, TransformLog Log)> GetAppliedTransformLogs(string targetSerializedName, HashSet? transformTypes = null) + { + foreach (var (transform, logs) in _transformItemDict) + { + if (transformTypes == null || transformTypes.Contains(transform.TransformType)) + { + foreach (var log in logs) + { + if (log.TargetFullSerializedName == targetSerializedName) + yield return (transform, log); + } + } + } + } + + public override Dictionary GenerateSection() + { + Dictionary r = new Dictionary(); + foreach (var group in _transformItemDict.GroupBy(item => item.Key.TransformType).OrderBy(g => g.Key)) + { + var key = group.Key; + Dictionary> value = new Dictionary>(); + foreach (var (item, logs) in group.OrderBy(kv => kv.Value.Count == 0 ? 0 : 1).ThenBy(kv => kv.Key.Key)) + { + List logList = logs.Select(log => $"[{log.Index}][{(item.Key == log.TargetFullSerializedName ? "=" : log.TargetFullSerializedName)}]: {log.LogMessage}").ToList(); + if (logList.Count == 0) + logList.Add(""); + value.Add( + $"{item.Key}{(string.IsNullOrEmpty(item.ArgumentsAsString) ? "" : ":" + item.ArgumentsAsString)}{(item.IsFromConfig ? "" : "!")}", + logList); + } + r.Add(key, value); + } + return r; + } + + public override void Reset() + { + this._transformItemDict = new Dictionary>(); + this._logIndex = 0; + } + + public void AddTransformer(TransformItem item) + { + if (this._transformItemDict.ContainsKey(item)) + { + AutoRestLogger.Warning($"Duplicate transform detected: {item}").Wait(); + return; + } + this._transformItemDict.Add(item, new List()); + } + + public void AddTransformer(string type, string key, bool fromConfig, params string[] arguments) + { + var item = new TransformItem(type, key, fromConfig, arguments); + this.AddTransformer(item); + } + + public void AddTransformers(IEnumerable items) + { + foreach (var item in items) + this.AddTransformer(item); + } + + private int GetNextLogIndex() + { + return this._logIndex++; + } + + public void AddTransformLog(TransformItem item, string targetFullSerializedName, string logMessage) + { + if (!_transformItemDict.ContainsKey(item)) + this.AddTransformer(item); + _transformItemDict[item].Add(new TransformLog(GetNextLogIndex(), targetFullSerializedName, logMessage)); + } + + public void AddTransformLogForApplyChange(TransformItem item, string targetFullSerializedName, string changeName, string? from, string? to) + { + this.AddTransformLog(item, targetFullSerializedName, CreateChangeMessage(changeName, from, to)); + } + + public void AddTransformLogForApplyChange(string transformType, string key, string argument, string targetFullSerializedName, string changeName, string? from, string? to) + { + this.AddTransformLog(new TransformItem(transformType, key, argument), targetFullSerializedName, CreateChangeMessage(changeName, from, to)); + } + + private string CreateChangeMessage(string changeName, string? from, string? to) => $"{changeName} '{from ?? ""}' --> '{to ?? ""}'"; + + public void ForEachTransform(Action> action) + { + foreach (var (transform, logs) in this._transformItemDict) + action(transform, logs.AsReadOnly()); + } + } + +} diff --git a/logger/autorest.csharp/mgmt/Report/TransformTypeName.cs b/logger/autorest.csharp/mgmt/Report/TransformTypeName.cs new file mode 100644 index 0000000..ebc3bf9 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/TransformTypeName.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class TransformTypeName + { + #region Transforms configured in MgmtConfiguration + public const string RenameMapping = "rename-mapping"; + public const string ParameterRenameMapping = "parameter-rename-mapping"; + public const string AcronymMapping = "acronym-mapping"; + public const string FormatByNameRules = "format-by-name-rules"; + public const string KeepPluralEnums = "keep-plural-enums"; + public const string OperationGroupsToOmit = "operation-groups-to-omit"; + public const string PrivilegedOperations = "privileged-operations"; + public const string RequestPathIsNonResource = "request-path-is-non-resource"; + public const string NoPropertyTypeReplacement = "no-property-type-replacement"; + public const string KeepPluralResourceData = "keep-plural-resource-data"; + public const string NoResourceSuffix = "no-resource-suffix"; + public const string PrependRpPrefix = "prepend-rp-prefix"; + public const string OverrideOperationName = "override-operation-name"; + #endregion + + #region Other Transforms + public const string ReplacePropertyType = "replac-property-type"; + public const string ReplaceBaseType = "replac-base-type"; + public const string ReplaceTypeWhenInitializingModel = "replace-type-when-initializing-model"; + public const string UpdateBodyParameter = "update-body-parameter"; + #endregion + } +} diff --git a/logger/autorest.csharp/mgmt/Report/TransformableItem.cs b/logger/autorest.csharp/mgmt/Report/TransformableItem.cs new file mode 100644 index 0000000..de27136 --- /dev/null +++ b/logger/autorest.csharp/mgmt/Report/TransformableItem.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace AutoRest.CSharp.Mgmt.Report +{ + internal class TransformableItem + { + private TransformSection _transformSection; + public TransformableItem(string fullSerializedName, TransformSection transformSection) + { + this.FullSerializedName = fullSerializedName; + this._transformSection = transformSection; + } + + public string FullSerializedName { get; set; } + protected virtual HashSet? TransformTypeAllowList { get { return null; } } + + private List? _appliedTransformLogs; + + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections | DefaultValuesHandling.OmitNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? TransformLogs + { + get + { + { + if (_appliedTransformLogs is null) + { + _appliedTransformLogs = _transformSection.GetAppliedTransformLogs( + this.FullSerializedName, this.TransformTypeAllowList) + .OrderBy(item => item.Log.Index) + .Select(item => $"[{item.Log.Index}][{item.Transform}] {item.Log.LogMessage}").ToList(); + } + // return null when it's an empty list so that it will be ignored in Json + return _appliedTransformLogs.Count == 0 ? null : _appliedTransformLogs; + } + } + } + } +} diff --git a/logger/typespec.extension/Emitter.Csharp/.prettierrc.json b/logger/typespec.extension/Emitter.Csharp/.prettierrc.json new file mode 100644 index 0000000..679ada2 --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": true, + "trailingComma": "none", + "printWidth": 80, + "endOfLine": "auto" +} \ No newline at end of file diff --git a/logger/typespec.extension/Emitter.Csharp/eslint.config.js b/logger/typespec.extension/Emitter.Csharp/eslint.config.js new file mode 100644 index 0000000..65ec19b --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/eslint.config.js @@ -0,0 +1,20 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { + languageOptions: { globals: globals.node } + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off" + } + }, + { + ignores: ["dist/"] + } +]; diff --git a/logger/typespec.extension/Emitter.Csharp/package.json b/logger/typespec.extension/Emitter.Csharp/package.json new file mode 100644 index 0000000..111402b --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/package.json @@ -0,0 +1,83 @@ +{ + "name": "@azure-tools/typespec-csharp", + "version": "0.2.0", + "author": "Microsoft Corporation", + "description": "The typespec library that can be used to generate C# models from a TypeSpec REST protocol binding", + "homepage": "https://github.com/Microsoft/typespec", + "readme": "https://github.com/Azure/autorest.csharp/blob/main/src/TypeSpec.Extension/Emitter.Csharp/readme.md", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Azure/autorest.csharp.git" + }, + "bugs": { + "url": "https://github.com/Azure/autorest.csharp/issues" + }, + "keywords": [ + "typespec" + ], + "type": "module", + "main": "dist/src/index.js", + "scripts": { + "clean": "rimraf ./dist ./temp", + "build": "tsc -p .", + "watch": "tsc -p . --watch", + "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", + "test": "vitest run", + "test-official": "vitest run --coverage --reporter=junit --reporter=default", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix", + "prettier:fix": "prettier --write --config .prettierrc.json ./src/**/*.ts", + "prettier": "prettier --check --config .prettierrc.json ./src/**/*.ts" + }, + "files": [ + "dist/src/**" + ], + "dependencies": { + "@autorest/csharp": "3.0.0-beta.20240625.4", + "json-serialize-refs": "0.1.0-0", + "@typespec/http-client-csharp": "0.1.9-alpha.20241122.1" + }, + "peerDependencies": { + "@azure-tools/typespec-azure-core": ">=0.48.0 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.48.1 <1.0.0", + "@azure-tools/typespec-azure-resource-manager": ">=0.48.0 <1.0.0", + "@azure-tools/typespec-azure-rulesets": ">=0.48.0 <1.0.0", + "@azure-tools/typespec-autorest": ">=0.48.0 <1.0.0", + "@typespec/compiler": ">=0.62.0 <1.0.0", + "@typespec/http": ">=0.62.0 <1.0.0", + "@typespec/openapi": ">=0.62.0 <1.0.0", + "@typespec/rest": ">=0.62.0 <1.0.0", + "@typespec/versioning": ">=0.62.0 <1.0.0", + "@typespec/xml": ">=0.62.0 <1.0.0" + }, + "devDependencies": { + "@azure-tools/typespec-azure-core": "0.48.0", + "@azure-tools/typespec-client-generator-core": "0.48.1", + "@azure-tools/typespec-azure-resource-manager": "0.48.0", + "@azure-tools/typespec-azure-rulesets": "0.48.0", + "@azure-tools/typespec-autorest": "0.48.0", + "@eslint/js": "^9.2.0", + "@types/lodash.isequal": "^4.5.6", + "@types/mocha": "~9.1.0", + "@types/node": "~22.7.5", + "@types/prettier": "^2.6.3", + "@typespec/compiler": "0.62.0", + "@typespec/http": "0.62.0", + "@typespec/json-schema": "0.62.0", + "@typespec/library-linter": "0.62.0", + "@typespec/openapi": "0.62.0", + "@typespec/rest": "0.62.0", + "@typespec/versioning": "0.62.0", + "@typespec/xml": "0.62.0", + "c8": "~7.11.0", + "eslint": "^8.57.0", + "globals": "^15.2.0", + "lodash.isequal": "^4.5.0", + "prettier": "~3.0.3", + "rimraf": "~3.0.2", + "typescript": "~5.6.3", + "typescript-eslint": "^7.8.0", + "vitest": "^1.4.0" + } +} diff --git a/logger/typespec.extension/Emitter.Csharp/readme.md b/logger/typespec.extension/Emitter.Csharp/readme.md new file mode 100644 index 0000000..0eabeb9 --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/readme.md @@ -0,0 +1,81 @@ +# TypeSpec csharp emitter library + +This is a TypeSpec library that will emit a .NET SDK from TypeSpec. + +## Prerequisite + +Install [Node.js](https://nodejs.org/en/download/) 16 or above. (Verify by `node --version`) +Install **.NET 6.0 SDK** for your specific platform. (or a higher version) +## Getting started + +### Initialize TypeSpec Project + +Follow [TypeSpec Getting Started](https://github.com/microsoft/typespec/#using-node--npm) to initialize your TypeSpec project. + +Make sure `npx tsp compile .` runs correctly. + +### Add typespec-csharp + +Include @azure-tools/typespec-csharp dependencies in `package.json` + +```diff + "dependencies": { ++ "@azure-tools/typespec-csharp": "latest" + }, +``` + +Run `npm install` to install the dependency + +### Generate .NET SDK + +Run command `npx tsp compile --emit @azure-tools/typespec-csharp ` + +e.g. + +```cmd +npx tsp compile main.tsp --emit @azure-tools/typespec-csharp +``` + +or + +```cmd +npx tsp compile client.tsp --emit @azure-tools/typespec-csharp +``` + +## Configuration + +You can further configure the SDK generated, using the emitter options on @azure-tools/typespec-csharp. + +You can set options in the command line directly via `--option @azure-tools/typespec-csharp.=XXX`, e.g. `--option @azure-tools/typespec-csharp.namespace=azure.AI.DeviceUpdate` + +or + +Modify `tspconfig.yaml` in typespec project, add emitter options under options/@azure-tools/typespec-csharp. + +```diff +emit: + - "@azure-tools/typespec-csharp" +options: + "@azure-tools/typespec-csharp": ++ namespace: Azure.Template.MyTypeSpecProject +``` + +**Supported Emitter options**: +- `namespace` define the client library namespace. e.g. Azure.IoT.DeviceUpdate. +- `emitter-output-dir` define the output dire path which will store the generated code. +- `generate-protocol-methods` indicate if you want to generate **protocol method** for every operation or not. The default value is true. +- `generate-convenience-methods` indicate if you want to generate **convenience method** for every operation or not. The default value is true. +- `unreferenced-types-handling` define the strategy how to handle the unreferenced types. It can be `removeOrInternalize`, `internalize` or `keepAll` +- `model-namespace` indicate if we want to put the models in their own namespace which is a sub namespace of the client library namespace plus ".Models". if it is set `false`, the models will be put in the same namespace of the client. The default value is `true`. +- `clear-output-folder` indicate if you want to clear up the output folder. +- `package-name` define the package folder name which will be used as service directory name under `sdk/` in azure-sdk-for-net repo. +- `save-inputs` indicate if you want to keep the intermediate files for debug purpose, e.g. the model json file parsed from typespec file. + +## Convenience API + +By default, TypeSpec-csharp generates all protocol APIs and convenience APIs. +A few exceptions are API of JSON Merge Patch, and API of long-running operation with ambiguous response type. + +You can configure whether generate convenience API or not via `convenienceAPI` decorator. + +See "convenientAPI" decorator from [typespec-client-generator-core](https://github.com/Azure/typespec-azure/tree/main/packages/typespec-client-generator-core). diff --git a/logger/typespec.extension/Emitter.Csharp/src/emitter.ts b/logger/typespec.extension/Emitter.Csharp/src/emitter.ts new file mode 100644 index 0000000..c9ef372 --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/src/emitter.ts @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import { EmitContext, Program, resolvePath } from "@typespec/compiler"; + +import { execSync } from "child_process"; +import fs, { existsSync } from "fs"; +import path from "node:path"; +import { + $onEmit as $OnMGCEmit, + Logger, + LoggerLevel, + configurationFileName, + resolveOutputFolder, + tspOutputFileName, + setSDKContextOptions +} from "@typespec/http-client-csharp"; +import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; +import { + AzureNetEmitterOptions, + resolveAzureEmitterOptions +} from "./options.js"; +import { azureSDKContextOptions } from "./sdk-context-options.js"; + +export async function $onEmit(context: EmitContext) { + const program: Program = context.program; + + if (program.compilerOptions.noEmit || program.hasError()) return; + + const options = resolveAzureEmitterOptions(context); + /* set the loglevel. */ + Logger.initialize(program, options.logLevel ?? LoggerLevel.INFO); + + context.options.skipSDKGeneration = true; + if ( + context.emitterOutputDir && + path.basename(context.emitterOutputDir) === "src" + ) { + context.emitterOutputDir = path.dirname(context.emitterOutputDir); + } + Logger.getInstance().info("Starting Microsoft Generator Csharp emitter."); + setSDKContextOptions(azureSDKContextOptions); + await $OnMGCEmit(context); + const outputFolder = resolveOutputFolder(context); + const isSrcFolder = path.basename(outputFolder) === "src"; + const generatedFolder = isSrcFolder + ? resolvePath(outputFolder, "Generated") + : resolvePath(outputFolder, "src", "Generated"); + if (options.skipSDKGeneration !== true) { + const configurationFilePath = resolvePath( + outputFolder, + configurationFileName + ); + const configurations = JSON.parse( + fs.readFileSync(configurationFilePath, "utf-8") + ); + //resolve shared folders based on generator path override + const resolvedSharedFolders: string[] = []; + const sharedFolders = [ + resolvePath(options.csharpGeneratorPath, "..", "Generator.Shared"), + resolvePath(options.csharpGeneratorPath, "..", "Azure.Core.Shared") + ]; + for (const sharedFolder of sharedFolders) { + resolvedSharedFolders.push( + path + .relative(generatedFolder, sharedFolder) + .replaceAll("\\", "/") + ); + } + configurations["shared-source-folders"] = resolvedSharedFolders ?? []; + configurations["flavor"] = + options["flavor"] ?? + (configurations.namespace.toLowerCase().startsWith("azure.") + ? "azure" + : undefined); + configurations["enable-internal-raw-data"] = + options["enable-internal-raw-data"]; + const examplesDir = + options["examples-dir"] ?? options["examples-directory"]; + if (examplesDir) { + configurations["examples-dir"] = path.relative( + outputFolder, + examplesDir + ); + } + /* TODO: when we support to emit decorator list https://github.com/Azure/autorest.csharp/issues/4887, we will update to use emitted decorator to identify if it is azure-arm */ + /* set azure-arm */ + const sdkContext = await createSdkContext( + context, + "@azure-tools/typespec-csharp" + ); + configurations["azure-arm"] = + sdkContext.arm === false ? undefined : sdkContext.arm; + await program.host.writeFile( + resolvePath(outputFolder, configurationFileName), + prettierOutput(JSON.stringify(configurations, null, 2)) + ); + + const csProjFile = resolvePath( + isSrcFolder ? outputFolder : resolvePath(outputFolder, "src"), + `${configurations["library-name"]}.csproj` + ); + Logger.getInstance().info(`Checking if ${csProjFile} exists`); + const newProjectOption = + options["new-project"] || !existsSync(csProjFile) + ? "--new-project" + : ""; + const existingProjectOption = options["existing-project-folder"] + ? `--existing-project-folder ${options["existing-project-folder"]}` + : ""; + const debugFlag = options.debug ?? false ? " --debug" : ""; + + const command = `dotnet --roll-forward Major ${resolvePath( + options.csharpGeneratorPath + )} --project-path ${outputFolder} ${newProjectOption} ${existingProjectOption} --clear-output-folder ${ + options["clear-output-folder"] + }${debugFlag}`; + Logger.getInstance().info(command); + + try { + execSync(command, { stdio: "inherit" }); + } catch (error: any) { + if (error.message) Logger.getInstance().info(error.message); + if (error.stderr) Logger.getInstance().error(error.stderr); + if (error.stdout) Logger.getInstance().verbose(error.stdout); + throw error; + } + if (!options["save-inputs"]) { + // delete + deleteFile(resolvePath(outputFolder, tspOutputFileName)); + deleteFile(resolvePath(outputFolder, configurationFileName)); + } + } +} + +function deleteFile(filePath: string) { + fs.unlink(filePath, (err) => { + if (err) { + //logger.error(`stderr: ${err}`); + } else { + Logger.getInstance().info(`File ${filePath} is deleted.`); + } + }); +} + +function prettierOutput(output: string) { + return output + "\n"; +} diff --git a/logger/typespec.extension/Emitter.Csharp/src/index.ts b/logger/typespec.extension/Emitter.Csharp/src/index.ts new file mode 100644 index 0000000..a40d9ea --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/src/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +export * from "./emitter.js"; diff --git a/logger/typespec.extension/Emitter.Csharp/src/options.ts b/logger/typespec.extension/Emitter.Csharp/src/options.ts new file mode 100644 index 0000000..a37e7d3 --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/src/options.ts @@ -0,0 +1,52 @@ +import { EmitContext, JSONSchemaType } from "@typespec/compiler"; +import { + NetEmitterOptions, + NetEmitterOptionsSchema, + defaultOptions, + resolveOptions +} from "@typespec/http-client-csharp"; +import { dllFilePath } from "@autorest/csharp"; + +export interface AzureNetEmitterOptions extends NetEmitterOptions { + csharpGeneratorPath?: string; + "enable-internal-raw-data"?: boolean; +} + +export const AzureNetEmitterOptionsSchema: JSONSchemaType = + { + type: "object", + additionalProperties: false, + properties: { + ...NetEmitterOptionsSchema.properties, + csharpGeneratorPath: { + type: "string", + default: dllFilePath, + nullable: true + }, + "enable-internal-raw-data": { + type: "boolean", + default: false + } + }, + required: [] + }; + +const defaultAzureEmitterOptions = { + ...defaultOptions, + csharpGeneratorPath: dllFilePath, + "enable-internal-raw-data": undefined +}; + +export function resolveAzureEmitterOptions( + context: EmitContext +) { + return { + ...resolveOptions(context), + csharpGeneratorPath: + context.options.csharpGeneratorPath ?? + defaultAzureEmitterOptions.csharpGeneratorPath, + "enable-internal-raw-data": + context.options["enable-internal-raw-data"] ?? + defaultAzureEmitterOptions["enable-internal-raw-data"] + }; +} diff --git a/logger/typespec.extension/Emitter.Csharp/src/sdk-context-options.ts b/logger/typespec.extension/Emitter.Csharp/src/sdk-context-options.ts new file mode 100644 index 0000000..4d67e13 --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/src/sdk-context-options.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import { CreateSdkContextOptions } from "@azure-tools/typespec-client-generator-core"; + +export const azureSDKContextOptions: CreateSdkContextOptions = { + versioning: {}, + additionalDecorators: [ + "Azure\\.ClientGenerator\\.Core\\.@useSystemTextJsonConverter" + ] +}; diff --git a/logger/typespec.extension/Emitter.Csharp/tsconfig.json b/logger/typespec.extension/Emitter.Csharp/tsconfig.json new file mode 100644 index 0000000..41fdce8 --- /dev/null +++ b/logger/typespec.extension/Emitter.Csharp/tsconfig.json @@ -0,0 +1,32 @@ + +{ + "compilerOptions": { + "composite": true, + "alwaysStrict": true, + "forceConsistentCasingInFileNames": true, + "preserveConstEnums": true, + "module": "node16", + "moduleResolution": "node16", + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "sourceMap": true, + "declarationMap": true, + "strict": true, + "declaration": true, + "stripInternal": true, + "noEmitHelpers": false, + "target": "es2021", + "lib": ["es2021"], + "experimentalDecorators": true, + "newLine": "LF", + "outDir": "dist", + "rootDir": ".", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "types": ["node", "mocha"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} + \ No newline at end of file diff --git a/logger/typespec.extension/codemodel.png b/logger/typespec.extension/codemodel.png new file mode 100644 index 0000000..52a3b56 Binary files /dev/null and b/logger/typespec.extension/codemodel.png differ diff --git a/logger/typespec.extension/flow.png b/logger/typespec.extension/flow.png new file mode 100644 index 0000000..2b46b0a Binary files /dev/null and b/logger/typespec.extension/flow.png differ