diff --git a/packages/http-client-csharp/emitter/src/lib/type-converter.ts b/packages/http-client-csharp/emitter/src/lib/type-converter.ts index f6b31db5578..56760039ab8 100644 --- a/packages/http-client-csharp/emitter/src/lib/type-converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/type-converter.ts @@ -269,6 +269,7 @@ function fromSdkModelProperty( serializationOptions: sdkProperty.serializationOptions, // A property is defined to be metadata if it is marked `@header`, `@cookie`, `@query`, `@path`. isHttpMetadata: isHttpMetadata(sdkContext, sdkProperty), + encode: sdkProperty.encode, } as InputModelProperty; if (property) { diff --git a/packages/http-client-csharp/emitter/src/type/input-type.ts b/packages/http-client-csharp/emitter/src/type/input-type.ts index b4008677dd8..922aa4a8063 100644 --- a/packages/http-client-csharp/emitter/src/type/input-type.ts +++ b/packages/http-client-csharp/emitter/src/type/input-type.ts @@ -181,6 +181,7 @@ export interface InputModelProperty extends InputPropertyTypeBase { serializationOptions: SerializationOptions; flatten: boolean; isHttpMetadata: boolean; + encode?: string; } export type InputProperty = InputModelProperty | InputParameter; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 4a36125f9e1..fc3e049782f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -969,7 +969,7 @@ private List BuildDeserializePropertiesStatements(ScopedApi var propertyExpression = parameter.Property?.AsVariableExpression ?? parameter.Field?.AsVariableExpression; var checkIfJsonPropEqualsName = new IfStatement(jsonProperty.NameEquals(propertySerializationName)) { - DeserializeProperty(propertyName!, propertyType!, wireInfo, propertyExpression!, jsonProperty, serializationAttributes) + DeserializeProperty(propertyName!, propertyType!, wireInfo, propertyExpression!, jsonProperty, serializationAttributes, parameter.Property?.SerializationFormat) }; propertyDeserializationStatements.Add(checkIfJsonPropEqualsName); } @@ -1285,17 +1285,12 @@ private MethodBodyStatement[] DeserializeProperty( PropertyWireInformation wireInfo, VariableExpression variableExpression, ScopedApi jsonProperty, - IEnumerable serializationAttributes) + IEnumerable serializationAttributes, + SerializationFormat? arrayEncoding = null) { - bool useCustomDeserializationHook = false; var serializationFormat = wireInfo.SerializationFormat; - var propertyVarReference = variableExpression; - var deserializationStatements = new MethodBodyStatement[2] - { - DeserializeValue(propertyType, jsonProperty.Value(), serializationFormat, out ValueExpression value), - propertyVarReference.Assign(value).Terminate() - }; + // Check for custom deserialization foreach (var attribute in serializationAttributes) { if (CodeGenAttributes.TryGetCodeGenSerializationAttributeValue( @@ -1306,21 +1301,60 @@ private MethodBodyStatement[] DeserializeProperty( out var deserializationHook, out _) && name == propertyName && deserializationHook != null) { + return + [ + MethodBodyStatement.Empty, + Static().Invoke(deserializationHook, jsonProperty, ByRef(variableExpression)).Terminate(), + Continue + ]; + } + } + + MethodBodyStatement[] deserializationStatements; + if (arrayEncoding.HasValue && (propertyType.IsList || propertyType.IsArray)) + { + try + { + var delimiter = ArrayKnownEncodingExtensions.GetDelimiter(arrayEncoding.Value); + var elementType = propertyType.ElementType; + if (IsSupportedEncodedArrayElementType(elementType)) + { + deserializationStatements = CreateEncodedArrayDeserializationStatements( + propertyType, variableExpression, jsonProperty, arrayEncoding.Value); + } + else + { + // Fall back to default deserialization for unsupported element types + deserializationStatements = + [ + DeserializeValue(propertyType, jsonProperty.Value(), serializationFormat, out ValueExpression value), + variableExpression.Assign(value).Terminate() + ]; + } + } + catch (ArgumentOutOfRangeException) + { + // Fall back to default deserialization for non-array encoding formats deserializationStatements = - [Static().Invoke( - deserializationHook, - jsonProperty, - ByRef(propertyVarReference)).Terminate()]; - useCustomDeserializationHook = true; - break; + [ + DeserializeValue(propertyType, jsonProperty.Value(), serializationFormat, out ValueExpression value), + variableExpression.Assign(value).Terminate() + ]; } } + else + { + // Default deserialization for non-encoded arrays and other types + deserializationStatements = + [ + DeserializeValue(propertyType, jsonProperty.Value(), serializationFormat, out ValueExpression value), + variableExpression.Assign(value).Terminate() + ]; + } return [ - useCustomDeserializationHook - ? MethodBodyStatement.Empty - : DeserializationPropertyNullCheckStatement(propertyType, wireInfo, jsonProperty, propertyVarReference), + DeserializationPropertyNullCheckStatement(propertyType, wireInfo, jsonProperty, variableExpression), deserializationStatements, Continue ]; @@ -1578,7 +1612,7 @@ private MethodBodyStatement[] CreateWritePropertiesStatements(bool isDynamicMode { continue; } - propertyStatements.Add(CreateWritePropertyStatement(property.WireInfo, property.Type, property.Name, property)); + propertyStatements.Add(CreateWritePropertyStatement(property.WireInfo, property.Type, property.Name, property, property.WireInfo?.SerializationFormat)); } foreach (var field in baseModelProvider.CanonicalView.Fields) @@ -1587,7 +1621,7 @@ private MethodBodyStatement[] CreateWritePropertiesStatements(bool isDynamicMode { continue; } - propertyStatements.Add(CreateWritePropertyStatement(field.WireInfo, field.Type, field.Name, field)); + propertyStatements.Add(CreateWritePropertyStatement(field.WireInfo, field.Type, field.Name, field, field.WireInfo?.SerializationFormat)); } baseModelProvider = baseModelProvider.BaseModelProvider; @@ -1603,7 +1637,7 @@ private MethodBodyStatement[] CreateWritePropertiesStatements(bool isDynamicMode continue; } - propertyStatements.Add(CreateWritePropertyStatement(property.WireInfo, property.Type, property.Name, property)); + propertyStatements.Add(CreateWritePropertyStatement(property.WireInfo, property.Type, property.Name, property, property.SerializationFormat)); } foreach (var field in _model.CanonicalView.Fields) @@ -1613,7 +1647,7 @@ private MethodBodyStatement[] CreateWritePropertiesStatements(bool isDynamicMode continue; } - propertyStatements.Add(CreateWritePropertyStatement(field.WireInfo, field.Type, field.Name, field)); + propertyStatements.Add(CreateWritePropertyStatement(field.WireInfo, field.Type, field.Name, field, field.WireInfo?.SerializationFormat)); } return [.. propertyStatements]; @@ -1623,7 +1657,8 @@ private MethodBodyStatement CreateWritePropertyStatement( PropertyWireInformation wireInfo, CSharpType propertyType, string propertyName, - MemberExpression propertyExpression) + MemberExpression propertyExpression, + SerializationFormat? serializationFormat) { var propertySerializationName = wireInfo.SerializedName; var propertySerializationFormat = wireInfo.SerializationFormat; @@ -1634,6 +1669,27 @@ private MethodBodyStatement CreateWritePropertyStatement( // Generate the serialization statements for the property var serializationStatement = CreateSerializationStatement(propertyType, propertyExpression, propertySerializationFormat, propertySerializationName); + // Check for encoded arrays and override the serialization statement + if (serializationFormat.HasValue && (propertyType.IsList || propertyType.IsArray)) + { + try + { + var delimiter = ArrayKnownEncodingExtensions.GetDelimiter(serializationFormat.Value); + var elementType = propertyType.ElementType; + if (IsSupportedEncodedArrayElementType(elementType)) + { + serializationStatement = CreateEncodedArraySerializationStatement( + propertyType, + propertyExpression, + serializationFormat.Value); + } + } + catch (ArgumentOutOfRangeException) + { + // Not an array encoding format, continue with regular serialization + } + } + // Check for custom serialization hooks foreach (var attribute in _model.CustomCodeView?.Attributes .Where(a => a.Type.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? []) @@ -1810,6 +1866,138 @@ private MethodBodyStatement CreateListSerialization( }; } + private static bool IsSupportedEncodedArrayElementType(CSharpType elementType) + { + // Support string arrays + if (elementType.IsFrameworkType && elementType.FrameworkType == typeof(string)) + { + return true; + } + + // Support string enum arrays + if (elementType.IsEnum && elementType.UnderlyingEnumType?.Equals(typeof(string)) == true) + { + return true; + } + return false; + } + + private MethodBodyStatement CreateEncodedArraySerializationStatement( + CSharpType propertyType, + ValueExpression propertyExpression, + SerializationFormat serializationFormat) + { + var delimiter = ArrayKnownEncodingExtensions.GetDelimiter(serializationFormat); + + var elementType = propertyType.ElementType; + + ValueExpression stringJoinExpression; + if (elementType.IsFrameworkType && elementType.FrameworkType == typeof(string)) + { + stringJoinExpression = StringSnippets.Join(Literal(delimiter), propertyExpression); + } + else if (elementType.IsEnum && !elementType.IsStruct && elementType.UnderlyingEnumType?.Equals(typeof(string)) == true) + { + var x = new VariableExpression(typeof(object), "x"); + var selectExpression = propertyExpression.Invoke(nameof(Enumerable.Select), + new FuncExpression([x.Declaration], new TernaryConditionalExpression( + x.Equal(Null), + Literal(""), + elementType.ToSerial(x)))); + stringJoinExpression = StringSnippets.Join(Literal(delimiter), selectExpression); + } + else + { + ScmCodeModelGenerator.Instance.Emitter.ReportDiagnostic( + DiagnosticCodes.UnsupportedSerialization, + $"Encoded array serialization is only supported for string and string enum arrays. Element type: {elementType.Name}.", + severity: EmitterDiagnosticSeverity.Warning); + stringJoinExpression = propertyExpression.InvokeToString(); + } + + return _utf8JsonWriterSnippet.WriteStringValue(stringJoinExpression); + } + + private MethodBodyStatement[] CreateEncodedArrayDeserializationStatements( + CSharpType propertyType, + VariableExpression variableExpression, + ScopedApi jsonProperty, + SerializationFormat serializationFormat) + { + var delimiter = ArrayKnownEncodingExtensions.GetDelimiter(serializationFormat); + var elementType = propertyType.ElementType; + var delimiterChar = Literal(delimiter.ToCharArray()[0]); + var isStringElement = elementType.IsFrameworkType && elementType.FrameworkType == typeof(string); + + var getStringStatement = Declare("stringValue", typeof(string), jsonProperty.Value().GetString(), out var stringValueVar); + var isNullOrEmptyCheck = StringSnippets.IsNullOrEmpty(stringValueVar.As()); + + MethodBodyStatement createArrayStatement; + + if (isStringElement) + { + var splitResult = stringValueVar.As().Split(delimiterChar); + if (propertyType.IsArray) + { + var emptyExpression = New.Array(elementType); + var conditionalExpression = new TernaryConditionalExpression( + isNullOrEmptyCheck, emptyExpression, splitResult); + createArrayStatement = variableExpression.Assign(conditionalExpression).Terminate(); + } + else if (propertyType.IsList) + { + var listType = New.List(elementType); + var populatedExpression = New.Instance(typeof(List<>).MakeGenericType(elementType.FrameworkType), splitResult); + var conditionalExpression = new TernaryConditionalExpression( + isNullOrEmptyCheck, listType, populatedExpression); + createArrayStatement = variableExpression.Assign(conditionalExpression).Terminate(); + } + else + { + var initType = propertyType.PropertyInitializationType; + var listExpression = New.Instance(initType, splitResult); + var conditionalExpression = new TernaryConditionalExpression( + isNullOrEmptyCheck, New.Instance(initType), listExpression); + createArrayStatement = variableExpression.Assign(conditionalExpression).Terminate(); + } + } + else if (elementType.IsEnum && !elementType.IsStruct && elementType.UnderlyingEnumType?.Equals(typeof(string)) == true) + { + var splitExpression = new TernaryConditionalExpression( + isNullOrEmptyCheck, + New.Array(typeof(string)), + stringValueVar.As().Split(delimiterChar)); + + var s = new VariableExpression(typeof(string), "s"); + var trimmedS = s.Invoke(nameof(string.Trim)); + + var parseExpression = Static(elementType).Invoke("Parse", trimmedS); + + var selectExpression = splitExpression.Invoke(nameof(Enumerable.Select), + new FuncExpression([s.Declaration], parseExpression)); + + var finalExpression = propertyType.IsArray + ? selectExpression.Invoke(nameof(Enumerable.ToArray)) + : propertyType.IsList + ? New.Instance(typeof(List<>).MakeGenericType(elementType.FrameworkType), selectExpression) + : New.Instance(propertyType.PropertyInitializationType, selectExpression); + createArrayStatement = variableExpression.Assign(finalExpression).Terminate(); + } + else + { + ScmCodeModelGenerator.Instance.Emitter.ReportDiagnostic( + DiagnosticCodes.UnsupportedSerialization, + $"Encoded array deserialization is only supported for string and string enum arrays. Element type: {elementType.Name}.", + severity: EmitterDiagnosticSeverity.Warning); + createArrayStatement = variableExpression.Assign(propertyType.IsList ? New.Instance(propertyType) : New.Array(elementType)).Terminate(); + } + return + [ + getStringStatement, + createArrayStatement + ]; + } + private MethodBodyStatement CreateDictionarySerialization( DictionaryExpression dictionary, SerializationFormat serializationFormat, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs index fcff349c3f3..141a731d1d9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs @@ -526,5 +526,62 @@ public async Task ExplicitOperatorDoesNotPreventImplicit() m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Operator)); Assert.IsNotNull(implicitOperator, "Implicit operator should still be generated when only explicit operator is customized"); } + + // Unit test for enum array serialization - shows generated output like spector tests show behavior + [Test] + public async Task ValidateEnumArraySerializationGeneratedOutput() + { + var inputEnum = InputFactory.StringEnum("TestEnum", [("Blue", "blue"), ("Red", "red"), ("Green", "green")], isExtensible: false); + var props = new[] + { + InputFactory.Property("Colors", InputFactory.Array(inputEnum)) + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [inputModel], + inputEnums: () => [inputEnum]); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + + var writer = new TypeProviderWriter(serializationProvider); + var file = writer.Write(); + + var generatedCode = file.Content; + + Assert.IsTrue(generatedCode.Contains("writer.WriteStringValue(item.ToSerialString())")); + Assert.IsTrue(generatedCode.Contains("array.Add(item.GetString().ToTestEnum())")); + Assert.IsTrue(generatedCode.Contains("foreach")); + Assert.IsTrue(generatedCode.Contains("ToSerialString()")); + } + + [Test] + public async Task ValidateEnumArrayDeserializationGeneratedOutput() + { + var inputEnum = InputFactory.StringEnum("TestEnum", [("Blue", "blue"), ("Red", "red"), ("Green", "green")], isExtensible: false); + var props = new[] + { + InputFactory.Property("Colors", InputFactory.Array(inputEnum)) + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props, usage: InputModelTypeUsage.Json); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [inputModel], + inputEnums: () => [inputEnum]); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + + var writer = new TypeProviderWriter(serializationProvider); + var file = writer.Write(); + + var generatedCode = file.Content; + + Assert.IsTrue(generatedCode.Contains("array.Add(item.GetString().ToTestEnum())")); + Assert.IsTrue(generatedCode.Contains("foreach")); + Assert.IsTrue(generatedCode.Contains("JsonElement")); + Assert.IsTrue(generatedCode.Contains("ToTestEnum()") || generatedCode.Contains(".ToEnum()")); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Extensions/ArrayKnownEncodingExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Extensions/ArrayKnownEncodingExtensions.cs new file mode 100644 index 00000000000..5e7208fa199 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Extensions/ArrayKnownEncodingExtensions.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.TypeSpec.Generator.Input.Extensions +{ + public static class ArrayKnownEncodingExtensions + { + /// + /// Converts a string representation to ArrayKnownEncoding. + /// + public static bool TryParse(string? value, out ArrayKnownEncoding encoding) + { + encoding = default; + if (value == null) return false; + + return value switch + { + "ArrayEncoding.commaDelimited" or "commaDelimited" => + SetEncodingAndReturnTrue(out encoding, ArrayKnownEncoding.CommaDelimited), + "ArrayEncoding.spaceDelimited" or "spaceDelimited" => + SetEncodingAndReturnTrue(out encoding, ArrayKnownEncoding.SpaceDelimited), + "ArrayEncoding.pipeDelimited" or "pipeDelimited" => + SetEncodingAndReturnTrue(out encoding, ArrayKnownEncoding.PipeDelimited), + "ArrayEncoding.newlineDelimited" or "newlineDelimited" => + SetEncodingAndReturnTrue(out encoding, ArrayKnownEncoding.NewlineDelimited), + _ => false + }; + } + + /// + /// Converts ArrayKnownEncoding to SerializationFormat. + /// + public static SerializationFormat ToSerializationFormat(this ArrayKnownEncoding encoding) + { + return encoding switch + { + ArrayKnownEncoding.CommaDelimited => SerializationFormat.Array_CommaDelimited, + ArrayKnownEncoding.SpaceDelimited => SerializationFormat.Array_SpaceDelimited, + ArrayKnownEncoding.PipeDelimited => SerializationFormat.Array_PipeDelimited, + ArrayKnownEncoding.NewlineDelimited => SerializationFormat.Array_NewlineDelimited, + _ => throw new ArgumentOutOfRangeException(nameof(encoding), encoding, "Unknown array encoding") + }; + } + + /// + /// Gets the delimiter string for array serialization format. + /// + public static string GetDelimiter(SerializationFormat format) + { + return format switch + { + SerializationFormat.Array_CommaDelimited => ",", + SerializationFormat.Array_SpaceDelimited => " ", + SerializationFormat.Array_PipeDelimited => "|", + SerializationFormat.Array_NewlineDelimited => "\n", + _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported array serialization format") + }; + } + + private static bool SetEncodingAndReturnTrue(out ArrayKnownEncoding encoding, ArrayKnownEncoding value) + { + encoding = value; + return true; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/ArrayKnownEncoding.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/ArrayKnownEncoding.cs new file mode 100644 index 00000000000..3b4100642ec --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/ArrayKnownEncoding.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.TypeSpec.Generator.Input +{ + /// + /// Represents the known encoding formats for arrays. + /// + public enum ArrayKnownEncoding + { + /// + /// Comma-delimited array encoding + /// + CommaDelimited, + + /// + /// Space-delimited array encoding + /// + SpaceDelimited, + + /// + /// Pipe-delimited array encoding + /// + PipeDelimited, + + /// + /// Newline-delimited array encoding + /// + NewlineDelimited + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputModelProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputModelProperty.cs index e0e0f3126f1..52b0d256407 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputModelProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputModelProperty.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.TypeSpec.Generator.Input.Extensions; + namespace Microsoft.TypeSpec.Generator.Input { public class InputModelProperty : InputProperty @@ -18,7 +20,8 @@ public InputModelProperty( bool isHttpMetadata, bool isApiVersion, InputConstant? defaultValue, - InputSerializationOptions serializationOptions) + InputSerializationOptions serializationOptions, + string? encode = null) : base(name, summary, doc, type, isRequired, isReadOnly, access, serializedName, isApiVersion, defaultValue) { Name = name; @@ -30,11 +33,13 @@ public InputModelProperty( IsDiscriminator = isDiscriminator; IsHttpMetadata = isHttpMetadata; SerializationOptions = serializationOptions; + Encode = encode; } public bool IsDiscriminator { get; internal set; } public InputSerializationOptions? SerializationOptions { get; internal set; } public bool IsHttpMetadata { get; internal set; } + public string? Encode { get; internal set; } /// /// Updates the properties of the input model property. @@ -117,5 +122,14 @@ public void Update( SerializationOptions = serializationOptions; } } + + public ArrayKnownEncoding? GetArrayEncoding() + { + if (ArrayKnownEncodingExtensions.TryParse(Encode, out var encoding)) + { + return encoding; + } + return null; + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputModelPropertyConverter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputModelPropertyConverter.cs index 85ab88453c7..a9c62ac76cb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputModelPropertyConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/InputModelPropertyConverter.cs @@ -46,7 +46,8 @@ internal static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader rea isHttpMetadata: false, isApiVersion: false, defaultValue: null, - serializationOptions: null!); + serializationOptions: null!, + encode: null); resolver.AddReference(id, property); string? kind = null; @@ -63,6 +64,7 @@ internal static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader rea bool isDiscriminator = false; IReadOnlyList? decorators = null; InputSerializationOptions? serializationOptions = null; + string? encode = null; while (reader.TokenType != JsonTokenType.EndObject) { @@ -81,7 +83,8 @@ internal static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader rea || reader.TryReadString("serializedName", ref serializedName) || reader.TryReadBoolean("isApiVersion", ref isApiVersion) || reader.TryReadComplexType("defaultValue", options, ref defaultValue) - || reader.TryReadComplexType("serializationOptions", options, ref serializationOptions); + || reader.TryReadComplexType("serializationOptions", options, ref serializationOptions) + || reader.TryReadString("encode", ref encode); if (!isKnownProperty) { @@ -103,6 +106,7 @@ internal static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader rea property.SerializedName = serializedName ?? serializationOptions?.Json?.Name ?? name; property.IsApiVersion = isApiVersion; property.DefaultValue = defaultValue; + property.Encode = encode; return property; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/SerializationFormat.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/SerializationFormat.cs index 3497fe41243..18199def355 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/SerializationFormat.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/Serialization/SerializationFormat.cs @@ -28,5 +28,9 @@ public enum SerializationFormat Bytes_Base64Url, Bytes_Base64, Int_String, + Array_CommaDelimited, + Array_SpaceDelimited, + Array_PipeDelimited, + Array_NewlineDelimited, } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index 3f4b7d13a4b..e6fc8fa64fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -39,6 +39,7 @@ public class PropertyProvider public FieldProvider? BackingField { get; set; } public PropertyProvider? BaseProperty { get; set; } public bool IsRef { get; private set; } + public SerializationFormat SerializationFormat => _serializationFormat; /// /// Converts this property to a parameter. @@ -91,7 +92,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T } EnclosingType = enclosingType; - _serializationFormat = CodeModelGenerator.Instance.TypeFactory.GetSerializationFormat(inputProperty.Type); + _serializationFormat = GetSerializationFormat(inputProperty); _isRequiredNonNullableConstant = inputProperty.IsRequired && propertyType is { IsLiteral: true, IsNullable: false }; var propHasSetter = PropertyHasSetter(propertyType, inputProperty); MethodSignatureModifiers setterModifier = propHasSetter ? MethodSignatureModifiers.Public : MethodSignatureModifiers.None; @@ -334,5 +335,21 @@ public void Update( BuildDocs(); } } + + private SerializationFormat GetSerializationFormat(InputProperty inputProperty) + { + // Handle array encoding from InputModelProperty + if (inputProperty is InputModelProperty modelProperty && + inputProperty.Type is InputArrayType) + { + var arrayEncoding = modelProperty.GetArrayEncoding(); + if (arrayEncoding.HasValue) + { + return arrayEncoding.Value.ToSerializationFormat(); + } + } + + return CodeModelGenerator.Instance.TypeFactory.GetSerializationFormat(inputProperty.Type); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/StringSnippets.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/StringSnippets.cs index 50ac2f71ccf..af77b445581 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/StringSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Snippets/StringSnippets.cs @@ -21,9 +21,15 @@ public static ScopedApi Format(ScopedApi format, params ValueExp public static ScopedApi IsNullOrWhiteSpace(ScopedApi value, params ValueExpression[] args) => Static().Invoke(nameof(string.IsNullOrWhiteSpace), args.Prepend(value).ToArray()).As(); + public static ScopedApi IsNullOrEmpty(ScopedApi value) + => Static().Invoke(nameof(string.IsNullOrEmpty), [value]).As(); + public static ScopedApi Join(ValueExpression separator, ValueExpression values) => Static().Invoke(nameof(string.Join), [separator, values]).As(); + public static ValueExpression Split(this ScopedApi stringExpression, ValueExpression separator) + => stringExpression.Invoke(nameof(string.Split), [separator], null, false); + public static ScopedApi Substring(this ScopedApi stringExpression, ValueExpression startIndex) => stringExpression.Invoke(nameof(string.Substring), [startIndex], null, false).As(); diff --git a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Encode/Array/EncodeArrayTests.cs b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Encode/Array/EncodeArrayTests.cs new file mode 100644 index 00000000000..fda1d9d1423 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Encode/Array/EncodeArrayTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Threading.Tasks; +using Encode._Array; +using Encode._Array._Property; +using NUnit.Framework; + +namespace TestProjects.Spector.Tests.Http.Encode.Array +{ + public class EncodeArrayTests : SpectorTestBase + { + [SpectorTest] + public Task CommaDelimited() => Test(async (host) => + { + var testData = new List { "blue", "red", "green" }; + var body = new CommaDelimitedArrayProperty(testData); + + ClientResult result = await new ArrayClient(host, null).GetPropertyClient().CommaDelimitedAsync(body); + Assert.AreEqual(200, result.GetRawResponse().Status); + Assert.AreEqual(testData, result.Value.Value); + }); + + [SpectorTest] + public Task SpaceDelimited() => Test(async (host) => + { + var testData = new List { "blue", "red", "green" }; + var body = new SpaceDelimitedArrayProperty(testData); + + ClientResult result = await new ArrayClient(host, null).GetPropertyClient().SpaceDelimitedAsync(body); + Assert.AreEqual(200, result.GetRawResponse().Status); + Assert.AreEqual(testData, result.Value.Value); + }); + + [SpectorTest] + public Task PipeDelimited() => Test(async (host) => + { + var testData = new List { "blue", "red", "green" }; + var body = new PipeDelimitedArrayProperty(testData); + + ClientResult result = await new ArrayClient(host, null).GetPropertyClient().PipeDelimitedAsync(body); + Assert.AreEqual(200, result.GetRawResponse().Status); + Assert.AreEqual(testData, result.Value.Value); + }); + + [SpectorTest] + public Task NewlineDelimited() => Test(async (host) => + { + var testData = new List { "blue", "red", "green" }; + var body = new NewlineDelimitedArrayProperty(testData); + + ClientResult result = await new ArrayClient(host, null).GetPropertyClient().NewlineDelimitedAsync(body); + Assert.AreEqual(200, result.GetRawResponse().Status); + Assert.AreEqual(testData, result.Value.Value); + }); + + + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj index 467b32720de..d02bd82aef4 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj +++ b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/encode/array/tspCodeModel.json b/packages/http-client-csharp/generator/TestProjects/Spector/http/encode/array/tspCodeModel.json index 627bb564cbf..e4f470080a2 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/encode/array/tspCodeModel.json +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/encode/array/tspCodeModel.json @@ -172,7 +172,8 @@ "name": "value" } }, - "isHttpMetadata": false + "isHttpMetadata": false, + "encode": "commaDelimited" } ] }, @@ -204,7 +205,8 @@ "name": "value" } }, - "isHttpMetadata": false + "isHttpMetadata": false, + "encode": "spaceDelimited" } ] }, @@ -236,7 +238,8 @@ "name": "value" } }, - "isHttpMetadata": false + "isHttpMetadata": false, + "encode": "pipeDelimited" } ] }, @@ -268,7 +271,8 @@ "name": "value" } }, - "isHttpMetadata": false + "isHttpMetadata": false, + "encode": "newlineDelimited" } ] }