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..3fe0464ffcb 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,10 @@ private MethodBodyStatement[] DeserializeProperty( PropertyWireInformation wireInfo, VariableExpression variableExpression, ScopedApi jsonProperty, - IEnumerable serializationAttributes) + IEnumerable serializationAttributes, + SerializationFormat? serializationFormat = 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 +1299,59 @@ 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 (serializationFormat.HasValue && (propertyType.IsList || propertyType.IsArray)) + { + if (ArrayKnownEncodingExtensions.TryGetDelimiter(serializationFormat.Value, out var delimiter)) + { + var elementType = propertyType.ElementType; + if (IsSupportedEncodedArrayElementType(elementType)) + { + deserializationStatements = CreateEncodedArrayDeserializationStatements( + propertyType, variableExpression, jsonProperty, serializationFormat.Value); + } + else + { + // Fall back to default deserialization for unsupported element types + deserializationStatements = + [ + DeserializeValue(propertyType, jsonProperty.Value(), wireInfo.SerializationFormat, out ValueExpression value), + variableExpression.Assign(value).Terminate() + ]; + } + } + else + { + // 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(), wireInfo.SerializationFormat, out ValueExpression value), + variableExpression.Assign(value).Terminate() + ]; } } + else + { + // Default deserialization for non-encoded arrays and other types + deserializationStatements = + [ + DeserializeValue(propertyType, jsonProperty.Value(), wireInfo.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 +1609,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 +1618,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 +1634,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 +1644,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 +1654,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 +1666,22 @@ 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)) + { + if (ArrayKnownEncodingExtensions.TryGetDelimiter(serializationFormat.Value, out var delimiter)) + { + var elementType = propertyType.ElementType; + if (IsSupportedEncodedArrayElementType(elementType)) + { + serializationStatement = CreateEncodedArraySerializationStatement( + propertyType, + propertyExpression, + serializationFormat.Value); + } + } + } + // Check for custom serialization hooks foreach (var attribute in _model.CustomCodeView?.Attributes .Where(a => a.Type.Name == CodeGenAttributes.CodeGenSerializationAttributeName) ?? []) @@ -1810,6 +1858,150 @@ 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) + { + if (!ArrayKnownEncodingExtensions.TryGetDelimiter(serializationFormat, out var delimiter)) + { + ScmCodeModelGenerator.Instance.Emitter.ReportDiagnostic( + DiagnosticCodes.UnsupportedSerialization, + $"Unsupported array serialization format: {serializationFormat}"); + return MethodBodyStatement.Empty; + } + + 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) + { + if (!ArrayKnownEncodingExtensions.TryGetDelimiter(serializationFormat, out var delimiter)) + { + ScmCodeModelGenerator.Instance.Emitter.ReportDiagnostic( + DiagnosticCodes.UnsupportedSerialization, + $"Unsupported array serialization format: {serializationFormat}"); + return []; + } + 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/MrwSerializationTypeDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs index 500b419386b..8132e9f323f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs @@ -1259,5 +1259,75 @@ public void TestSerializationTypeNameMatchesModelProviderName() Assert.AreEqual($"Deserialize{model.Name}", deserializationMethod.Signature.Name, "Deserialization method name should use ModelProvider name"); } + + [TestCase("commaDelimited", ",")] + [TestCase("spaceDelimited", " ")] + [TestCase("pipeDelimited", "|")] + [TestCase("newlineDelimited", "\\n")] + public void TestArrayEncodingSerializationStatement(string encoding, string expectedDelimiter) + { + Enum.TryParse(encoding, ignoreCase: true, out var arrayEncoding); + var arrayType = new InputArrayType("TestArray", "TypeSpec.Array", InputPrimitiveType.String); + var arrayProperty = new InputModelProperty( + "TestArray", + "Test array property summary", + "Test array property", + arrayType, + true, + false, + null, + false, + "testArray", + false, + false, + null, + new(json: new("testArray")), + arrayEncoding); + + var properties = new List { arrayProperty }; + var inputModel = new InputModelType("TestModel", "TestNamespace", "TestModel", "public", null, null, "Test model.", InputModelTypeUsage.Input, properties, null, Array.Empty(), null, null, new Dictionary(), null, false, new(), false); + + var (_, serialization) = CreateModelAndSerialization(inputModel); + var writeMethod = serialization.BuildJsonModelWriteCoreMethod(); + var methodBody = writeMethod.BodyStatements!.ToDisplayString(); + + Assert.IsTrue(methodBody.Contains($"string.Join(\"{expectedDelimiter}\", TestArray)"), + $"Expected serialization to use string.Join with delimiter '{expectedDelimiter}', but got: {methodBody}"); + } + + [TestCase("commaDelimited", ",")] + [TestCase("spaceDelimited", " ")] + [TestCase("pipeDelimited", "|")] + [TestCase("newlineDelimited", "\\n")] + public void TestArrayEncodingDeserializationStatement(string encoding, string expectedDelimiter) + { + Enum.TryParse(encoding, ignoreCase: true, out var arrayEncoding); + var arrayType = new InputArrayType("TestArray", "TypeSpec.Array", InputPrimitiveType.String); + var arrayProperty = new InputModelProperty( + "TestArray", + "Test array property summary", + "Test array property", + arrayType, + true, + false, + null, + false, + "testArray", + false, + false, + null, + new(json: new("testArray")), + arrayEncoding); + + var properties = new List { arrayProperty }; + var inputModel = new InputModelType("TestModel", "TestNamespace", "TestModel", "public", null, null, "Test model.", InputModelTypeUsage.Input, properties, null, Array.Empty(), null, null, new Dictionary(), null, false, new(), false); + + var (_, serialization) = CreateModelAndSerialization(inputModel); + var deserializeMethod = serialization.BuildDeserializationMethod(); + var methodBody = deserializeMethod.BodyStatements!.ToDisplayString(); + + Assert.IsTrue(methodBody.Contains($".Split('{expectedDelimiter}')") || methodBody.Contains($".Split(\"{expectedDelimiter}\")"), + $"Expected deserialization to use Split with delimiter '{expectedDelimiter}', but got: {methodBody}"); + } } } 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..3863078b626 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Extensions/ArrayKnownEncodingExtensions.cs @@ -0,0 +1,41 @@ +// 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 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") + }; + } + + /// + /// Get the delimiter string for array serialization format. + /// + public static bool TryGetDelimiter(SerializationFormat format, out string? delimiter) + { + delimiter = format switch + { + SerializationFormat.Array_CommaDelimited => ",", + SerializationFormat.Array_SpaceDelimited => " ", + SerializationFormat.Array_PipeDelimited => "|", + SerializationFormat.Array_NewlineDelimited => "\n", + _ => null + }; + return delimiter != null; + } + } +} 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..7a0eda712e5 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, + ArrayKnownEncoding? 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 ArrayKnownEncoding? Encode { get; internal set; } /// /// Updates the properties of the input model property. 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..28c24a8d666 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? encodeString = 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 encodeString); 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 = Enum.TryParse(encodeString, ignoreCase: true, out var encode) ? encode : null; 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..d6ffb0fa6ed 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.Encode; + 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" } ] }