diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs index a9125f412a2..c222c9d63f4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs @@ -41,13 +41,12 @@ public static ParameterProvider ClientOptions(CSharpType clientOptionsType) private static readonly FormattableString RequestContentDescription = $"The content to send as the body of the request."; private const string RequestContentParameterName = "content"; - public static ParameterProvider CreateRequestContent(InputParameter? parameter = null, bool optional = false, bool nullable = false) => new( + public static ParameterProvider CreateRequestContent(bool optional = false, bool nullable = false) => new( RequestContentParameterName, RequestContentDescription, ScmCodeModelGenerator.Instance.TypeFactory.RequestContentApi.RequestContentType, location: ParameterLocation.Body, - defaultValue: optional ? Null : null, - inputParameter: parameter) + defaultValue: optional ? Null : null) { Validation = nullable ? ParameterValidationType.None : ParameterValidationType.AssertNotNull, }; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 747e51bcc2f..77c12bf3418 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -1316,11 +1316,11 @@ internal static List GetMethodParameters( { if (methodType == ScmMethodKind.CreateRequest) { - parameter = ScmKnownParameters.CreateRequestContent(inputParam); + parameter = ScmKnownParameters.CreateRequestContent(); } else { - parameter = ScmKnownParameters.CreateRequestContent(inputParam, + parameter = ScmKnownParameters.CreateRequestContent( optional: parameter.DefaultValue != null); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index fb0bb6e6025..a9095402ae9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -1509,7 +1509,7 @@ private ParameterProvider ProcessOptionalParameters( if (optionalParameter.IsContentParameter) { var nullableRequiredContent = - ScmKnownParameters.CreateRequestContent(optionalParameter.InputParameter, nullable: true); + ScmKnownParameters.CreateRequestContent(nullable: true); requiredParameters.Add(nullableRequiredContent); // Update the body param in the underlying collection var bodyParamIndex = ProtocolMethodParameters.IndexOf(optionalParameter); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs index f5cb1a54a62..7515ce441c1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs @@ -55,7 +55,7 @@ protected override void CompareModels(XmlAdvancedModel model, XmlAdvancedModel m Assert.AreEqual(model.Metadata.Count, model2.Metadata.Count); // Compare date/time and duration - Assert.AreEqual(model.CreatedAt, model2.CreatedAt); + Assert.AreEqual(model.CreatedOn, model2.CreatedOn); Assert.AreEqual(model.Duration, model2.Duration); // Compare enums @@ -102,7 +102,7 @@ protected override void VerifyModel(XmlAdvancedModel model, string format) Assert.AreEqual("value2", model.Metadata["key2"]); // Verify date/time - Assert.AreEqual(new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero), model.CreatedAt); + Assert.AreEqual(new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero), model.CreatedOn); Assert.AreEqual(new TimeSpan(1, 30, 0), model.Duration); // Verify enums diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs index a64a6102cfb..a2e945aa23e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs @@ -63,10 +63,10 @@ internal virtual void XmlModelWriteCore(global::System.Xml.XmlWriter writer, glo throw new global::System.FormatException($"The model {nameof(global::Sample.Models.TestXmlModel)} does not support writing '{format}' format."); } - if (global::Sample.Optional.IsDefined(Timestamp)) + if (global::Sample.Optional.IsDefined(On)) { writer.WriteStartElement("timestamp"); - writer.WriteStringValue(Timestamp.Value, "O"); + writer.WriteStringValue(On.Value, "O"); writer.WriteEndElement(); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs index c34ae949f14..d0ae40f8f84 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs @@ -261,8 +261,8 @@ public void XmlDeserializationHandlesDateTimeOffsetProperty() Assert.IsNotNull(xmlDeserializationMethod); var methodBody = xmlDeserializationMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(methodBody.Contains("timestamp = child.GetDateTimeOffset(\"O\")"), - $"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}"); + Assert.IsTrue(methodBody.Contains("GetDateTimeOffset(\"O\")"), + $"DateTimeOffset property should use RFC3339 format. Actual:\n{methodBody}"); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs index 91417351981..563329b08be 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs @@ -335,7 +335,7 @@ public void XmlSerializationHandlesDateTimeOffsetProperty() Assert.IsNotNull(xmlSerializationMethod); var methodBody = xmlSerializationMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(methodBody.Contains("WriteStringValue") && methodBody.Contains("Timestamp"), + Assert.IsTrue(methodBody.Contains("writer.WriteStringValue(On.Value, \"O\")"), $"DateTimeOffset property should be serialized with WriteStringValue. Actual:\n{methodBody}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs index b253e910c6f..496612af7d3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs @@ -63,7 +63,10 @@ public sealed class ParameterProvider : IEquatable public ParameterProvider(InputParameter inputParameter) { InputParameter = inputParameter; - Name = inputParameter.Name; + Name = inputParameter is InputMethodParameter && !inputParameter.IsExactName + && inputParameter.Type.IsDateTimeInputType() + ? inputParameter.Name.NormalizeDateTimeSuffix() + : inputParameter.Name; Description = DocHelpers.GetFormattableDescription(inputParameter.Summary, inputParameter.Doc) ?? FormattableStringHelpers.Empty; var type = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputParameter.Type) ?? throw new InvalidOperationException($"Failed to create CSharpType for {inputParameter.Type}"); if (!inputParameter.IsRequired) 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 b8dfe9fe276..4984219b395 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 @@ -111,7 +111,8 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T (lastContractProperties is null || !lastContractProperties.Any(p => p.Name == legacyName))) { - identifierName = identifierName.NormalizeCSharpAcronyms(); + identifierName = identifierName + .NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType()); } Name = identifierName == enclosingType.Name ? $"{identifierName}Property" diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs index ee86f836a90..f1896e15e89 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Text; +using Microsoft.TypeSpec.Generator.Input; namespace Microsoft.TypeSpec.Generator.Utilities { @@ -20,8 +21,9 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu ("Os", "OS") ]; - public static string NormalizeCSharpAcronyms(this string name) + public static string NormalizeCSharpAcronyms(this string name, bool normalizeDateTimeSuffix = false) { + name = normalizeDateTimeSuffix ? name.NormalizeDateTimeSuffix() : name; StringBuilder? normalizedName = null; int segmentStart = 0; for (int index = 0; index < name.Length - 1; index++) @@ -57,6 +59,85 @@ public static string NormalizeCSharpAcronyms(this string name) return normalizedName.ToString(); } + public static string NormalizeDateTimeSuffix(this string name) + { + if (DateTimeNameRules.HasExcludedComponent(name)) + { + return name; + } + + var suffixLength = DateTimeNameRules.GetSuffixLength(name); + if (suffixLength == 0) + { + return name; + } + + var prefix = name[..^suffixLength]; + var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) + ? DateTimeNameRules.LowercaseOnSuffix + : DateTimeNameRules.OnSuffix; + return prefix + onSuffix; + } + + private static class DateTimeNameRules + { + private const string AtSuffix = "At"; + private const string DateSuffix = "Date"; + private const string DateTimeSuffix = "DateTime"; + private const string FromName = "From"; + internal const string LowercaseOnSuffix = "on"; + internal const string OnSuffix = "On"; + private const string PointInTimeName = "PointInTime"; + private const string TimeStampSuffix = "TimeStamp"; + private const string TimeSuffix = "Time"; + private const string TimestampSuffix = "Timestamp"; + private const string ToName = "To"; + + internal static bool HasExcludedComponent(string name) + { + return name.StartsWith(FromName, StringComparison.OrdinalIgnoreCase) || + name.StartsWith(ToName, StringComparison.OrdinalIgnoreCase) || + name.EndsWith(PointInTimeName, StringComparison.OrdinalIgnoreCase); + } + + internal static int GetSuffixLength(string name) + { + if (name.EndsWith(TimestampSuffix, StringComparison.Ordinal) || + name.EndsWith(TimeStampSuffix, StringComparison.Ordinal) || + name.Equals(TimestampSuffix, StringComparison.OrdinalIgnoreCase)) + { + return TimestampSuffix.Length; + } + + if (name.Length > DateTimeSuffix.Length && name.EndsWith(DateTimeSuffix, StringComparison.Ordinal)) + { + return DateTimeSuffix.Length; + } + + if (name.Length > TimeSuffix.Length && name.EndsWith(TimeSuffix, StringComparison.Ordinal)) + { + return TimeSuffix.Length; + } + + if (name.Equals(DateSuffix, StringComparison.OrdinalIgnoreCase) || + name.EndsWith(DateSuffix, StringComparison.Ordinal)) + { + return DateSuffix.Length; + } + + return name.Length > AtSuffix.Length && name.EndsWith(AtSuffix, StringComparison.Ordinal) + ? AtSuffix.Length + : 0; + } + } + + public static bool IsDateTimeInputType(this InputType inputType) => inputType switch + { + InputDateTimeType => true, + InputPrimitiveType { Kind: InputPrimitiveTypeKind.PlainDate } => true, + InputNullableType nullableType => IsDateTimeInputType(nullableType.Type), + _ => false + }; [return: NotNullIfNotNull(nameof(name))] public static string? NormalizeCSharpUrlSuffix(this string? name) => !string.IsNullOrEmpty(name) && name.EndsWith("Url", StringComparison.Ordinal) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index 979d40194ca..b08a6bd9368 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; @@ -189,13 +190,9 @@ public static void RestorePreviousParameterNames( string? preservedName = null; var inputParameter = parameter.InputParameter; - if (inputParameter is not null && string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal)) + if (inputParameter is not null) { - var originalName = inputParameter.OriginalName; - if (!string.IsNullOrEmpty(originalName)) - { - preservedName = FindPreviousParameterName(lastContractView, originalName, method.Signature.Name); - } + preservedName = FindPreviousParameterName(lastContractView, inputParameter.OriginalName, method.Signature.Name); } // Fall back to a positional match for synthesized parameters diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs index c4671db76a8..a3fe1cea310 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs @@ -67,6 +67,27 @@ public void ValidateArrayHandling() Assert.IsTrue(parameter.ToPublicInputParameter().Type.Equals(typeof(IEnumerable))); } + [TestCaseSource(nameof(DateTimeParameterNameTestCases))] + public void MethodParameterNameNormalizesDateTimeSuffix( + string inputName, + InputType inputType, + bool isExactName, + string expectedName) + { + MockHelpers.LoadMockGenerator(); + var inputParameter = InputFactory.MethodParameter( + inputName, + inputType, + isRequired: true, + isExactName: isExactName); + + var parameter = CodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParameter); + + Assert.IsNotNull(parameter); + Assert.AreEqual(expectedName, parameter!.Name); + Assert.AreEqual(inputName, parameter.WireInfo.SerializedName); + } + private static IEnumerable ValueInputTypes() { yield return InputPrimitiveType.Int32; @@ -74,6 +95,36 @@ private static IEnumerable ValueInputTypes() yield return InputFactory.Int32Enum("inputEnum", [("foo", 1)], isExtensible: true); } + private static IEnumerable DateTimeParameterNameTestCases() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + var testCases = new (string Name, InputType Type, string NormalizedName)[] + { + ("startTime", dateTime, "startOn"), + ("createdAt", dateTime, "createdOn"), + ("timestamp", dateTime, "on"), + ("date", InputPrimitiveType.PlainDate, "on"), + ("modifiedAt", dateTime.WithNullable(true), "modifiedOn"), + ("fromTime", dateTime, "fromTime"), + ("toDate", dateTime, "toDate"), + ("pointInTime", dateTime, "pointInTime"), + ("recoveryPointInTime", dateTime, "recoveryPointInTime"), + ("startTime", InputPrimitiveType.String, "startTime"), + ("creationTimestamp", InputPrimitiveType.String, "creationTimestamp") + }; + + foreach (var testCase in testCases) + { + yield return new TestCaseData(testCase.Name, testCase.Type, false, testCase.NormalizedName); + yield return new TestCaseData(testCase.Name, testCase.Type, true, testCase.Name); + } + } + private static IEnumerable NotEqualsTestCases() { yield return new TestCaseData( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs index 20529648892..fb58ca4f84d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; @@ -129,6 +130,26 @@ public void TestPropertyNameNormalizesAcronymCasing(string inputName, bool isExa Assert.AreEqual(expectedName, property.Name); } + [TestCaseSource(nameof(DateTimePropertyNameTestCases))] + public void TestPropertyNameNormalizesDateTimeSuffix( + string inputName, + InputType inputType, + bool isExactName, + string expectedName) + { + var inputProperty = InputFactory.Property( + inputName, + inputType, + isRequired: true, + isExactName: isExactName); + InputFactory.Model("TestModel", properties: [inputProperty]); + + var property = new PropertyProvider(inputProperty, new TestTypeProvider()); + + Assert.AreEqual(expectedName, property.Name); + Assert.AreEqual(inputName.ToVariableName(), property.WireInfo?.SerializedName); + } + [TestCase("Ipv4", false, "ipv4")] [TestCase("Ipv6", false, "ipv6")] [TestCase("IpAddress", false, "ipAddress")] @@ -151,6 +172,32 @@ public void TestPropertyParameterDeclarationNormalizesAcronymCasing(string input Assert.AreEqual(expectedName, property.AsParameter.AsVariable().Declaration.RequestedName); } + [Test] + public async Task TestPropertyNamePreservesLastContractDateTimeSuffix() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "StartTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + var modelProvider = new ModelProvider(inputModel); + var actual = new TypeProviderWriter(modelProvider).Write().Content; + + Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), actual); + } + [TestCaseSource(nameof(CollectionPropertyTestCases))] public void CollectionProperty(CSharpType coreType, InputModelProperty collectionProperty, CSharpType expectedType) { @@ -224,6 +271,32 @@ public void TestPropertyNameConflictsWithTypeNameAfterAcronymNormalization() Assert.AreEqual("IPAddressProperty", property.Name); } + private static IEnumerable DateTimePropertyNameTestCases() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + yield return new TestCaseData("StartTime", dateTime, false, "StartOn"); + yield return new TestCaseData("CreatedAt", dateTime, false, "CreatedOn"); + yield return new TestCaseData("DeletionTimestamp", dateTime, false, "DeletionOn"); + yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModificationOn"); + yield return new TestCaseData("Timestamp", dateTime, false, "On"); + yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpirationOn"); + yield return new TestCaseData("RecordedAt", InputPrimitiveType.String, false, "RecordedAt"); + yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, false, "On"); + yield return new TestCaseData("SnapshotTimestamp", dateTime.WithNullable(true), false, "SnapshotOn"); + yield return new TestCaseData("FromTime", dateTime, false, "FromTime"); + yield return new TestCaseData("ToDate", dateTime, false, "ToDate"); + yield return new TestCaseData("RecoveryPointInTime", dateTime, false, "RecoveryPointInTime"); + yield return new TestCaseData("StartTime", InputPrimitiveType.String, false, "StartTime"); + yield return new TestCaseData("CreationTimestamp", InputPrimitiveType.String, false, "CreationTimestamp"); + yield return new TestCaseData("CreationTimestamp", dateTime, true, "CreationTimestamp"); + } + + [Test] public void CanUpdatePropertyProvider() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs new file mode 100644 index 00000000000..2bc79bbeadc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs @@ -0,0 +1,27 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Test +{ + public partial class TestModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public TestModel(global::System.DateTimeOffset startTime) + { + StartTime = startTime; + } + + internal TestModel(global::System.DateTimeOffset startTime, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartTime = startTime; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset StartTime { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs new file mode 100644 index 00000000000..d3860bb862d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartTime { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs new file mode 100644 index 00000000000..88494a19a32 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using System; + +namespace Test +{ + public partial class TestClient + { + public string Foo(global::System.DateTimeOffset startTime) + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs new file mode 100644 index 00000000000..a38628fee6c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestClient + { + public string Foo(string startTime) { return null; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index b4b6d14584d..303f8157a8a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -786,6 +786,29 @@ public void FindPreviousParameterNameReturnsNullWithoutLastContract() Assert.IsNull(BackCompatHelper.FindPreviousParameterName(typeProvider.LastContractView, "oldParam", "Foo")); } + [Test] + public async Task RestorePreviousParameterNamesRestoresNormalizedMethodParameterName() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var parameter = new ParameterProvider(InputFactory.MethodParameter("startTime", dateTime, isRequired: true)); + var method = new MethodProvider( + new MethodSignature("Foo", $"", MethodSignatureModifiers.Public, new CSharpType(typeof(string)), $"", [parameter]), + Snippet.Return(Snippet.Null), + new TestTypeProvider()); + var typeProvider = new TestTypeProvider(name: "TestClient", methods: [method]); + + BackCompatHelper.RestorePreviousParameterNames(typeProvider, typeProvider.Methods); + + var actual = new TypeProviderWriter(typeProvider).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), actual); + } + // A reorder combined with a casing-only rename must still restore the published spelling. // The reorder matches parameters via ToVariableName (so current "URL" aligns with published // "url"), which previously short-circuited the exact-name pass and left the CP0017-breaking diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs index 1e84328cb7f..3cf3937eac2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Utilities; using NUnit.Framework; @@ -19,5 +21,41 @@ public void TestNormalizeCSharpUrlSuffix(string name, string expected) { Assert.AreEqual(expected, name.NormalizeCSharpUrlSuffix()); } + + [TestCaseSource(nameof(DateTimeNameTestCases))] + public void TestNormalizeDateTimeSuffix(string name, InputType type, string expected) + { + Assert.AreEqual(expected, type.IsDateTimeInputType() ? name.NormalizeDateTimeSuffix() : name); + } + + [Test] + public void NormalizeCSharpAcronymsNormalizesDateTimeSuffixInSinglePass() + { + Assert.AreEqual("IPStartOn", "IpStartTime".NormalizeCSharpAcronyms(normalizeDateTimeSuffix: true)); + } + + private static IEnumerable DateTimeNameTestCases() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + yield return new TestCaseData("startTime", dateTime, "startOn"); + yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, "On"); + yield return new TestCaseData("date", InputPrimitiveType.PlainDate, "on"); + yield return new TestCaseData("fromTime", dateTime, "fromTime"); + yield return new TestCaseData("toDate", dateTime, "toDate"); + yield return new TestCaseData("pointInTime", dateTime, "pointInTime"); + yield return new TestCaseData("recoveryPointInTime", dateTime, "recoveryPointInTime"); + yield return new TestCaseData("startTime", InputPrimitiveType.String, "startTime"); + yield return new TestCaseData("createdAt", dateTime, "createdOn"); + yield return new TestCaseData("expiresAt", dateTime, "expiresOn"); + yield return new TestCaseData("deletedTime", dateTime, "deletedOn"); + yield return new TestCaseData("finishedTime", dateTime, "finishedOn"); + yield return new TestCaseData("stateTransitionTime", dateTime, "stateTransitionOn"); + yield return new TestCaseData("notBefore", dateTime, "notBefore"); + } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs index f1d688b9098..e1cf5add97d 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs @@ -226,7 +226,7 @@ internal virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptio } writer.WriteEndElement(); writer.WriteStartElement("createdAt"); - writer.WriteStringValue(CreatedAt, "O"); + writer.WriteStringValue(CreatedOn, "O"); writer.WriteEndElement(); writer.WriteStartElement("duration"); writer.WriteStringValue(Duration, "P"); @@ -390,7 +390,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M XmlNestedModel nestedModel = default; XmlNestedModel optionalNestedModel = default; IDictionary metadata = default; - DateTimeOffset createdAt = default; + DateTimeOffset createdOn = default; TimeSpan duration = default; BinaryData data = default; IDictionary optionalRecordUnknown = default; @@ -544,7 +544,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M } if (localName == "createdAt") { - createdAt = child.GetDateTimeOffset("O"); + createdOn = child.GetDateTimeOffset("O"); continue; } if (localName == "duration") @@ -717,7 +717,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M nestedModel, optionalNestedModel, metadata, - createdAt, + createdOn, duration, data, optionalRecordUnknown ?? new ChangeTrackingDictionary(), diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs index 32a82daf882..e52d1f3f9d5 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs @@ -34,7 +34,7 @@ public partial class XmlAdvancedModel /// A wrapped array with custom wrapper name. /// A nested model property. /// A dictionary property. - /// A date-time property. + /// A date-time property. /// A duration property. /// A bytes property. /// A fixed enum property. @@ -51,7 +51,7 @@ public partial class XmlAdvancedModel /// /// /// , , , , , , , , , , , , , , , , , , , , , or is null. - public XmlAdvancedModel(string name, int age, bool enabled, float score, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IEnumerable unwrappedStrings, IEnumerable unwrappedCounts, IEnumerable unwrappedItems, IEnumerable wrappedColors, IEnumerable items, XmlNestedModel nestedModel, IDictionary metadata, DateTimeOffset createdAt, TimeSpan duration, BinaryData data, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, string label, int daysUsed, IEnumerable fooItems, XmlNestedModel anotherModel, IEnumerable modelsWithNamespaces, IEnumerable unwrappedModelsWithNamespaces, IEnumerable> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IEnumerable> listOfDictionaryFoo) + public XmlAdvancedModel(string name, int age, bool enabled, float score, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IEnumerable unwrappedStrings, IEnumerable unwrappedCounts, IEnumerable unwrappedItems, IEnumerable wrappedColors, IEnumerable items, XmlNestedModel nestedModel, IDictionary metadata, DateTimeOffset createdOn, TimeSpan duration, BinaryData data, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, string label, int daysUsed, IEnumerable fooItems, XmlNestedModel anotherModel, IEnumerable modelsWithNamespaces, IEnumerable unwrappedModelsWithNamespaces, IEnumerable> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IEnumerable> listOfDictionaryFoo) { Argument.AssertNotNull(name, nameof(name)); Argument.AssertNotNull(id, nameof(id)); @@ -95,7 +95,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string Items = items.ToList(); NestedModel = nestedModel; Metadata = metadata; - CreatedAt = createdAt; + CreatedOn = createdOn; Duration = duration; Data = data; OptionalRecordUnknown = new ChangeTrackingDictionary(); @@ -136,7 +136,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string /// A nested model property. /// An optional nested model. /// A dictionary property. - /// A date-time property. + /// A date-time property. /// A duration property. /// A bytes property. /// optional record of unknown. @@ -155,7 +155,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string /// /// /// - internal XmlAdvancedModel(string name, int age, bool enabled, float score, string optionalString, int? optionalInt, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IList unwrappedStrings, IList unwrappedCounts, IList unwrappedItems, IList wrappedColors, IList items, XmlNestedModel nestedModel, XmlNestedModel optionalNestedModel, IDictionary metadata, DateTimeOffset createdAt, TimeSpan duration, BinaryData data, IDictionary optionalRecordUnknown, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, IntFixedEnum? optionalFixedEnum, IntExtensibleEnum? optionalExtensibleEnum, string label, int daysUsed, IList fooItems, XmlNestedModel anotherModel, IList modelsWithNamespaces, IList unwrappedModelsWithNamespaces, IList> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IList> listOfDictionaryFoo) + internal XmlAdvancedModel(string name, int age, bool enabled, float score, string optionalString, int? optionalInt, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IList unwrappedStrings, IList unwrappedCounts, IList unwrappedItems, IList wrappedColors, IList items, XmlNestedModel nestedModel, XmlNestedModel optionalNestedModel, IDictionary metadata, DateTimeOffset createdOn, TimeSpan duration, BinaryData data, IDictionary optionalRecordUnknown, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, IntFixedEnum? optionalFixedEnum, IntExtensibleEnum? optionalExtensibleEnum, string label, int daysUsed, IList fooItems, XmlNestedModel anotherModel, IList modelsWithNamespaces, IList unwrappedModelsWithNamespaces, IList> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IList> listOfDictionaryFoo) { Name = name; Age = age; @@ -178,7 +178,7 @@ internal XmlAdvancedModel(string name, int age, bool enabled, float score, strin NestedModel = nestedModel; OptionalNestedModel = optionalNestedModel; Metadata = metadata; - CreatedAt = createdAt; + CreatedOn = createdOn; Duration = duration; Data = data; OptionalRecordUnknown = optionalRecordUnknown; @@ -263,7 +263,7 @@ internal XmlAdvancedModel(string name, int age, bool enabled, float score, strin public IDictionary Metadata { get; } /// A date-time property. - public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset CreatedOn { get; set; } /// A duration property. public TimeSpan Duration { get; set; } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs index 3a65a21dc61..65fc0e6464a 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs @@ -270,7 +270,7 @@ public static AnotherDynamicModel AnotherDynamicModel(string bar = default) /// A nested model property. /// An optional nested model. /// A dictionary property. - /// A date-time property. + /// A date-time property. /// A duration property. /// A bytes property. /// optional record of unknown. @@ -290,7 +290,7 @@ public static AnotherDynamicModel AnotherDynamicModel(string bar = default) /// /// /// A new instance for mocking. - public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = default, bool enabled = default, float score = default, string optionalString = default, int? optionalInt = default, string nullableString = default, string id = default, int version = default, bool isActive = default, string originalName = default, string xmlIdentifier = default, string content = default, IEnumerable unwrappedStrings = default, IEnumerable unwrappedCounts = default, IEnumerable unwrappedItems = default, IEnumerable wrappedColors = default, IEnumerable items = default, XmlNestedModel nestedModel = default, XmlNestedModel optionalNestedModel = default, IDictionary metadata = default, DateTimeOffset createdAt = default, TimeSpan duration = default, BinaryData data = default, IDictionary optionalRecordUnknown = default, StringFixedEnum fixedEnum = default, StringExtensibleEnum extensibleEnum = default, IntFixedEnum? optionalFixedEnum = default, IntExtensibleEnum? optionalExtensibleEnum = default, string label = default, int daysUsed = default, IEnumerable fooItems = default, XmlNestedModel anotherModel = default, IEnumerable modelsWithNamespaces = default, IEnumerable unwrappedModelsWithNamespaces = default, IEnumerable> listOfListFoo = default, IDictionary dictionaryFoo = default, IDictionary> dictionaryOfDictionaryFoo = default, IDictionary> dictionaryListFoo = default, IEnumerable> listOfDictionaryFoo = default) + public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = default, bool enabled = default, float score = default, string optionalString = default, int? optionalInt = default, string nullableString = default, string id = default, int version = default, bool isActive = default, string originalName = default, string xmlIdentifier = default, string content = default, IEnumerable unwrappedStrings = default, IEnumerable unwrappedCounts = default, IEnumerable unwrappedItems = default, IEnumerable wrappedColors = default, IEnumerable items = default, XmlNestedModel nestedModel = default, XmlNestedModel optionalNestedModel = default, IDictionary metadata = default, DateTimeOffset createdOn = default, TimeSpan duration = default, BinaryData data = default, IDictionary optionalRecordUnknown = default, StringFixedEnum fixedEnum = default, StringExtensibleEnum extensibleEnum = default, IntFixedEnum? optionalFixedEnum = default, IntExtensibleEnum? optionalExtensibleEnum = default, string label = default, int daysUsed = default, IEnumerable fooItems = default, XmlNestedModel anotherModel = default, IEnumerable modelsWithNamespaces = default, IEnumerable unwrappedModelsWithNamespaces = default, IEnumerable> listOfListFoo = default, IDictionary dictionaryFoo = default, IDictionary> dictionaryOfDictionaryFoo = default, IDictionary> dictionaryListFoo = default, IEnumerable> listOfDictionaryFoo = default) { unwrappedStrings ??= new ChangeTrackingList(); unwrappedCounts ??= new ChangeTrackingList(); @@ -330,7 +330,7 @@ public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = nestedModel, optionalNestedModel, metadata, - createdAt, + createdOn, duration, data, optionalRecordUnknown,