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/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index 3ed29d78898..5bcf61bdd0d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -2805,6 +2805,57 @@ private static void AssertHasFields(TypeProvider provider, IReadOnlyList requestOn) on both the protocol and the + // convenience surface, so the previously published name is restored consistently for both and the + // convenience method forwards every argument positionally. When only one surface is normalized, the + // date argument is dropped (passed as null) and the remaining arguments are passed by name. + [Test] + public async Task BackCompatibility_DateParameterNameIsPreservedInConvenienceCall() + { + var dateType = new InputDateTimeType( + DateTimeKnownEncoding.Rfc7231, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var operation = InputFactory.Operation( + "TestMethod", + parameters: + [ + InputFactory.HeaderParameter("requestDate", dateType), + InputFactory.HeaderParameter("ifMatch", InputPrimitiveType.String) + ]); + var method = InputFactory.BasicServiceMethod( + "TestMethod", + operation, + parameters: + [ + InputFactory.MethodParameter("requestDate", dateType, location: InputRequestLocation.Header), + InputFactory.MethodParameter("ifMatch", InputPrimitiveType.String, location: InputRequestLocation.Header) + ]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + clientProvider!.ProcessTypeForBackCompatibility(); + + using var writer = new CodeWriter(); + foreach (var methodName in new[] { "TestMethod", "TestMethodAsync" }) + { + writer.WriteMethod(clientProvider.Methods + .Single(m => m.Signature.Name == methodName && m is ScmMethodProvider { Kind: ScmMethodKind.Protocol })); + writer.WriteMethod(clientProvider.Methods + .Single(m => m.Signature.Name == methodName && m is ScmMethodProvider { Kind: ScmMethodKind.Convenience })); + } + + Assert.AreEqual(Helpers.GetExpectedFromFile(), writer.ToString(false)); + } + [Test] public async Task BackCompatibility_ProtocolMethodParamOrderChanged() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_DateParameterNameIsPreservedInConvenienceCall.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_DateParameterNameIsPreservedInConvenienceCall.cs new file mode 100644 index 00000000000..a20285b016e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_DateParameterNameIsPreservedInConvenienceCall.cs @@ -0,0 +1,18 @@ +public virtual global::System.ClientModel.ClientResult TestMethod(global::System.DateTimeOffset? requestDate, string ifMatch, global::System.ClientModel.Primitives.RequestOptions options) +{ + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateTestMethodRequest(requestDate, ifMatch, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); +} +public virtual global::System.ClientModel.ClientResult TestMethod(global::System.DateTimeOffset? requestDate = default, string ifMatch = default, global::System.Threading.CancellationToken cancellationToken = default) +{ + return this.TestMethod(requestDate, ifMatch, cancellationToken.ToRequestOptions()); +} +public virtual async global::System.Threading.Tasks.Task TestMethodAsync(global::System.DateTimeOffset? requestDate, string ifMatch, global::System.ClientModel.Primitives.RequestOptions options) +{ + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateTestMethodRequest(requestDate, ifMatch, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); +} +public virtual async global::System.Threading.Tasks.Task TestMethodAsync(global::System.DateTimeOffset? requestDate = default, string ifMatch = default, global::System.Threading.CancellationToken cancellationToken = default) +{ + return await this.TestMethodAsync(requestDate, ifMatch, cancellationToken.ToRequestOptions()).ConfigureAwait(false); +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_DateParameterNameIsPreservedInConvenienceCall/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_DateParameterNameIsPreservedInConvenienceCall/TestClient.cs new file mode 100644 index 00000000000..b885949e3bc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_DateParameterNameIsPreservedInConvenienceCall/TestClient.cs @@ -0,0 +1,20 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult TestMethod(DateTimeOffset? requestDate, string ifMatch, RequestOptions options) + { + throw new NotImplementedException(); + } + + public virtual Task TestMethodAsync(DateTimeOffset? requestDate, string ifMatch, RequestOptions options) + { + throw new NotImplementedException(); + } + } +} 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..52666216903 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,7 +261,7 @@ public void XmlDeserializationHandlesDateTimeOffsetProperty() Assert.IsNotNull(xmlDeserializationMethod); var methodBody = xmlDeserializationMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(methodBody.Contains("timestamp = child.GetDateTimeOffset(\"O\")"), + Assert.IsTrue(methodBody.Contains("@on = child.GetDateTimeOffset(\"O\")"), $"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}"); } 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.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs index 92a5c820965..6aadbf6d2f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs @@ -611,6 +611,53 @@ public async Task SpreadModelWithOptionalDictionaryIsNotNull() Assert.AreEqual(Helpers.GetExpectedFromFile(), methodBody); } + [Test] + public async Task ConvenienceMethodForwardsNormalizedDateParameter() + { + var dateType = new InputDateTimeType( + DateTimeKnownEncoding.Rfc7231, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var operation = InputFactory.Operation( + "GetThing", + parameters: [InputFactory.HeaderParameter("requestDate", dateType, isRequired: true)], + responses: [InputFactory.OperationResponse([204])]); + var serviceMethod = InputFactory.BasicServiceMethod( + "GetThing", + operation, + parameters: + [ + InputFactory.MethodParameter( + "requestDate", + dateType, + isRequired: true, + location: InputRequestLocation.Header) + ]); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + await MockHelpers.LoadMockGeneratorAsync(clients: () => [inputClient]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + Assert.IsNotNull(client); + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); + + // Both the protocol and convenience methods use the normalized name so that the + // convenience method still forwards the value to the protocol method. + foreach (var method in methodCollection) + { + Assert.IsTrue(method.Signature.Parameters.Any(p => p.Name == "requestOn")); + } + + var asyncConvenienceMethod = methodCollection.Single(m => + m.Signature.Name.EndsWith("Async") + && m.Signature.Parameters.Any(p => p.Type.Equals(typeof(CancellationToken)))); + using var writer = new CodeWriter(); + writer.WriteMethod(asyncConvenienceMethod); + Assert.AreEqual( + Helpers.GetExpectedFromFile(), + writer.ToString(false)); + } + [Test] public void ListMethodWithNoPaging() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethodForwardsNormalizedDateParameter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethodForwardsNormalizedDateParameter.cs new file mode 100644 index 00000000000..0c3cd5e4f00 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethodForwardsNormalizedDateParameter.cs @@ -0,0 +1,4 @@ +public virtual async global::System.Threading.Tasks.Task GetThingAsync(global::System.DateTimeOffset requestOn, global::System.Threading.CancellationToken cancellationToken = default) +{ + return await this.GetThingAsync(requestOn, cancellationToken.ToRequestOptions()).ConfigureAwait(false); +} 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 ab1998ca2ae..744850962ff 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,9 @@ public sealed class ParameterProvider : IEquatable public ParameterProvider(InputParameter inputParameter) { InputParameter = inputParameter; - Name = inputParameter.Name; + Name = !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 74a973a8761..248f522c13a 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..8d56efd8a5e 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 @@ -2,8 +2,10 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; +using Microsoft.TypeSpec.Generator.Input; namespace Microsoft.TypeSpec.Generator.Utilities { @@ -20,8 +22,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 +60,112 @@ 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 = DateTimeNameRules.ToVerbForm(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 StatusTimeStampName = "StatusTimeStamp"; + private const string StatusTimestampName = "StatusTimestamp"; + private const string TimeStampSuffix = "TimeStamp"; + private const string TimeSuffix = "Time"; + private const string TimestampSuffix = "Timestamp"; + private const string ToName = "To"; + + // Complete prefixes that read better as verbs when combined with the "On" suffix. + private static readonly Dictionary _nounToVerbMap = new(StringComparer.OrdinalIgnoreCase) + { + ["Creation"] = "Created", + ["Deletion"] = "Deleted", + ["Expiration"] = "Expire", + ["Modification"] = "Modified" + }; + + internal static string ToVerbForm(string prefix) + { + if (!_nounToVerbMap.TryGetValue(prefix, out var verb)) + { + return prefix; + } + + return char.IsLower(prefix[0]) + ? char.ToLowerInvariant(verb[0]) + verb[1..] + : verb; + } + + internal static bool HasExcludedComponent(string name) + { + // StatusTimestamp is a semantic compound. Keep the exclusion exact so names such as + // LastSyncTimestamp continue to normalize. + return name.StartsWith(FromName, StringComparison.OrdinalIgnoreCase) || + name.StartsWith(ToName, StringComparison.OrdinalIgnoreCase) || + name.EndsWith(PointInTimeName, StringComparison.OrdinalIgnoreCase) || + name.Equals(StatusTimestampName, StringComparison.OrdinalIgnoreCase) || + name.Equals(StatusTimeStampName, 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 f7b475fe14b..f6c12b047dd 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 && !parameter.IsContentParameter) { - 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/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index ec68d260f79..47acc281a79 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -621,6 +621,31 @@ public async Task BackCompatibility_MultipleParamNamesChanged() Assert.AreEqual("previousDictProp", docParams[3].Parameter.Name); } + [Test] + public async Task BackCompatibility_ModelFactoryParameterPreservesDateTimeSuffix() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var model = InputFactory.Model( + "DateTimeModel", + properties: [InputFactory.Property("StartTime", dateTime, isRequired: true)]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var method = modelFactory.Methods.Single(m => m.Signature.Name == "DateTimeModel"); + Assert.AreEqual("startTime", method.Signature.Parameters.Single().Name); + StringAssert.Contains("DateTimeModel(startTime", method.BodyStatements!.ToDisplayString()); + } + // Validates that when a new property is added AND the previous contract used different // names for some of the surviving parameters, the rename-only fast path does NOT apply // (parameter counts differ). Instead the standard "new property added" backcompat overload diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ModelFactoryParameterPreservesDateTimeSuffix/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ModelFactoryParameterPreservesDateTimeSuffix/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..9dfd511ce20 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ModelFactoryParameterPreservesDateTimeSuffix/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using System; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static DateTimeModel DateTimeModel(DateTimeOffset startTime = default) + { + return default; + } + } +} + +namespace Sample.Models +{ + public partial class DateTimeModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 66ce8b8954e..659f1625be9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -2804,6 +2804,58 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public void ConstructorParameterNormalizesDateTimeSuffix() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "DateTimeModel", + usage: InputModelTypeUsage.Input, + properties: [InputFactory.Property("StartTime", dateTime, isRequired: true)]); + + MockHelpers.LoadMockGenerator(inputModelTypes: [inputModel]); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.OfType().Single(); + var constructor = modelProvider.Constructors.Single(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + Assert.AreEqual("startOn", constructor.Signature.Parameters.Single().Name); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BackCompat_ConstructorParameterPreservesDateTimeSuffix() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "DateTimeModel", + usage: InputModelTypeUsage.Input, + properties: [InputFactory.Property("StartTime", dateTime, isRequired: true)]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.OfType().Single(); + modelProvider.ProcessTypeForBackCompatibility(); + + var constructor = modelProvider.Constructors.Single(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + Assert.AreEqual("startTime", constructor.Signature.Parameters.Single().Name); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public async Task BackCompat_ConstructorNotRestoredWhenLastContractMissing() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterPreservesDateTimeSuffix.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterPreservesDateTimeSuffix.cs new file mode 100644 index 00000000000..afe7711f25f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterPreservesDateTimeSuffix.cs @@ -0,0 +1,27 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class DateTimeModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public DateTimeModel(global::System.DateTimeOffset startTime) + { + StartOn = startTime; + } + + internal DateTimeModel(global::System.DateTimeOffset startTime, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartOn = startTime; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset StartOn { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterPreservesDateTimeSuffix/DateTimeModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterPreservesDateTimeSuffix/DateTimeModel.cs new file mode 100644 index 00000000000..7ce9bb2696e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterPreservesDateTimeSuffix/DateTimeModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace Sample.Models +{ + public partial class DateTimeModel + { + public DateTimeModel(DateTimeOffset startTime) + { + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/ConstructorParameterNormalizesDateTimeSuffix.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/ConstructorParameterNormalizesDateTimeSuffix.cs new file mode 100644 index 00000000000..223d9b6a5b5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/ConstructorParameterNormalizesDateTimeSuffix.cs @@ -0,0 +1,27 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class DateTimeModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public DateTimeModel(global::System.DateTimeOffset startOn) + { + StartOn = startOn; + } + + internal DateTimeModel(global::System.DateTimeOffset startOn, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartOn = startOn; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset StartOn { get; } + } +} 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 899f101c626..0646f57b13b 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); + } + [TestCase("IPv4Routes", "ipv4Routes")] [TestCase("iPv4Routes", "iPv4Routes")] [TestCase("regularName", "regularName")] @@ -84,6 +105,48 @@ 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"), + ("expirationDate", dateTime, "expireOn"), + ("creationDate", dateTime, "createdOn"), + ("creationTime", dateTime, "createdOn"), + ("expirationDateTime", dateTime.WithNullable(true), "expireOn"), + ("deletionDateTime", dateTime, "deletedOn"), + ("modificationTime", dateTime, "modifiedOn"), + ("accountExpirationDate", dateTime, "accountExpirationOn"), + ("statusTimestamp", dateTime.WithNullable(true), "statusTimestamp"), + ("lastSyncTimestamp", dateTime, "lastSyncOn"), + ("totalTime", dateTime, "totalTime"), + ("topicTimestamp", dateTime.WithNullable(true), "topicTimestamp"), + ("tokenExpirationDate", dateTime, "tokenExpirationDate"), + ("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 c2fbf2d328b..379b15849bf 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")] @@ -152,6 +173,32 @@ public void TestPropertyParameterDeclarationNormalizesAcronymCasing(string input Assert.AreEqual(expectedName, property.AsVariableExpression.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) { @@ -225,6 +272,41 @@ 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, "DeletedOn"); + yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModifiedOn"); + yield return new TestCaseData("Timestamp", dateTime, false, "On"); + yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpireOn"); + yield return new TestCaseData("CreationDate", dateTime, false, "CreatedOn"); + yield return new TestCaseData("CreationTime", dateTime, false, "CreatedOn"); + yield return new TestCaseData("ExpirationDateTime", dateTime.WithNullable(true), false, "ExpireOn"); + yield return new TestCaseData("DeletionDateTime", dateTime, false, "DeletedOn"); + yield return new TestCaseData("AccountExpirationDate", dateTime, false, "AccountExpirationOn"); + 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("StatusTimestamp", dateTime.WithNullable(true), false, "StatusTimestamp"); + yield return new TestCaseData("LastSyncTimestamp", dateTime, false, "LastSyncOn"); + yield return new TestCaseData("TotalTime", dateTime, false, "TotalTime"); + yield return new TestCaseData("TopicTimestamp", dateTime.WithNullable(true), false, "TopicTimestamp"); + 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..d184095d067 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,63 @@ 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("CreationTime", dateTime, "CreatedOn"); + yield return new TestCaseData("creationTime", dateTime, "createdOn"); + yield return new TestCaseData("ExpirationDateTime", dateTime, "ExpireOn"); + yield return new TestCaseData("expirationDateTime", dateTime, "expireOn"); + yield return new TestCaseData("AccountExpirationDate", dateTime, "AccountExpirationOn"); + yield return new TestCaseData("accountExpirationDate", dateTime, "accountExpirationOn"); + yield return new TestCaseData("RecreationTime", dateTime, "RecreationOn"); + yield return new TestCaseData("recreationTime", dateTime, "recreationOn"); + yield return new TestCaseData("TotalTime", dateTime, "TotalTime"); + yield return new TestCaseData("totalTime", dateTime, "totalTime"); + yield return new TestCaseData("TopicTimestamp", dateTime, "TopicTimestamp"); + yield return new TestCaseData("topicTimestamp", dateTime, "topicTimestamp"); + yield return new TestCaseData("TokenExpirationDate", dateTime, "TokenExpirationDate"); + yield return new TestCaseData("tokenExpirationDate", dateTime, "tokenExpirationDate"); + yield return new TestCaseData("FromageTime", dateTime, "FromageTime"); + yield return new TestCaseData("fromageTime", dateTime, "fromageTime"); + yield return new TestCaseData("StatusTimestamp", dateTime, "StatusTimestamp"); + yield return new TestCaseData("statusTimestamp", dateTime, "statusTimestamp"); + yield return new TestCaseData("StatusTimeStamp", dateTime, "StatusTimeStamp"); + yield return new TestCaseData("statusTimeStamp", dateTime, "statusTimeStamp"); + yield return new TestCaseData("LastSyncTimestamp", dateTime, "LastSyncOn"); + yield return new TestCaseData("lastSyncTimestamp", dateTime, "lastSyncOn"); + 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,