Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(On))
if (global::Sample.Optional.IsDefined(Timestamp))
{
writer.WriteStartElement("timestamp");
writer.WriteStringValue(On.Value, "O");
writer.WriteStringValue(Timestamp.Value, "O");
writer.WriteEndElement();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public void XmlDeserializationHandlesDateTimeOffsetProperty()
Assert.IsNotNull(xmlDeserializationMethod);
var methodBody = xmlDeserializationMethod!.BodyStatements!.ToDisplayString();

Assert.IsTrue(methodBody.Contains("@on = child.GetDateTimeOffset(\"O\")"),
Assert.IsTrue(methodBody.Contains("timestamp = child.GetDateTimeOffset(\"O\")"),
$"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public void XmlSerializationHandlesDateTimeOffsetProperty()
Assert.IsNotNull(xmlSerializationMethod);
var methodBody = xmlSerializationMethod!.BodyStatements!.ToDisplayString();

Assert.IsTrue(methodBody.Contains("writer.WriteStringValue(On.Value, \"O\")"),
Assert.IsTrue(methodBody.Contains("writer.WriteStringValue(Timestamp.Value, \"O\")"),
$"DateTimeOffset property should be serialized with WriteStringValue. Actual:\n{methodBody}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu
_generatedTypeProvider = generatedTypeProvider;
var inputModel = inputType as InputModelType;
_specProperties = inputModel?.Properties ?? [];
_specPropertiesMap = _specProperties.ToDictionary(p => p.IsExactName ? p.Name : p.Name.ToIdentifierName(), p => p);
_specPropertiesMap = [];
foreach (var property in _specProperties)
{
var name = property.IsExactName ? property.Name : property.Name.ToIdentifierName();
_specPropertiesMap.TryAdd(name, property);
if (!property.IsExactName)
{
_specPropertiesMap.TryAdd(name.NormalizeCSharpAcronyms(property.Type.IsDateTimeInputType()), property);
}
}
_serializedNameMap = BuildSerializationNameMap();
_renamedProperties = (_generatedTypeProvider.CustomCodeView?.Properties ?? [])
.Where(p => p.OriginalName != null).Select(p => p.OriginalName!).ToHashSet();
Expand Down Expand Up @@ -179,7 +188,7 @@ protected internal override PropertyProvider[] BuildProperties()
{
// Check if custom property is in spec
if (_specPropertiesMap.TryGetValue(prop.Name, out var specProp) ||
(prop.OriginalName != null && _specPropertiesMap.TryGetValue(prop.OriginalName, out specProp)))
(prop.OriginalName != null && TryGetSpecProperty(prop.OriginalName, out specProp)))
{
inputProperties.Add(specProp);
}
Expand Down Expand Up @@ -281,7 +290,7 @@ private bool TryGetSpecProperty(
PropertyProvider customProperty,
[NotNullWhen(true)] out InputModelProperty? candidateSpecProperty)
{
if (customProperty.OriginalName != null && _specPropertiesMap.TryGetValue(customProperty.OriginalName, out candidateSpecProperty))
if (customProperty.OriginalName != null && TryGetSpecProperty(customProperty.OriginalName, out candidateSpecProperty))
{
return true;
}
Expand All @@ -299,7 +308,7 @@ private bool TryGetSpecProperty(

private bool TryGetSpecProperty(FieldProvider customField, [NotNullWhen(true)] out InputModelProperty? candidateSpecProperty)
{
if (customField.OriginalName != null && _specPropertiesMap.TryGetValue(customField.OriginalName, out candidateSpecProperty))
if (customField.OriginalName != null && TryGetSpecProperty(customField.OriginalName, out candidateSpecProperty))
{
return true;
}
Expand All @@ -314,6 +323,16 @@ private bool TryGetSpecProperty(FieldProvider customField, [NotNullWhen(true)] o
return false;
}

private bool TryGetSpecProperty(string name, [NotNullWhen(true)] out InputModelProperty? candidateSpecProperty)
{
if (_specPropertiesMap.TryGetValue(name, out candidateSpecProperty))
{
return true;
}

return _specPropertiesMap.TryGetValue(name.ToIdentifierName(), out candidateSpecProperty);
}

private Dictionary<string, string?> BuildSerializationNameMap()
{
var serializedNameMapping = new Dictionary<string, string?>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,25 +435,18 @@ protected static bool IsResolvableBuildableType(CSharpType type) =>

internal PropertyProvider[] FilterCustomizedProperties(IEnumerable<PropertyProvider> specProperties)
{
var specPropertiesByName = BuildSpecPropertiesByName(specProperties);
var properties = new List<PropertyProvider>();
var customProperties = new HashSet<string>();

foreach (var customProperty in BuildAllCustomProperties())
{
customProperties.Add(customProperty.Name);
if (customProperty.OriginalName != null)
{
customProperties.Add(customProperty.OriginalName);
}
AddCustomName(customProperties, customProperty.Name, customProperty.OriginalName, specPropertiesByName);
}

foreach (var customField in BuildAllCustomFields())
{
customProperties.Add(customField.Name);
if (customField.OriginalName != null)
{
customProperties.Add(customField.OriginalName);
}
AddCustomName(customProperties, customField.Name, customField.OriginalName, specPropertiesByName);
}

foreach (var property in specProperties)
Expand All @@ -467,6 +460,48 @@ internal PropertyProvider[] FilterCustomizedProperties(IEnumerable<PropertyProvi
return [.. properties];
}

private static void AddCustomName(
HashSet<string> customNames,
string name,
string? originalName,
IReadOnlyDictionary<string, InputProperty> specPropertiesByName)
{
customNames.Add(name);
if (originalName is null)
{
return;
}

customNames.Add(originalName);
if (specPropertiesByName.TryGetValue(originalName, out var inputProperty) && !inputProperty.IsExactName)
{
customNames.Add(
originalName
.ToIdentifierName()
.NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType()));
}
}

private static IReadOnlyDictionary<string, InputProperty> BuildSpecPropertiesByName(IEnumerable<PropertyProvider> specProperties)
{
var specPropertiesByName = new Dictionary<string, InputProperty>(StringComparer.Ordinal);

foreach (var specProperty in specProperties)
{
var inputProperty = specProperty.InputProperty;
if (inputProperty is null)
{
continue;
}

var identifierName = inputProperty.Name.ToIdentifierName();
specPropertiesByName.TryAdd(inputProperty.Name, inputProperty);
specPropertiesByName.TryAdd(identifierName, inputProperty);
}

return specPropertiesByName;
}

internal FieldProvider[] FilterCustomizedFields(IEnumerable<FieldProvider> specFields)
{
var fields = new List<FieldProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static string NormalizeDateTimeSuffix(this string name)
}

var suffixLength = DateTimeNameRules.GetSuffixLength(name);
if (suffixLength == 0)
if (suffixLength == 0 || suffixLength == name.Length)
{
return name;
}
Expand Down Expand Up @@ -101,20 +101,30 @@ private static class DateTimeNameRules
{
["Creation"] = "Created",
["Deletion"] = "Deleted",
["Expiration"] = "Expire",
["Expiration"] = "Expires",
["Modification"] = "Modified"
};

internal static string ToVerbForm(string prefix)
{
if (!_nounToVerbMap.TryGetValue(prefix, out var verb))
if (_nounToVerbMap.TryGetValue(prefix, out var verb))
{
return prefix;
return char.IsLower(prefix[0])
? char.ToLowerInvariant(verb[0]) + verb[1..]
: verb;
}

return char.IsLower(prefix[0])
? char.ToLowerInvariant(verb[0]) + verb[1..]
: verb;
foreach (var (noun, compoundVerb) in _nounToVerbMap)
{
if (prefix.Length > noun.Length &&
prefix.EndsWith(noun, StringComparison.OrdinalIgnoreCase) &&
char.IsUpper(prefix[^noun.Length]))
{
return prefix[..^noun.Length] + compoundVerb;
}
}

return prefix;
}

internal static bool HasExcludedComponent(string name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,111 @@ public async Task CustomCodeWinsOverIsExactName()
Assert.AreEqual("AccessToken", modelTypeProvider.CanonicalView.Properties[0].Name);
}

[Test]
public async Task CustomCodeReplacesDateNormalizedProperty()
{
var dateTime = new InputDateTimeType(
DateTimeKnownEncoding.Rfc3339,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);
var inputModel = InputFactory.Model(
"mockInputModel",
properties: [InputFactory.Property("creationDate", dateTime, isRequired: true)]);
Comment thread
jorgerangel-msft marked this conversation as resolved.

var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel");

Assert.AreEqual(1, modelTypeProvider.CustomCodeView!.Properties.Count);
Assert.AreEqual("Created", modelTypeProvider.CustomCodeView.Properties[0].Name);
Assert.AreEqual(0, modelTypeProvider.Properties.Count);
Assert.AreEqual(1, modelTypeProvider.CanonicalView!.Properties.Count);
Assert.AreEqual("Created", modelTypeProvider.CanonicalView.Properties[0].Name);
Comment thread
jorgerangel-msft marked this conversation as resolved.
Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content);
}

[Test]
public async Task CustomCodeReplacesDateNormalizedPropertyUsingRawSpecName()
{
var dateTime = new InputDateTimeType(
DateTimeKnownEncoding.Rfc3339,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);
var inputModel = InputFactory.Model(
"mockInputModel",
properties: [InputFactory.Property("creationDate", dateTime, isRequired: true)]);

var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel");

Assert.AreEqual(1, modelTypeProvider.CustomCodeView!.Properties.Count);
Assert.AreEqual("Created", modelTypeProvider.CustomCodeView.Properties[0].Name);
Assert.AreEqual(0, modelTypeProvider.Properties.Count);
Assert.AreEqual(1, modelTypeProvider.CanonicalView!.Properties.Count);
Assert.AreEqual("Created", modelTypeProvider.CanonicalView.Properties[0].Name);
Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content);
}

[Test]
public async Task CustomCodeReplacesAcronymNormalizedProperty()
{
var inputModel = InputFactory.Model(
"mockInputModel",
properties: [InputFactory.Property("ipFoo", InputPrimitiveType.String, isRequired: true)]);
Comment thread
jorgerangel-msft marked this conversation as resolved.

var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel");

Assert.AreEqual(1, modelTypeProvider.CustomCodeView!.Properties.Count);
Assert.AreEqual("Foo", modelTypeProvider.CustomCodeView.Properties[0].Name);
Assert.AreEqual(0, modelTypeProvider.Properties.Count);
Assert.AreEqual(1, modelTypeProvider.CanonicalView!.Properties.Count);
Assert.AreEqual("Foo", modelTypeProvider.CanonicalView.Properties[0].Name);
Comment thread
jorgerangel-msft marked this conversation as resolved.
Assert.IsTrue(modelTypeProvider.CanonicalView.Properties[0].WireInfo!.IsRequired);
Assert.IsFalse(modelTypeProvider.CanonicalView.Properties[0].WireInfo!.IsReadOnly);
Assert.IsTrue(modelTypeProvider.CanonicalView.Properties[0].Body.HasSetter);
CollectionAssert.AreEqual(
new[] { "foo" },
modelTypeProvider.Constructors.Single(c => c.Signature.Modifiers == MethodSignatureModifiers.Public).Signature.Parameters.Select(p => p.Name));
Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content);
}

[Test]
public async Task ExactNameCodeGenMemberReplacesProperty()
{
var dateTime = new InputDateTimeType(
DateTimeKnownEncoding.Rfc3339,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);
var inputModel = InputFactory.Model(
"mockInputModel",
properties: [InputFactory.Property("CreatedFoo", dateTime, isRequired: true, isExactName: true)]);

var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel");

Assert.AreEqual(1, modelTypeProvider.CustomCodeView!.Properties.Count);
Assert.AreEqual("Created", modelTypeProvider.CustomCodeView.Properties[0].Name);
Assert.AreEqual(0, modelTypeProvider.Properties.Count);
Assert.AreEqual(1, modelTypeProvider.CanonicalView!.Properties.Count);
Assert.AreEqual("Created", modelTypeProvider.CanonicalView.Properties[0].Name);
Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content);
}

[Test]
public async Task CustomCodeWinsOverIsExactNameOnModel()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// <auto-generated/>

#nullable disable

using System;
using System.Collections.Generic;
using Sample;

namespace Sample.Models
{
public partial class MockInputModel
{
private protected readonly global::System.Collections.Generic.IDictionary<string, global::System.BinaryData> _additionalBinaryDataProperties;

public MockInputModel(string foo)
{
global::Sample.Argument.AssertNotNull(foo, nameof(foo));

Foo = foo;
}

internal MockInputModel(string foo, global::System.Collections.Generic.IDictionary<string, global::System.BinaryData> additionalBinaryDataProperties)
{
Foo = foo;
_additionalBinaryDataProperties = additionalBinaryDataProperties;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable disable

using Sample;
using SampleTypeSpec;
using Microsoft.TypeSpec.Generator.Customizations;

namespace Sample.Models
{
public partial class MockInputModel
{
[CodeGenMember("IPFoo")]
public string Foo { get; set; }
}
}
Loading