diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 84ad4f50cf..692605cec8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -21,3 +21,11 @@ ###################### /eng/emitters/ @m-nash +###################### +# IDE +###################### +/packages/typespec-vs/ @RodgeFu @bterlson @markcowl @allenjzhang @timotheeguerin +/packages/typespec-vscode/ @RodgeFu @bterlson @markcowl @allenjzhang @timotheeguerin +/packages/compiler/src/server/ @RodgeFu @bterlson @markcowl @allenjzhang @timotheeguerin + + diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 53dd9ba593..15c5376c86 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -733,24 +733,29 @@ private List BuildDeserializePropertiesStatements(ScopedApi for (int i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; - if (parameter.Property is { } property) + if (parameter.Property != null || parameter.Field != null) { // handle additional properties - if (property != _additionalBinaryDataProperty && property.IsAdditionalProperties) + if (parameter.Property != null && parameter.Property != _additionalBinaryDataProperty && parameter.Property.IsAdditionalProperties) { - AddAdditionalPropertiesValueKindStatements(additionalPropsValueKindBodyStatements, property, jsonProperty); + AddAdditionalPropertiesValueKindStatements(additionalPropsValueKindBodyStatements, parameter.Property, jsonProperty); continue; } + var wireInfo = parameter.Property?.WireInfo ?? parameter.Field?.WireInfo; + // By default, we should only deserialize properties with wire info. Those properties without wire info indicate they are not spec properties. - if (property.WireInfo is not { } wireInfo) + if (wireInfo == null) { continue; } var propertySerializationName = wireInfo.SerializedName; + var propertyName = parameter.Property?.Name ?? parameter.Field?.Name; + var propertyType = parameter.Property?.Type ?? parameter.Field?.Type; + var propertyExpression = parameter.Property?.AsVariableExpression ?? parameter.Field?.AsVariableExpression; var checkIfJsonPropEqualsName = new IfStatement(jsonProperty.NameEquals(propertySerializationName)) { - DeserializeProperty(property, jsonProperty, serializationAttributes) + DeserializeProperty(propertyName!, propertyType!, wireInfo, propertyExpression!, jsonProperty, serializationAttributes) }; propertyDeserializationStatements.Add(checkIfJsonPropEqualsName); } @@ -1051,15 +1056,18 @@ private static SwitchStatement CreateDeserializeAdditionalPropsValueKindCheck( } private MethodBodyStatement[] DeserializeProperty( - PropertyProvider property, + string propertyName, + CSharpType propertyType, + PropertyWireInformation wireInfo, + VariableExpression variableExpression, ScopedApi jsonProperty, IEnumerable serializationAttributes) { - var serializationFormat = property.WireInfo?.SerializationFormat ?? SerializationFormat.Default; - var propertyVarReference = property.AsVariableExpression; + var serializationFormat = wireInfo.SerializationFormat; + var propertyVarReference = variableExpression; var deserializationStatements = new MethodBodyStatement[2] { - DeserializeValue(property.Type, jsonProperty.Value(), serializationFormat, out ValueExpression value), + DeserializeValue(propertyType, jsonProperty.Value(), serializationFormat, out ValueExpression value), propertyVarReference.Assign(value).Terminate() }; @@ -1071,7 +1079,7 @@ private MethodBodyStatement[] DeserializeProperty( out _, out _, out var deserializationHook, - out _) && name == property.Name && deserializationHook != null) + out _) && name == propertyName && deserializationHook != null) { deserializationStatements = [Static().Invoke( @@ -1084,7 +1092,7 @@ private MethodBodyStatement[] DeserializeProperty( return [ - DeserializationPropertyNullCheckStatement(property, jsonProperty, propertyVarReference), + DeserializationPropertyNullCheckStatement(propertyType, wireInfo, jsonProperty, propertyVarReference), deserializationStatements, Continue ]; @@ -1097,14 +1105,15 @@ private MethodBodyStatement[] DeserializeProperty( /// return a null check for the json property. /// private static MethodBodyStatement DeserializationPropertyNullCheckStatement( - PropertyProvider property, + CSharpType propertyType, + PropertyWireInformation wireInfo, ScopedApi jsonProperty, VariableExpression propertyVarRef) { // Produces: if (prop.Value.ValueKind == System.Text.Json.JsonValueKind.Null) var checkEmptyProperty = jsonProperty.Value().ValueKindEqualsNull(); - CSharpType serializedType = property.Type; - var propertyIsRequired = property.WireInfo?.IsRequired ?? false; + CSharpType serializedType = propertyType; + var propertyIsRequired = wireInfo.IsRequired; if (serializedType.IsNullable || !propertyIsRequired) { @@ -1311,89 +1320,110 @@ private static MethodBodyStatement ThrowValidationFailException(ValueExpression /// private MethodBodyStatement[] CreateWritePropertiesStatements() { - var properties = _model.CanonicalView.Properties; List propertyStatements = new(); - foreach (var property in properties) + foreach (var property in _model.CanonicalView.Properties) { // we should only write those properties with a wire info. Those properties without wireinfo indicate they are not spec properties. - if (property.WireInfo is not { } wireInfo) + if (property.WireInfo == null) { continue; } - var propertySerializationName = wireInfo.SerializedName; - var propertySerializationFormat = wireInfo.SerializationFormat; - var propertyIsReadOnly = wireInfo.IsReadOnly; - var propertyIsRequired = wireInfo.IsRequired; - var propertyIsNullable = wireInfo.IsNullable; - - // Generate the serialization statements for the property - var serializationStatement = CreateSerializationStatement(property.Type, property, propertySerializationFormat); - - // Check for custom serialization hooks - foreach (var attribute in _model.CustomCodeView?.GetAttributes() - .Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? []) + + propertyStatements.Add(CreateWritePropertyStatement(property.WireInfo, property.Type, property.Name, property)); + } + + foreach (var field in _model.CanonicalView.Fields) + { + // we should only write those properties with a wire info. Those properties without wireinfo indicate they are not spec properties. + if (field.WireInfo == null) { - if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue( - attribute, - out var name, - out _, - out var serializationHook, - out _, - out _) && name == property.Name && serializationHook != null) - { - serializationStatement = This.Invoke( - serializationHook, - _utf8JsonWriterSnippet, - _serializationOptionsParameter) - .Terminate(); - } + continue; } - var writePropertySerializationStatements = new MethodBodyStatement[] - { - _utf8JsonWriterSnippet.WritePropertyName(propertySerializationName), - serializationStatement - }; + propertyStatements.Add(CreateWritePropertyStatement(field.WireInfo, field.Type, field.Name, field)); + } - // Wrap the serialization statement in a check for whether the property is defined - var wrapInIsDefinedStatement = WrapInIsDefined(property, property, propertyIsRequired, propertyIsReadOnly, propertyIsNullable, writePropertySerializationStatements); - if (propertyIsReadOnly && wrapInIsDefinedStatement is not IfStatement) + return [.. propertyStatements]; + } + + private MethodBodyStatement CreateWritePropertyStatement( + PropertyWireInformation wireInfo, + CSharpType propertyType, + string propertyName, + MemberExpression propertyExpression) + { + var propertySerializationName = wireInfo.SerializedName; + var propertySerializationFormat = wireInfo.SerializationFormat; + var propertyIsReadOnly = wireInfo.IsReadOnly; + var propertyIsRequired = wireInfo.IsRequired; + var propertyIsNullable = wireInfo.IsNullable; + + // Generate the serialization statements for the property + var serializationStatement = CreateSerializationStatement(propertyType, propertyExpression, propertySerializationFormat); + + // Check for custom serialization hooks + foreach (var attribute in _model.CustomCodeView?.GetAttributes() + .Where(a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? []) + { + if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue( + attribute, + out var name, + out _, + out var serializationHook, + out _, + out _) && name == propertyName && serializationHook != null) { - wrapInIsDefinedStatement = new IfStatement(_isNotEqualToWireConditionSnippet) - { - wrapInIsDefinedStatement - }; + serializationStatement = This.Invoke( + serializationHook, + _utf8JsonWriterSnippet, + _serializationOptionsParameter) + .Terminate(); } - propertyStatements.Add(wrapInIsDefinedStatement); } - return [.. propertyStatements]; + var writePropertySerializationStatements = new MethodBodyStatement[] + { + _utf8JsonWriterSnippet.WritePropertyName(propertySerializationName), + serializationStatement + }; + + // Wrap the serialization statement in a check for whether the property is defined + var wrapInIsDefinedStatement = WrapInIsDefined( + propertyExpression, + propertyType, + wireInfo, + propertyIsRequired, + propertyIsReadOnly, + propertyIsNullable, + writePropertySerializationStatements); + if (propertyIsReadOnly && wrapInIsDefinedStatement is not IfStatement) + { + wrapInIsDefinedStatement = new IfStatement(_isNotEqualToWireConditionSnippet) + { + wrapInIsDefinedStatement + }; + } + + return wrapInIsDefinedStatement; } - /// - /// Wraps the serialization statement in a condition check to ensure only initialized and required properties are serialized. - /// - /// The model property. - /// The expression representing the property to serialize. - /// The serialization statement to conditionally execute. - /// A method body statement that includes condition checks before serialization. private MethodBodyStatement WrapInIsDefined( - PropertyProvider propertyProvider, - MemberExpression propertyMemberExpression, + MemberExpression propertyExpression, + CSharpType propertyType, + PropertyWireInformation wireInfo, bool propertyIsRequired, bool propertyIsReadOnly, bool propertyIsNullable, MethodBodyStatement writePropertySerializationStatement) { - var propertyType = propertyProvider.Type; - // Create the first conditional statement to check if the property is defined if (propertyIsNullable) { writePropertySerializationStatement = CheckPropertyIsInitialized( - propertyProvider, + propertyType, + wireInfo, propertyIsRequired, - propertyMemberExpression, + propertyExpression, writePropertySerializationStatement); } @@ -1404,18 +1434,16 @@ private MethodBodyStatement WrapInIsDefined( } // Conditionally serialize based on whether the property is a collection or a single value - return CreateConditionalSerializationStatement(propertyType, propertyMemberExpression, propertyIsReadOnly, writePropertySerializationStatement); + return CreateConditionalSerializationStatement(propertyType, propertyExpression, propertyIsReadOnly, writePropertySerializationStatement); } private IfElseStatement CheckPropertyIsInitialized( - PropertyProvider propertyProvider, + CSharpType propertyType, + PropertyWireInformation wireInfo, bool isPropRequired, MemberExpression propertyMemberExpression, MethodBodyStatement writePropertySerializationStatement) { - var propertyType = propertyProvider.Type; - var propertySerialization = propertyProvider.WireInfo; - var propertyName = propertySerialization?.SerializedName ?? propertyProvider.Name; ScopedApi propertyIsInitialized; if (propertyType.IsCollection && !propertyType.IsReadOnlyMemory && isPropRequired) @@ -1431,7 +1459,7 @@ private IfElseStatement CheckPropertyIsInitialized( return new IfElseStatement( propertyIsInitialized, writePropertySerializationStatement, - _utf8JsonWriterSnippet.WriteNull(propertyName.ToVariableName())); + _utf8JsonWriterSnippet.WriteNull(wireInfo.SerializedName.ToVariableName())); } /// diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs index b48d273ef3..ce5de2e76f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs @@ -84,5 +84,26 @@ public async Task CanSerializeCustomPropertyFromBase() Assert.IsNotNull(fullCtor); Assert.IsTrue(fullCtor!.Signature.Parameters.Any(p => p.Name == "prop1")); } + + [Test] + public async Task CanCustomizePropertyUsingField() + { + var props = new[] + { + InputFactory.Property("Prop1", InputPrimitiveType.String), + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json); + var plugin = await MockHelpers.LoadMockPluginAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = plugin.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + + Assert.AreEqual(0, modelProvider.Properties.Count); + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs index 6dfbe6c893..82eafb72d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SerializationCustomizationTests.cs @@ -242,6 +242,38 @@ public async Task CanCustomizeExtensibleEnum(InputPrimitiveType enumType) Assert.AreEqual(Helpers.GetExpectedFromFile(enumType.Name), file.Content); } + private static IEnumerable ExtensibleEnumCasesFromLiteral => + [ + new TestCaseData(InputPrimitiveType.String, "foo"), + new TestCaseData(InputPrimitiveType.Int32, 1), + ]; + + [TestCaseSource(nameof(ExtensibleEnumCasesFromLiteral))] + public async Task CanCustomizeLiteralExtensibleEnum(InputPrimitiveType enumType, object value) + { + var props = new[] + { + InputFactory.Property("Prop1", InputFactory.Literal.Enum( + InputFactory.Enum("EnumType", enumType, isExtensible: true), + value: value)) + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json); + var parameters = $"{enumType.Name},{value}"; + var plugin = await MockHelpers.LoadMockPluginAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters)); + + var modelProvider = plugin.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + Assert.IsNotNull(serializationProvider); + Assert.AreEqual(0, serializationProvider.Fields.Count); + + var writer = new TypeProviderWriter(serializationProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(parameters), file.Content); + } + [Test] public async Task CanReplaceSerializationMethod() { @@ -299,5 +331,33 @@ public async Task CanReplaceDeserializationMethod() var file = writer.Write(); Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + + [TestCase(true)] + [TestCase(false)] + public async Task CanCustomizePropertyUsingField(bool redefineProperty) + { + var props = new[] + { + InputFactory.Property("Prop1", InputPrimitiveType.String), + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json); + var plugin = await MockHelpers.LoadMockPluginAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(redefineProperty.ToString())); + + var modelProvider = plugin.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + Assert.IsNotNull(serializationProvider); + Assert.AreEqual(0, modelProvider.Properties.Count); + + var writer = new TypeProviderWriter(serializationProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + + var fullCtor = modelProvider.Constructors.Last(); + Assert.IsTrue(fullCtor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.AreEqual(2, fullCtor.Signature.Parameters.Count); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/ModelCustomizationTests/CanCustomizePropertyUsingField.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/ModelCustomizationTests/CanCustomizePropertyUsingField.cs new file mode 100644 index 0000000000..a490209861 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/ModelCustomizationTests/CanCustomizePropertyUsingField.cs @@ -0,0 +1,26 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + /// mockInputModel description. + public partial class MockInputModel + { + /// Keeps track of any properties unknown to the library. + private protected global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + internal MockInputModel() + { + } + + internal MockInputModel(string myField, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + _myField = myField; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/ModelCustomizationTests/CanCustomizePropertyUsingField/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/ModelCustomizationTests/CanCustomizePropertyUsingField/MockInputModel.cs new file mode 100644 index 0000000000..b553440cf3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/ModelCustomizationTests/CanCustomizePropertyUsingField/MockInputModel.cs @@ -0,0 +1,11 @@ + +using Microsoft.Generator.CSharp.Customization; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("Prop1")] + private string _myField; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(int32,1).cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(int32,1).cs new file mode 100644 index 0000000000..4032f62c2b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(int32,1).cs @@ -0,0 +1,145 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using Sample; + +namespace Sample.Models +{ + /// + public partial class MockInputModel : global::System.ClientModel.Primitives.IJsonModel + { + void global::System.ClientModel.Primitives.IJsonModel.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + this.JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + /// The JSON writer. + /// The client options for reading and writing models. + protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{format}' format."); + } + writer.WritePropertyName("prop1"u8); + writer.WriteNumberValue(Prop1.ToSerialInt32()); + if (((options.Format != "W") && (_additionalBinaryDataProperties != null))) + { + foreach (var item in _additionalBinaryDataProperties) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value)) + { + global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IJsonModel.Create(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.JsonModelCreateCore(ref reader, options)); + + /// The JSON reader. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel JsonModelCreateCore(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{format}' format."); + } + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.ParseValue(ref reader); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + + internal static global::Sample.Models.MockInputModel DeserializeMockInputModel(global::System.Text.Json.JsonElement element, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + return null; + } + global::Sample.Models.EnumType prop1 = default; + global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); + foreach (var prop in element.EnumerateObject()) + { + if (prop.NameEquals("prop1"u8)) + { + if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + prop1 = null; + continue; + } + prop1 = new global::Sample.Models.EnumType(prop.Value.GetInt32()); + continue; + } + if ((options.Format != "W")) + { + additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText())); + } + } + return new global::Sample.Models.MockInputModel(prop1, additionalBinaryDataProperties); + } + + global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual global::System.BinaryData PersistableModelWriteCore(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + return global::System.ClientModel.Primitives.ModelReaderWriter.Write(this, options); + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{options.Format}' format."); + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IPersistableModel.Create(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.PersistableModelCreateCore(data, options)); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel PersistableModelCreateCore(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(data)) + { + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{options.Format}' format."); + } + } + + string global::System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; + + /// The to serialize into . + public static implicit operator BinaryContent(global::Sample.Models.MockInputModel mockInputModel) + { + return global::System.ClientModel.BinaryContent.Create(mockInputModel, global::Sample.ModelSerializationExtensions.WireOptions); + } + + /// The to deserialize the from. + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result) + { + using global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(int32,1)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(int32,1)/MockInputModel.cs new file mode 100644 index 0000000000..a27e763526 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(int32,1)/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using Microsoft.Generator.CSharp.Customization; +using Microsoft.Generator.CSharp.Primitives; + +namespace Sample.Models +{ + public partial class MockInputModel + { + public EnumType Prop1 { get; set; } + } + + public readonly partial struct EnumType + { + public static EnumType Foo = new EnumType(1); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(string,foo).cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(string,foo).cs new file mode 100644 index 0000000000..68d078fc24 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(string,foo).cs @@ -0,0 +1,145 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using Sample; + +namespace Sample.Models +{ + /// + public partial class MockInputModel : global::System.ClientModel.Primitives.IJsonModel + { + void global::System.ClientModel.Primitives.IJsonModel.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + this.JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + /// The JSON writer. + /// The client options for reading and writing models. + protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{format}' format."); + } + writer.WritePropertyName("prop1"u8); + writer.WriteStringValue(Prop1.ToString()); + if (((options.Format != "W") && (_additionalBinaryDataProperties != null))) + { + foreach (var item in _additionalBinaryDataProperties) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value)) + { + global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IJsonModel.Create(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.JsonModelCreateCore(ref reader, options)); + + /// The JSON reader. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel JsonModelCreateCore(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{format}' format."); + } + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.ParseValue(ref reader); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + + internal static global::Sample.Models.MockInputModel DeserializeMockInputModel(global::System.Text.Json.JsonElement element, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + return null; + } + global::Sample.Models.EnumType prop1 = default; + global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); + foreach (var prop in element.EnumerateObject()) + { + if (prop.NameEquals("prop1"u8)) + { + if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + prop1 = null; + continue; + } + prop1 = new global::Sample.Models.EnumType(prop.Value.GetString()); + continue; + } + if ((options.Format != "W")) + { + additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText())); + } + } + return new global::Sample.Models.MockInputModel(prop1, additionalBinaryDataProperties); + } + + global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual global::System.BinaryData PersistableModelWriteCore(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + return global::System.ClientModel.Primitives.ModelReaderWriter.Write(this, options); + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{options.Format}' format."); + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IPersistableModel.Create(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.PersistableModelCreateCore(data, options)); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel PersistableModelCreateCore(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(data)) + { + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{options.Format}' format."); + } + } + + string global::System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; + + /// The to serialize into . + public static implicit operator BinaryContent(global::Sample.Models.MockInputModel mockInputModel) + { + return global::System.ClientModel.BinaryContent.Create(mockInputModel, global::Sample.ModelSerializationExtensions.WireOptions); + } + + /// The to deserialize the from. + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result) + { + using global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(string,foo)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(string,foo)/MockInputModel.cs new file mode 100644 index 0000000000..a27e763526 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizeLiteralExtensibleEnum(string,foo)/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using Microsoft.Generator.CSharp.Customization; +using Microsoft.Generator.CSharp.Primitives; + +namespace Sample.Models +{ + public partial class MockInputModel + { + public EnumType Prop1 { get; set; } + } + + public readonly partial struct EnumType + { + public static EnumType Foo = new EnumType(1); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField(False)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField(False)/MockInputModel.cs new file mode 100644 index 0000000000..2be3bfab2c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField(False)/MockInputModel.cs @@ -0,0 +1,11 @@ + +using Microsoft.Generator.CSharp.Customization; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("Prop1")] + private string _prop1; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField(True)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField(True)/MockInputModel.cs new file mode 100644 index 0000000000..b690d64517 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField(True)/MockInputModel.cs @@ -0,0 +1,17 @@ + +using Microsoft.Generator.CSharp.Customization; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("Prop1")] + private string _prop1; + + public string Prop1 + { + get => _prop1; + set => _prop1 = value; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField.cs new file mode 100644 index 0000000000..cecef45f50 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/SerializationCustomizationTests/CanCustomizePropertyUsingField.cs @@ -0,0 +1,148 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using Sample; + +namespace Sample.Models +{ + /// + public partial class MockInputModel : global::System.ClientModel.Primitives.IJsonModel + { + void global::System.ClientModel.Primitives.IJsonModel.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + this.JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + /// The JSON writer. + /// The client options for reading and writing models. + protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{format}' format."); + } + if (global::Sample.Optional.IsDefined(_prop1)) + { + writer.WritePropertyName("prop1"u8); + writer.WriteStringValue(_prop1); + } + if (((options.Format != "W") && (_additionalBinaryDataProperties != null))) + { + foreach (var item in _additionalBinaryDataProperties) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value)) + { + global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IJsonModel.Create(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.JsonModelCreateCore(ref reader, options)); + + /// The JSON reader. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel JsonModelCreateCore(ref global::System.Text.Json.Utf8JsonReader reader, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{format}' format."); + } + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.ParseValue(ref reader); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + + internal static global::Sample.Models.MockInputModel DeserializeMockInputModel(global::System.Text.Json.JsonElement element, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + return null; + } + string prop1 = default; + global::System.Collections.Generic.IDictionary additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); + foreach (var prop in element.EnumerateObject()) + { + if (prop.NameEquals("prop1"u8)) + { + if ((prop.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + prop1 = null; + continue; + } + prop1 = prop.Value.GetString(); + continue; + } + if ((options.Format != "W")) + { + additionalBinaryDataProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText())); + } + } + return new global::Sample.Models.MockInputModel(prop1, additionalBinaryDataProperties); + } + + global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual global::System.BinaryData PersistableModelWriteCore(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + return global::System.ClientModel.Primitives.ModelReaderWriter.Write(this, options); + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support writing '{options.Format}' format."); + } + } + + global::Sample.Models.MockInputModel global::System.ClientModel.Primitives.IPersistableModel.Create(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => ((global::Sample.Models.MockInputModel)this.PersistableModelCreateCore(data, options)); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual global::Sample.Models.MockInputModel PersistableModelCreateCore(global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(data)) + { + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, options); + } + default: + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.MockInputModel)} does not support reading '{options.Format}' format."); + } + } + + string global::System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; + + /// The to serialize into . + public static implicit operator BinaryContent(global::Sample.Models.MockInputModel mockInputModel) + { + return global::System.ClientModel.BinaryContent.Create(mockInputModel, global::Sample.ModelSerializationExtensions.WireOptions); + } + + /// The to deserialize the from. + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result) + { + using global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs index dfb8c31108..83601d79b0 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/CanonicalTypeProvider.cs @@ -12,13 +12,19 @@ namespace Microsoft.Generator.CSharp.Providers { internal class CanonicalTypeProvider : TypeProvider { - private readonly InputModelType? _inputModel; private readonly TypeProvider _generatedTypeProvider; + private readonly Dictionary _specPropertiesMap; + private readonly Dictionary _serializedNameMap; + private readonly Dictionary _specToCustomFieldMap; public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inputType) { _generatedTypeProvider = generatedTypeProvider; - _inputModel = inputType as InputModelType; + var inputModel = inputType as InputModelType; + var specProperties = inputModel?.Properties ?? []; + _specPropertiesMap = specProperties.ToDictionary(p => p.Name.ToCleanName(), p => p); + _serializedNameMap = BuildSerializationNameMap(); + _specToCustomFieldMap = BuildSpecToCustomFieldMap(); } protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation"); @@ -36,32 +42,28 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu protected override PropertyProvider[] BuildProperties() { - var specProperties = _inputModel?.Properties ?? []; - var specPropertiesMap = specProperties.ToDictionary(p => p.Name.ToCleanName(), p => p); var generatedProperties = _generatedTypeProvider.Properties; var customProperties = _generatedTypeProvider.CustomCodeView?.Properties ?? []; - Dictionary serializedNameMapping = BuildSerializationNameMap(); - // Update the serializedName of generated properties if necessary foreach (var generatedProperty in generatedProperties) { - if (serializedNameMapping.TryGetValue(generatedProperty.Name, out var serializedName) && serializedName != null) + if (_serializedNameMap.TryGetValue(generatedProperty.Name, out var serializedName) && serializedName != null) { generatedProperty.WireInfo!.SerializedName = serializedName; } } - Dictionary specToCustomPropertiesMap = BuildSpecToCustomPropertyMap(customProperties, specPropertiesMap); + Dictionary specToCustomPropertiesMap = BuildSpecToCustomPropertyMap(customProperties); foreach (var customProperty in customProperties) { InputModelProperty? specProperty = null; - if (((customProperty.OriginalName != null && specPropertiesMap.TryGetValue(customProperty.OriginalName, out var candidateSpecProperty)) - || specPropertiesMap.TryGetValue(customProperty.Name, out candidateSpecProperty)) + if (((customProperty.OriginalName != null && _specPropertiesMap.TryGetValue(customProperty.OriginalName, out var candidateSpecProperty)) + || _specPropertiesMap.TryGetValue(customProperty.Name, out candidateSpecProperty)) // Ensure that the spec property is mapped to this custom property - && specToCustomPropertiesMap[candidateSpecProperty] == customProperty) + && specToCustomPropertiesMap.TryGetValue(candidateSpecProperty, out var mappedProperty) && mappedProperty == customProperty) { specProperty = candidateSpecProperty; customProperty.WireInfo = new PropertyWireInformation(specProperty); @@ -70,8 +72,8 @@ protected override PropertyProvider[] BuildProperties() string? serializedName = specProperty?.SerializedName; bool hasCustomSerialization = false; // Update the serializedName of custom properties if necessary - if (serializedNameMapping.TryGetValue(customProperty.Name, out var customSerializedName) || - (customProperty.OriginalName != null && serializedNameMapping.TryGetValue(customProperty.OriginalName, out customSerializedName))) + if (_serializedNameMap.TryGetValue(customProperty.Name, out var customSerializedName) || + (customProperty.OriginalName != null && _serializedNameMap.TryGetValue(customProperty.OriginalName, out customSerializedName))) { hasCustomSerialization = true; if (customSerializedName != null) @@ -99,7 +101,8 @@ protected override PropertyProvider[] BuildProperties() } // handle customized extensible enums, since the custom type would not be an enum, but the spec type would be an enum - if (specProperty?.Type is InputEnumType { IsExtensible: true } inputEnumType) + + if (IsExtensibleEnum(specProperty!, out var inputEnumType)) { customProperty.Type = new CSharpType( customProperty.Type.Name, @@ -111,31 +114,143 @@ protected override PropertyProvider[] BuildProperties() customProperty.Type.IsPublic, customProperty.Type.IsStruct, customProperty.Type.BaseType, - TypeFactory.CreatePrimitiveCSharpTypeCore(inputEnumType.ValueType)); + TypeFactory.CreatePrimitiveCSharpTypeCore(inputEnumType!.ValueType)); } } return [..generatedProperties, ..customProperties]; } - private static Dictionary BuildSpecToCustomPropertyMap( - IReadOnlyList customProperties, - Dictionary specPropertiesMap) + protected override FieldProvider[] BuildFields() + { + var generatedFields = _generatedTypeProvider.Fields; + var customFields = _generatedTypeProvider.CustomCodeView?.Fields ?? []; + + // Update the serializedName of generated properties if necessary + foreach (var generatedField in generatedFields) + { + if (_serializedNameMap.TryGetValue(generatedField.Name, out var serializedName) && serializedName != null) + { + generatedField.WireInfo!.SerializedName = serializedName; + } + } + + foreach (var customField in customFields) + { + InputModelProperty? specProperty = null; + + if (((customField.OriginalName != null && _specPropertiesMap.TryGetValue(customField.OriginalName, out var candidateSpecProperty)) + || _specPropertiesMap.TryGetValue(customField.Name, out candidateSpecProperty)) + // Ensure that the spec property is mapped to this custom property + && _specToCustomFieldMap[candidateSpecProperty] == customField) + { + specProperty = candidateSpecProperty; + customField.WireInfo = new PropertyWireInformation(specProperty); + } + + string? serializedName = specProperty?.SerializedName; + bool hasCustomSerialization = false; + // Update the serializedName of custom properties if necessary + if (_serializedNameMap.TryGetValue(customField.Name, out var customSerializedName) || + (customField.OriginalName != null && _serializedNameMap.TryGetValue(customField.OriginalName, out customSerializedName))) + { + hasCustomSerialization = true; + if (customSerializedName != null) + { + serializedName = customSerializedName; + } + } + + if (serializedName != null || hasCustomSerialization) + { + if (specProperty == null) + { + customField.WireInfo = new( + SerializationFormat.Default, + false, + true, + customField.Type.IsNullable, + false, + serializedName ?? customField.Name.ToVariableName());; + } + else + { + customField.WireInfo!.SerializedName = serializedName!; + } + } + + // handle customized extensible enums, since the custom type would not be an enum, but the spec type would be an enum + if (IsExtensibleEnum(specProperty!, out var inputEnumType)) + { + customField.Type = new CSharpType( + customField.Type.Name, + customField.Type.Namespace, + customField.Type.IsValueType, + customField.Type.IsNullable, + customField.Type.DeclaringType, + customField.Type.Arguments, + customField.Type.IsPublic, + customField.Type.IsStruct, + customField.Type.BaseType, + TypeFactory.CreatePrimitiveCSharpTypeCore(inputEnumType!.ValueType)); + } + } + + return [..generatedFields, ..customFields]; + } + + private static bool IsExtensibleEnum(InputModelProperty? inputProperty, out InputEnumType? inputEnumType) + { + switch (inputProperty?.Type) + { + case InputEnumType { IsExtensible: true } enumType: + inputEnumType = enumType; + return true; + case InputLiteralType { ValueType: InputEnumType { IsExtensible: true } enumTypeFromLiteral }: + inputEnumType = enumTypeFromLiteral; + return true; + default: + inputEnumType = null; + return false; + } + } + + private Dictionary BuildSpecToCustomPropertyMap(IReadOnlyList customProperties) { var specToCustomPropertiesMap = new Dictionary(); // Create a map from spec properties to custom properties so that we know which custom properties are replacing spec properties foreach (var customProperty in customProperties) { - if ((customProperty.OriginalName != null && specPropertiesMap.TryGetValue(customProperty.OriginalName, out var specProperty)) - || specPropertiesMap.TryGetValue(customProperty.Name, out specProperty)) + if ((customProperty.OriginalName != null && _specPropertiesMap.TryGetValue(customProperty.OriginalName, out var specProperty)) + || _specPropertiesMap.TryGetValue(customProperty.Name, out specProperty)) { - // If the spec property is not already mapped to a custom property, map it to this custom property - specToCustomPropertiesMap.TryAdd(specProperty, customProperty); + // If the spec property is not already mapped to a custom property or field, map it to this custom property + if (!_specToCustomFieldMap.ContainsKey(specProperty)) + { + specToCustomPropertiesMap.TryAdd(specProperty, customProperty); + } } } return specToCustomPropertiesMap; } + private Dictionary BuildSpecToCustomFieldMap() + { + var customFields = _generatedTypeProvider.CustomCodeView?.Fields ?? []; + var specToCustomFieldsMap = new Dictionary(); + // Create a map from spec properties to custom properties so that we know which custom properties are replacing spec properties + foreach (var customField in customFields) + { + if ((customField.OriginalName != null && _specPropertiesMap.TryGetValue(customField.OriginalName, out var specProperty)) + || _specPropertiesMap.TryGetValue(customField.Name, out specProperty)) + { + // If the spec property is not already mapped to a custom property, map it to this custom property + specToCustomFieldsMap.TryAdd(specProperty, customField); + } + } + return specToCustomFieldsMap; + } + private Dictionary BuildSerializationNameMap() { var serializationAttributes = _generatedTypeProvider.CustomCodeView?.GetAttributes(). diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs index a1d6b466e2..851a3b169a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs @@ -2,9 +2,7 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Expressions; using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.Statements; @@ -17,10 +15,11 @@ public class FieldProvider private Lazy _parameter; public FormattableString? Description { get; } public FieldModifiers Modifiers { get; } - public CSharpType Type { get; } + public CSharpType Type { get; internal set; } public string Name { get; } public ValueExpression? InitializationValue { get; } public XmlDocProvider? XmlDocs { get; } + public PropertyWireInformation? WireInfo { get; internal set; } private CodeWriterDeclaration? _declaration; @@ -35,7 +34,7 @@ public class FieldProvider public TypeProvider EnclosingType { get; } - internal IEnumerable? Attributes { get; init; } + internal string? OriginalName { get; init; } // for mocking #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs index ca8653c45f..1a3fd8b783 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs @@ -71,25 +71,9 @@ protected override IReadOnlyList BuildEnumValues() string? customMemberName = null; foreach (var customMember in customMembers) { - var attributes = customMember.Attributes; - if (attributes == null) + if (customMember.OriginalName == name) { - continue; - } - - foreach (var attribute in attributes) - { - if (CodeGenAttributes.TryGetCodeGenMemberAttributeValue(attribute, out var originalName) - && originalName == name) - { - customMemberName = customMember.Name; - break; - } - } - - if (customMemberName != null) - { - break; + customMemberName = customMember.Name; } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelFactoryProvider.cs index 215f320dd7..1626bbfd23 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelFactoryProvider.cs @@ -52,7 +52,7 @@ protected override string BuildName() protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); protected override TypeSignatureModifiers GetDeclarationModifiers() - => TypeSignatureModifiers.Public | TypeSignatureModifiers.Static | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; + => TypeSignatureModifiers.Static | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs index 2e68330ec3..920884dc12 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs @@ -453,11 +453,13 @@ private ConstructorProvider BuildFullConstructor() var baseParameters = new List(); var constructorParameters = new List(); IEnumerable baseProperties = []; + IEnumerable baseFields = []; if (isPrimaryConstructor) { // the primary ctor should only include the properties of the direct base model baseProperties = BaseModelProvider?.CanonicalView.Properties ?? []; + baseFields = BaseModelProvider?.CanonicalView.Fields ?? []; } else if (BaseModelProvider?.FullConstructor.Signature != null) { @@ -469,7 +471,13 @@ private ConstructorProvider BuildFullConstructor() // add the base parameters, if any foreach (var property in baseProperties) { - AddInitializationParameterForCtor(baseParameters, property, Type.IsStruct, isPrimaryConstructor); + AddInitializationParameterForCtor(baseParameters, Type.IsStruct, isPrimaryConstructor, property); + } + + // add the base fields, if any + foreach (var field in baseFields) + { + AddInitializationParameterForCtor(baseParameters, Type.IsStruct, isPrimaryConstructor, field: field); } // construct the initializer using the parameters from base signature @@ -477,7 +485,12 @@ private ConstructorProvider BuildFullConstructor() foreach (var property in CanonicalView.Properties) { - AddInitializationParameterForCtor(constructorParameters, property, Type.IsStruct, isPrimaryConstructor); + AddInitializationParameterForCtor(constructorParameters, Type.IsStruct, isPrimaryConstructor, property); + } + + foreach (var field in CanonicalView.Fields) + { + AddInitializationParameterForCtor(constructorParameters, Type.IsStruct, isPrimaryConstructor, field: field); } constructorParameters.AddRange(_inputModel.IsUnknownDiscriminatorModel @@ -586,30 +599,35 @@ private ValueExpression GetExpressionForCtor(ParameterProvider parameter, HashSe private static void AddInitializationParameterForCtor( List parameters, - PropertyProvider property, bool isStruct, - bool isPrimaryConstructor) + bool isPrimaryConstructor, + PropertyProvider? property = default, + FieldProvider? field = default) { + var wireInfo = property?.WireInfo ?? field?.WireInfo; + var type = property?.Type ?? field?.Type; + // We only add those properties with wire info indicating they are coming from specs. - if (property.WireInfo is not { } wireInfo) + if (wireInfo == null) { return; } + var parameter = property?.AsParameter ?? field!.AsParameter; if (isPrimaryConstructor) { - if (isStruct || (wireInfo.IsRequired && !property.Type.IsLiteral)) + if (isStruct || (wireInfo.IsRequired && !type!.IsLiteral)) { if (!wireInfo.IsReadOnly) { - parameters.Add(property.AsParameter.ToPublicInputParameter()); + parameters.Add(parameter.ToPublicInputParameter()); } } } else { // For the serialization constructor, we always add the property as a parameter - parameters.Add(property.AsParameter); + parameters.Add(parameter); } } @@ -617,54 +635,17 @@ private MethodBodyStatement GetPropertyInitializers( bool isPrimaryConstructor, IReadOnlyList? parameters = null) { - List methodBodyStatements = new(CanonicalView.Properties.Count + 1); + List methodBodyStatements = new(CanonicalView.Properties.Count + CanonicalView.Fields.Count + 1); Dictionary parameterMap = parameters?.ToDictionary(p => p.Name) ?? []; foreach (var property in CanonicalView.Properties) { - // skip those non-spec properties - if (property.WireInfo == null) - continue; - - // skip if this is an overload / new of a base property - // also skip if the base was required or the derived property is not required - if (property.BaseProperty is not null && (!isPrimaryConstructor || property.WireInfo?.IsRequired == false || property.BaseProperty.WireInfo?.IsRequired == true)) - continue; - - ValueExpression assignee = property.BackingField is null ? property : property.BackingField; - - if (!isPrimaryConstructor) - { - // always add the property for the serialization constructor - methodBodyStatements.Add(assignee.Assign(GetConversion(property)).Terminate()); - continue; - } - - ValueExpression? initializationValue = null; - - if (parameterMap.TryGetValue(property.AsParameter.Name, out var parameter) || Type.IsStruct) - { - if (parameter != null) - { - initializationValue = parameter; - - if (CSharpType.RequiresToList(parameter.Type, property.Type)) - { - initializationValue = parameter.Type.IsNullable ? - initializationValue.NullConditional().ToList() : - initializationValue.ToList(); - } - } - } - else if (initializationValue == null && property.Type.IsCollection) - { - initializationValue = New.Instance(property.Type.PropertyInitializationType); - } + CreatePropertyAssignmentStatement(isPrimaryConstructor, methodBodyStatements, parameterMap, property); + } - if (initializationValue != null) - { - methodBodyStatements.Add(assignee.Assign(initializationValue).Terminate()); - } + foreach (var field in CanonicalView.Fields) + { + CreatePropertyAssignmentStatement(isPrimaryConstructor, methodBodyStatements, parameterMap, field: field); } // handle additional properties @@ -693,17 +674,76 @@ private MethodBodyStatement GetPropertyInitializers( return methodBodyStatements; } - private ValueExpression GetConversion(PropertyProvider property) + private void CreatePropertyAssignmentStatement( + bool isPrimaryConstructor, + List methodBodyStatements, + Dictionary parameterMap, + PropertyProvider? property = default, + FieldProvider? field = default) + { + var wireInfo = property?.WireInfo ?? field?.WireInfo; + // skip those non-spec properties + if (wireInfo == null) + return; + + // skip if this is an overload / new of a base property + // also skip if the base was required or the derived property is not required + if (property?.BaseProperty is not null && (!isPrimaryConstructor || wireInfo.IsRequired == false || property.BaseProperty.WireInfo?.IsRequired == true)) + return; + + ValueExpression assignee = property != null + ? property.BackingField is null ? property : property.BackingField + : field!; + + if (!isPrimaryConstructor) + { + // always add the property for the serialization constructor + methodBodyStatements.Add(assignee.Assign(GetConversion(property, field)).Terminate()); + return; + } + + ValueExpression? initializationValue = null; + + var type = property?.Type ?? field!.Type; + + if (parameterMap.TryGetValue(property?.AsParameter.Name ?? field!.AsParameter.Name, out var parameter) || Type.IsStruct) + { + if (parameter != null) + { + initializationValue = parameter; + + if (CSharpType.RequiresToList(parameter.Type, type)) + { + initializationValue = parameter.Type.IsNullable ? + initializationValue.NullConditional().ToList() : + initializationValue.ToList(); + } + } + } + else if (initializationValue == null && type.IsCollection) + { + initializationValue = New.Instance(type.PropertyInitializationType); + } + + if (initializationValue != null) + { + methodBodyStatements.Add(assignee.Assign(initializationValue).Terminate()); + } + } + + private ValueExpression GetConversion(PropertyProvider? property = default, FieldProvider? field = default) { - CSharpType to = property.BackingField is null ? property.Type : property.BackingField.Type; - CSharpType from = property.Type; + CSharpType to = property != null + ? property.BackingField is null ? property.Type : property.BackingField.Type + : field!.Type; + CSharpType from = property?.Type ?? field!.Type; if (from.IsEnum && to.Equals(from.UnderlyingEnumType)) { - return from.ToSerial(property.AsParameter); + return from.ToSerial(property?.AsParameter ?? field!.AsParameter); } - return property.AsParameter; + return property?.AsParameter ?? field!.AsParameter; } /// diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs index 0fb6f041aa..f692c88c3f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs @@ -95,7 +95,7 @@ protected override FieldProvider[] BuildFields() this, GetSymbolXmlDoc(fieldSymbol, "summary")) { - Attributes = fieldSymbol.GetAttributes() + OriginalName = GetOriginalName(fieldSymbol) }; fields.Add(fieldProvider); @@ -109,13 +109,6 @@ protected override PropertyProvider[] BuildProperties() List properties = new List(); foreach (var propertySymbol in _namedTypeSymbol.GetMembers().OfType()) { - var codeGenAttribute = propertySymbol.GetAttributes().SingleOrDefault( - a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenMemberAttributeName); - string? originalName = null; - if (codeGenAttribute != null) - { - CodeGenAttributes.TryGetCodeGenMemberAttributeValue(codeGenAttribute, out originalName); - } var propertyProvider = new PropertyProvider( GetSymbolXmlDoc(propertySymbol, "summary"), GetAccessModifier(propertySymbol.DeclaredAccessibility), @@ -124,13 +117,26 @@ protected override PropertyProvider[] BuildProperties() new AutoPropertyBody(propertySymbol.SetMethod is not null), this) { - OriginalName = originalName + OriginalName = GetOriginalName(propertySymbol) }; properties.Add(propertyProvider); } return [.. properties]; } + private static string? GetOriginalName(ISymbol symbol) + { + var codeGenAttribute = symbol.GetAttributes().SingleOrDefault( + a => a.AttributeClass?.Name == CodeGenAttributes.CodeGenMemberAttributeName); + string? originalName = null; + if (codeGenAttribute != null) + { + CodeGenAttributes.TryGetCodeGenMemberAttributeValue(codeGenAttribute, out originalName); + } + + return originalName; + } + protected override ConstructorProvider[] BuildConstructors() { List constructors = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs index 0f3aa2cc64..94ee2dfd23 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs @@ -39,7 +39,7 @@ protected TypeProvider() : this(null) public NamedTypeSymbolProvider? CustomCodeView => _customCodeView.Value; - internal IReadOnlyList GetAllCustomProperties() + private IReadOnlyList GetAllCustomProperties() { var allCustomProperties = CustomCodeView?.Properties != null ? new List(CustomCodeView.Properties) @@ -56,6 +56,23 @@ internal IReadOnlyList GetAllCustomProperties() return allCustomProperties; } + private IReadOnlyList GetAllCustomFields() + { + var allCustomFields = CustomCodeView?.Fields != null + ? new List(CustomCodeView.Fields) + : []; + var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView; + + // add all custom fields from base types + while (baseTypeCustomCodeView != null) + { + allCustomFields.AddRange(baseTypeCustomCodeView.Fields); + baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView; + } + + return allCustomFields; + } + private protected virtual CanonicalTypeProvider GetCanonicalView() => new CanonicalTypeProvider(this, _inputType); public TypeProvider CanonicalView => _canonicalView.Value; @@ -192,21 +209,29 @@ private TypeSignatureModifiers GetDeclarationModifiersInternal() private protected virtual PropertyProvider[] FilterCustomizedProperties(PropertyProvider[] specProperties) { var properties = new List(); - var customProperties = new Dictionary(); - var renamedProperties = new Dictionary(); + var customProperties = new HashSet(); foreach (var customProperty in GetAllCustomProperties()) { - customProperties.Add(customProperty.Name, customProperty); + customProperties.Add(customProperty.Name); if (customProperty.OriginalName != null) { - renamedProperties.Add(customProperty.OriginalName, customProperty); + customProperties.Add(customProperty.OriginalName); + } + } + + foreach (var customField in GetAllCustomFields()) + { + customProperties.Add(customField.Name); + if (customField.OriginalName != null) + { + customProperties.Add(customField.OriginalName); } } foreach (var property in specProperties) { - if (ShouldGenerate(property, customProperties, renamedProperties)) + if (ShouldGenerate(property, customProperties)) { properties.Add(property); } @@ -385,7 +410,7 @@ private bool ShouldGenerate(MethodProvider method) return true; } - private bool ShouldGenerate(PropertyProvider property, IDictionary customProperties, IDictionary renamedProperties) + private bool ShouldGenerate(PropertyProvider property, HashSet customProperties) { foreach (var attribute in GetMemberSuppressionAttributes()) { @@ -395,13 +420,7 @@ private bool ShouldGenerate(PropertyProvider property, IDictionary/**"], + "type": "node", + "smartStep": true, + "sourceMaps": true + }, + { + "name": "Python: Attach", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost" + } + ] +} diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index 5345a8d481..6c4d161754 100644 --- a/packages/http-client-python/CHANGELOG.md +++ b/packages/http-client-python/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log - @typespec/http-client-python +## 0.3.3 + +### Bug Fixes + +- Fix pylint issue for useless suppressions + ## 0.3.2 ### Bug Fixes diff --git a/packages/http-client-python/eng/scripts/ci/pylintrc b/packages/http-client-python/eng/scripts/ci/pylintrc index baee280ac8..0b7f1f403c 100644 --- a/packages/http-client-python/eng/scripts/ci/pylintrc +++ b/packages/http-client-python/eng/scripts/ci/pylintrc @@ -17,7 +17,7 @@ enable=useless-suppression # too-many-arguments: Due to the nature of the CLI many commands have large arguments set which reflect in large arguments set in corresponding methods. # too-many-lines: Due to code generation many files end up with too many lines. # Let's black deal with bad-continuation -disable=useless-object-inheritance,missing-docstring,locally-disabled,fixme,cyclic-import,too-many-arguments,invalid-name,duplicate-code,too-few-public-methods,consider-using-f-string,super-with-arguments,redefined-builtin,import-outside-toplevel,client-suffix-needed,unnecessary-dunder-call,unnecessary-ellipsis,disallowed-name,consider-using-max-builtin,too-many-lines,parse-error,useless-suppression,unknown-option-value +disable=useless-object-inheritance,missing-docstring,locally-disabled,fixme,cyclic-import,too-many-arguments,invalid-name,duplicate-code,too-few-public-methods,consider-using-f-string,super-with-arguments,redefined-builtin,import-outside-toplevel,client-suffix-needed,unnecessary-dunder-call,unnecessary-ellipsis,disallowed-name,consider-using-max-builtin [FORMAT] max-line-length=120 diff --git a/packages/http-client-python/eng/scripts/ci/run_pylint.py b/packages/http-client-python/eng/scripts/ci/run_pylint.py index f2b547d27c..2a9d362f0c 100644 --- a/packages/http-client-python/eng/scripts/ci/run_pylint.py +++ b/packages/http-client-python/eng/scripts/ci/run_pylint.py @@ -37,6 +37,8 @@ def _single_dir_pylint(mod): "--evaluation=(max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention + info)/ statement) * 10)))", "--load-plugins=pylint_guidelines_checker", "--output-format=parseable", + "--recursive=y", + "--py-version=3.8", str(inner_class.absolute()), ] ) @@ -47,4 +49,8 @@ def _single_dir_pylint(mod): if __name__ == "__main__": + if os.name == "nt": + # Before https://github.com/microsoft/typespec/issues/4759 fixed, skip running Pylint for now on Windows + logging.info("Skip running Pylint on Windows for now") + sys.exit(0) run_check("pylint", _single_dir_pylint, "Pylint") diff --git a/packages/http-client-python/generator/pygen/black.py b/packages/http-client-python/generator/pygen/black.py index b4915d824c..f8fd58b223 100644 --- a/packages/http-client-python/generator/pygen/black.py +++ b/packages/http-client-python/generator/pygen/black.py @@ -59,10 +59,9 @@ def format_file(self, file: Path) -> None: except: _LOGGER.error("Error: failed to format %s", file) raise - else: - if len(file_content.splitlines()) > 1000: - file_content = "# pylint: disable=too-many-lines\n" + file_content - self.write_file(file, file_content) + if len(file_content.splitlines()) > 1000: + file_content = "# pylint: disable=too-many-lines\n" + file_content + self.write_file(file, file_content) if __name__ == "__main__": diff --git a/packages/http-client-python/generator/pygen/codegen/models/client.py b/packages/http-client-python/generator/pygen/codegen/models/client.py index 0a82dd390d..b2fe0fda10 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/client.py +++ b/packages/http-client-python/generator/pygen/codegen/models/client.py @@ -354,7 +354,7 @@ class Config(_ClientConfigBase[ConfigGlobalParameterList]): """Model representing our Config type.""" def pylint_disable(self) -> str: - retval = add_to_pylint_disable("", "too-many-instance-attributes") + retval = add_to_pylint_disable("", "too-many-instance-attributes") if self.code_model.is_azure_flavor else "" if len(self.name) > NAME_LENGTH_LIMIT: retval = add_to_pylint_disable(retval, "name-too-long") return retval diff --git a/packages/http-client-python/generator/pygen/codegen/models/model_type.py b/packages/http-client-python/generator/pygen/codegen/models/model_type.py index 8dd3a94dfa..80e21252b4 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/model_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/model_type.py @@ -236,8 +236,6 @@ def discriminator_property(self) -> Optional[Property]: def pylint_disable(self) -> str: retval: str = "" - if len(self.properties) > 10: - retval = add_to_pylint_disable(retval, "too-many-instance-attributes") if len(self.name) > NAME_LENGTH_LIMIT: retval = add_to_pylint_disable(retval, "name-too-long") return retval diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index 68d662297d..1937a41678 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -142,11 +142,6 @@ def pylint_disable(self, async_mode: bool) -> str: if not async_mode and not self.is_overload and self.response_type_annotation(async_mode=False) == "None": # doesn't matter if it's async or not retval = add_to_pylint_disable(retval, "inconsistent-return-statements") - try: - if any(is_internal(r.type) for r in self.responses) or is_internal(self.parameters.body_parameter.type): - retval = add_to_pylint_disable(retval, "protected-access") - except ValueError: - pass if len(self.name) > NAME_LENGTH_LIMIT: retval = add_to_pylint_disable(retval, "name-too-long") return retval diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 7db633c9cf..de7d6c2086 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -553,11 +553,12 @@ def make_pipeline_call(self, builder: OperationType) -> List[str]: type_ignore = self.async_mode and builder.group_name == "" # is in a mixin if builder.stream_value is True and not self.code_model.options["version_tolerant"]: retval.append("_decompress = kwargs.pop('decompress', True)") + pylint_disable = " # pylint: disable=protected-access" if self.code_model.is_azure_flavor else "" retval.extend( [ f"_stream = {builder.stream_value}", f"pipeline_response: PipelineResponse = {self._call_method}self._client.{self.pipeline_name}.run( " - + f"{'# type: ignore' if type_ignore else ''} # pylint: disable=protected-access", + + f"{'# type: ignore' if type_ignore else ''}{pylint_disable}", " _request,", " stream=_stream,", " **kwargs", @@ -599,7 +600,7 @@ def _api_version_validation(self, builder: OperationType) -> str: retval.append(f" params_added_on={dict(params_added_on)},") if retval: retval_str = "\n".join(retval) - return f"@api_version_validation(\n{retval_str}\n){builder.pylint_disable(self.async_mode)}" + return f"@api_version_validation(\n{retval_str}\n)" return "" def pop_kwargs_from_signature(self, builder: OperationType) -> List[str]: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py index e4fbde6c66..6ad0fab40a 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py @@ -110,16 +110,22 @@ def initialize_properties(self, model: ModelType) -> List[str]: ... def need_init(self, model: ModelType) -> bool: return (not model.internal) and bool(self.init_line(model) or model.discriminator) - def pylint_disable(self, model: ModelType) -> str: + def pylint_disable_items(self, model: ModelType) -> List[str]: if model.flattened_property or self.initialize_properties(model): - return "" + return [""] if any(p for p in model.properties if p.is_discriminator and model.discriminator_value): - return "" + return [""] if model.parents and any( "=" in prop for parent in model.parents for prop in self.init_line(parent) if self.need_init(parent) ): - return "" - return " # pylint: disable=useless-super-delegation" + return [""] + return ["useless-super-delegation"] + + def pylint_disable(self, model: ModelType) -> str: + return " # pylint: disable=" + ", ".join(self.pylint_disable_items(model)) + + def global_pylint_disables(self) -> str: + return "" class MsrestModelSerializer(_ModelSerializer): @@ -315,3 +321,15 @@ def properties_to_pass_to_super(model: ModelType) -> str: properties_to_pass_to_super.append(f"{prop.client_name}={prop.get_declaration()}") properties_to_pass_to_super.append("**kwargs") return ", ".join(properties_to_pass_to_super) + + def global_pylint_disables(self) -> str: + result = [] + for model in self.code_model.model_types: + if self.need_init(model): + for item in self.pylint_disable_items(model): + if item: + result.append(item) + final_result = set(result) + if final_result: + return "# pylint: disable=" + ", ".join(final_result) + return "" diff --git a/packages/http-client-python/generator/pygen/codegen/templates/keywords.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/keywords.jinja2 index abf427748e..30debbee37 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/keywords.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/keywords.jinja2 @@ -11,7 +11,7 @@ try: {% endif %} {{ indentation }}from ._patch import __all__ as _patch_all -{{ indentation }}from ._patch import * # pylint: disable=unused-wildcard-import +{{ indentation }}from ._patch import * {% if try_except %} except ImportError: _patch_all = [] diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index f72e40fc88..9ca2ec4531 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -4,7 +4,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -# pylint: disable=protected-access, arguments-differ, signature-differs, broad-except, too-many-lines +# pylint: disable=protected-access, broad-except import copy import calendar @@ -573,7 +573,7 @@ class Model(_MyMutableMapping): def copy(self) -> "Model": return Model(self.__dict__) - def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: # pylint: disable=unused-argument + def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated: # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping', # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object' @@ -584,8 +584,8 @@ class Model(_MyMutableMapping): annotations = { k: v for mro_class in mros - if hasattr(mro_class, "__annotations__") # pylint: disable=no-member - for k, v in mro_class.__annotations__.items() # pylint: disable=no-member + if hasattr(mro_class, "__annotations__") + for k, v in mro_class.__annotations__.items() } for attr, rf in attr_to_rest_field.items(): rf._module = cls.__module__ @@ -600,8 +600,8 @@ class Model(_MyMutableMapping): def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None: for base in cls.__bases__: - if hasattr(base, "__mapping__"): # pylint: disable=no-member - base.__mapping__[discriminator or cls.__name__] = cls # type: ignore # pylint: disable=no-member + if hasattr(base, "__mapping__"): + base.__mapping__[discriminator or cls.__name__] = cls # type: ignore @classmethod def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: @@ -612,7 +612,7 @@ class Model(_MyMutableMapping): @classmethod def _deserialize(cls, data, exist_discriminators): - if not hasattr(cls, "__mapping__"): # pylint: disable=no-member + if not hasattr(cls, "__mapping__"): return cls(data) discriminator = cls._get_discriminator(exist_discriminators) if discriminator is None: @@ -632,7 +632,7 @@ class Model(_MyMutableMapping): discriminator_value = data.find(xml_name).text # pyright: ignore else: discriminator_value = data.get(discriminator._rest_name) - mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore # pylint: disable=no-member + mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore return mapped_cls._deserialize(data, exist_discriminators) def as_dict(self, *, exclude_readonly: bool = False) -> typing.Dict[str, typing.Any]: diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 index 2e273974a7..b2ec433efc 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 @@ -1,6 +1,9 @@ {% import 'operation_tools.jinja2' as op_tools %} # coding=utf-8 {{ code_model.options['license_header'] }} +{% if serializer.global_pylint_disables() %} +{{ serializer.global_pylint_disables() }} +{% endif %} {{ imports }} {% for model in code_model.model_types %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 index 604cbf1448..b35f4260b1 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 @@ -70,7 +70,7 @@ {% endif %} {% set initialize_properties = serializer.initialize_properties(model) %} {% if serializer.need_init(model) or initialize_properties %} - def __init__(self, *args: Any, **kwargs: Any) -> None:{{ '# pylint: disable=useless-super-delegation' if not initialize_properties else '' }} + def __init__(self, *args: Any, **kwargs: Any) -> None: {% for line in serializer.super_call(model) %} {{ line }} {% endfor %} diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 51dd110ce6..27ad345626 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -1,12 +1,12 @@ { "name": "@typespec/http-client-python", - "version": "0.3.1", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@typespec/http-client-python", - "version": "0.3.1", + "version": "0.3.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index e679a11886..81269b8490 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http-client-python", - "version": "0.3.1", + "version": "0.3.3", "author": "Microsoft Corporation", "description": "TypeSpec emitter for Python SDKs", "homepage": "https://typespec.io", diff --git a/packages/http-specs/specs/authentication/commonapi.ts b/packages/http-specs/specs/authentication/commonapi.ts deleted file mode 100644 index 84e093edf5..0000000000 --- a/packages/http-specs/specs/authentication/commonapi.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - json, - mockapi, - MockRequest, - passOnCode, - PassOnCodeScenario, - passOnSuccess, - PassOnSuccessScenario, - ScenarioMockApi, -} from "@typespec/spec-api"; - -export const Scenarios: Record = {}; - -interface ValidAndInvalidCodeScenarios { - valid: PassOnSuccessScenario; - invalid: PassOnCodeScenario; -} - -export function getValidAndInvalidScenarios( - scenarioFolder: string, - errorCode: string, - authenticationValidation: (req: MockRequest) => void, -): ValidAndInvalidCodeScenarios { - return { - valid: passOnSuccess( - mockapi.get(`/authentication/${scenarioFolder}/valid`, (req) => { - authenticationValidation(req); - return { status: 204 }; - }), - ), - invalid: passOnCode( - 403, - mockapi.get(`/authentication/${scenarioFolder}/invalid`, (req) => { - return { - status: 403, - body: json({ - error: errorCode, - }), - }; - }), - ), - }; -} diff --git a/packages/http-specs/specs/type/enum/fixed/mockapi.ts b/packages/http-specs/specs/type/enum/fixed/mockapi.ts index 41f0343175..4839129ad8 100644 --- a/packages/http-specs/specs/type/enum/fixed/mockapi.ts +++ b/packages/http-specs/specs/type/enum/fixed/mockapi.ts @@ -1,4 +1,4 @@ -import { json, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; +import { json, passOnCode, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; export const Scenarios: Record = {}; @@ -27,7 +27,7 @@ Scenarios.Type_Enum_Fixed_String_putKnownValue = passOnSuccess({ kind: "MockApiDefinition", }); -Scenarios.Type_Enum_Fixed_String_putUnknownValue = passOnSuccess({ +Scenarios.Type_Enum_Fixed_String_putUnknownValue = passOnCode(500, { uri: "/type/enum/fixed/string/unknown-value", method: "put", request: { diff --git a/packages/spector/src/app/app.ts b/packages/spector/src/app/app.ts index 1ae839d6ae..dc5072a867 100644 --- a/packages/spector/src/app/app.ts +++ b/packages/spector/src/app/app.ts @@ -94,7 +94,11 @@ function createHandler(apiDefinition: MockApiDefinition) { if (apiDefinition.request.headers) { Object.entries(apiDefinition.request.headers).forEach(([key, value]) => { if (key !== "Content-Type") { - req.expect.containsHeader(key, value as string); + if (Array.isArray(value)) { + req.expect.deepEqual(req.headers[key], value); + } else { + req.expect.containsHeader(key.toLowerCase(), String(value)); + } } }); } @@ -102,7 +106,11 @@ function createHandler(apiDefinition: MockApiDefinition) { // Validate query params if present in the request if (apiDefinition.request.params) { Object.entries(apiDefinition.request.params).forEach(([key, value]) => { - req.expect.containsQueryParam(key, value as string); + if (Array.isArray(value)) { + req.expect.deepEqual(req.query[key], value); + } else { + req.expect.containsQueryParam(key, String(value)); + } }); }