Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b4f5cd7
minor: added long path support to prerequisite
radhgupta Oct 22, 2025
ccef568
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Oct 23, 2025
3486047
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Oct 31, 2025
25e823a
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Nov 4, 2025
7958158
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Nov 13, 2025
5227fce
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Nov 13, 2025
175f199
Merge remote-tracking branch 'upstream/main'
radhgupta Nov 14, 2025
b39a7f4
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Dec 1, 2025
699978e
Merge branch 'main' of https://github.com/microsoft/typespec
radhgupta Dec 15, 2025
d0909f3
Merge remote-tracking branch 'upstream/main'
radhgupta Jan 6, 2026
17ae707
Merge remote-tracking branch 'upstream/main'
radhgupta Jan 7, 2026
339819f
Merge remote-tracking branch 'upstream/main'
radhgupta Jan 13, 2026
9cb6c4c
Merge remote-tracking branch 'upstream/main'
radhgupta Jan 21, 2026
6606677
csv encoding
radhgupta Jan 21, 2026
fbbfaf5
geenrate script
radhgupta Jan 22, 2026
4be7888
serealization format used
radhgupta Jan 26, 2026
b2757da
Refactoring
radhgupta Jan 27, 2026
434bb4a
refactoring
radhgupta Jan 27, 2026
1e737ab
Merge branch 'main' into csv-encoding
radhgupta Jan 27, 2026
4013b4a
added unit tests
radhgupta Jan 29, 2026
1ca3af7
Merge branch 'csv-encoding' of https://github.com/radhgupta/typespec …
radhgupta Jan 29, 2026
759da07
refactored code
radhgupta Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export interface InputModelProperty extends InputPropertyTypeBase {
serializationOptions: SerializationOptions;
flatten: boolean;
isHttpMetadata: boolean;
encode?: string;
}

export type InputProperty = InputModelProperty | InputParameter;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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())"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what exactly is this testing? Also, any reason why we put this test in the ModelCustomizationTests suite?

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()"));
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Converts a string representation to ArrayKnownEncoding.
/// </summary>
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
};
}

/// <summary>
/// Converts ArrayKnownEncoding to SerializationFormat.
/// </summary>
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")
};
}

/// <summary>
/// Gets the delimiter string for array serialization format.
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.TypeSpec.Generator.Input
{
/// <summary>
/// Represents the known encoding formats for arrays.
/// </summary>
public enum ArrayKnownEncoding
{
/// <summary>
/// Comma-delimited array encoding
/// </summary>
CommaDelimited,

/// <summary>
/// Space-delimited array encoding
/// </summary>
SpaceDelimited,

/// <summary>
/// Pipe-delimited array encoding
/// </summary>
PipeDelimited,

/// <summary>
/// Newline-delimited array encoding
/// </summary>
NewlineDelimited
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand All @@ -30,11 +33,13 @@ public InputModelProperty(
IsDiscriminator = isDiscriminator;
IsHttpMetadata = isHttpMetadata;
SerializationOptions = serializationOptions;
Encode = encode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should follow the same pattern we use for other encoding formats. We can define a new ArrayKnownEncoding enum and add each of the possible values and then flow this through to a SerializationFormat. See https://github.com/microsoft/typespec/blob/main/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs#L340

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoshLove-msft the factory method only accepts an InputType. Should we add a new overload for the factory method for InputProperty ? ie.

public SerializationFormat GetSerializationFormat(InputProperty input) => input switch
{
    InputModelProperty modelProperty => modelProperty.Encode switch
    {
        ArrayKnownEncoding.PipeDelimited => SerializationFormat.ArrayPipeDelimited,
    },
    _ => SerializationFormat.Default
};

Copy link
Contributor

@JoshLove-msft JoshLove-msft Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current approach is okay since these encodings are handled in PropertyProvider and we shouldn't need it outside of that scope. That said, we still need to update to use an ArrayKnownEncoding enum instead of a string.

}

public bool IsDiscriminator { get; internal set; }
public InputSerializationOptions? SerializationOptions { get; internal set; }
public bool IsHttpMetadata { get; internal set; }
public string? Encode { get; internal set; }

/// <summary>
/// Updates the properties of the input model property.
Expand Down Expand Up @@ -117,5 +122,14 @@ public void Update(
SerializationOptions = serializationOptions;
}
}

public ArrayKnownEncoding? GetArrayEncoding()
{
if (ArrayKnownEncodingExtensions.TryParse(Encode, out var encoding))
{
return encoding;
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -63,6 +64,7 @@ internal static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader rea
bool isDiscriminator = false;
IReadOnlyList<InputDecoratorInfo>? decorators = null;
InputSerializationOptions? serializationOptions = null;
string? encode = null;

while (reader.TokenType != JsonTokenType.EndObject)
{
Expand All @@ -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)
{
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ public enum SerializationFormat
Bytes_Base64Url,
Bytes_Base64,
Int_String,
Array_CommaDelimited,
Array_SpaceDelimited,
Array_PipeDelimited,
Array_NewlineDelimited,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Converts this property to a parameter.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ public static ScopedApi<string> Format(ScopedApi<string> format, params ValueExp
public static ScopedApi<bool> IsNullOrWhiteSpace(ScopedApi<string> value, params ValueExpression[] args)
=> Static<string>().Invoke(nameof(string.IsNullOrWhiteSpace), args.Prepend(value).ToArray()).As<bool>();

public static ScopedApi<bool> IsNullOrEmpty(ScopedApi<string> value)
=> Static<string>().Invoke(nameof(string.IsNullOrEmpty), [value]).As<bool>();

public static ScopedApi<string> Join(ValueExpression separator, ValueExpression values)
=> Static<string>().Invoke(nameof(string.Join), [separator, values]).As<string>();

public static ValueExpression Split(this ScopedApi<string> stringExpression, ValueExpression separator)
=> stringExpression.Invoke(nameof(string.Split), [separator], null, false);

public static ScopedApi<string> Substring(this ScopedApi<string> stringExpression, ValueExpression startIndex)
=> stringExpression.Invoke(nameof(string.Substring), [startIndex], null, false).As<string>();

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> { "blue", "red", "green" };
var body = new CommaDelimitedArrayProperty(testData);

ClientResult<CommaDelimitedArrayProperty> 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<string> { "blue", "red", "green" };
var body = new SpaceDelimitedArrayProperty(testData);

ClientResult<SpaceDelimitedArrayProperty> 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<string> { "blue", "red", "green" };
var body = new PipeDelimitedArrayProperty(testData);

ClientResult<PipeDelimitedArrayProperty> 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<string> { "blue", "red", "green" };
var body = new NewlineDelimitedArrayProperty(testData);

ClientResult<NewlineDelimitedArrayProperty> result = await new ArrayClient(host, null).GetPropertyClient().NewlineDelimitedAsync(body);
Assert.AreEqual(200, result.GetRawResponse().Status);
Assert.AreEqual(testData, result.Value.Value);
});


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\client\structure\multi-client\src\Client.Structure.Service.Multi.Client.csproj" Aliases="ClientStructureMultiClient" />
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\client\structure\renamed-operation\src\Client.Structure.Service.Renamed.Operation.csproj" Aliases="ClientStructureRenamedOperation" />
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\client\structure\two-operation-group\src\Client.Structure.Service.TwoOperationGroup.csproj" Aliases="ClientStructureTwoOperationGroup" />
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\encode\array\src\Encode.Array.csproj" />
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\documentation\src\Documentation.csproj" />
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\encode\bytes\src\Encode.Bytes.csproj" />
<ProjectReference Include="$(RepoRoot)\TestProjects\Spector\http\encode\datetime\src\Encode.Datetime.csproj" />
Expand Down
Loading