Skip to content

Commit

Permalink
[http-client-csharp] Fix: allow suppressing & replacing fields (#4795)
Browse files Browse the repository at this point in the history
This PR enables the suppression of fields via the `CodeGenSuppress`
attribute, and enables the replacement of fields via custom code.

fixes: #4792
  • Loading branch information
jorgerangel-msft authored Oct 21, 2024
1 parent 48d7bc0 commit a8c67ff
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ protected override MethodProvider[] BuildMethods()
// in general, this loop builds up if statements for each value, it looks like:
// if (<condition>) { return EnumType.TheValue; }
// the condition could be different depending on the type of the underlying value type of the enum
for (int i = 0; i < _enumProvider.Fields.Count; i++)
for (int i = 0; i < _enumProvider.EnumValues.Count; i++)
{
var enumField = _enumProvider.Fields[i];
var enumValue = _enumProvider.EnumValues[i];
ScopedApi<bool> condition;
if (_enumProvider.EnumUnderlyingType.Equals(typeof(string)))
Expand All @@ -121,7 +120,7 @@ protected override MethodProvider[] BuildMethods()
}
deserializationBody.Add(new IfStatement(condition)
{
Return(new MemberExpression(_enumProvider.Type, enumField.Name))
Return(new MemberExpression(_enumProvider.Type, enumValue.Name))
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,5 +292,32 @@ public async Task CanRenameSubClient()
var subClientFactoryMethod = parentClientProvider!.Methods.SingleOrDefault(m => m.Signature.Name == "GetCustomClient");
Assert.IsNotNull(subClientFactoryMethod);
}

// Validates that the sub-client caching field is removed when the field is suppressed.
[Test]
public async Task CanRemoveCachingField()
{
var inputOperation = InputFactory.Operation("HelloAgain", parameters:
[
InputFactory.Parameter("p1", InputFactory.Array(InputPrimitiveType.String))
]);
var inputClient = InputFactory.Client("TestClient", operations: [inputOperation]);
InputClient subClient = InputFactory.Client("dog", [], [], inputClient.Name);
var plugin = await MockHelpers.LoadMockPluginAsync(
clients: () => [inputClient, subClient],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// find the parent client provider
var parentClientProvider = plugin.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ClientProvider && t.Name == "TestClient");
Assert.IsNotNull(parentClientProvider);

// the sub-client caching field should not be present
var fields = parentClientProvider!.Fields;
Assert.AreEqual(1, fields.Count);
Assert.AreEqual("_endpoint", fields[0].Name);

var cachingField = fields.SingleOrDefault(f => f.Name == "_cachedDog");
Assert.IsNull(cachingField);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

using System;
using Microsoft.Generator.CSharp.Customization;

namespace Sample
{
[CodeGenSuppress("_cachedDog")]
public partial class TestClient
{

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu
protected override TypeSignatureModifiers GetDeclarationModifiers() => _generatedTypeProvider.DeclarationModifiers;

private protected override PropertyProvider[] FilterCustomizedProperties(PropertyProvider[] canonicalProperties) => canonicalProperties;
private protected override FieldProvider[] FilterCustomizedFields(FieldProvider[] canonicalFields) => canonicalFields;

private protected override CanonicalTypeProvider GetCanonicalView() => this;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ private TypeSignatureModifiers GetDeclarationModifiersInternal()
public IReadOnlyList<ConstructorProvider> Constructors => _constructors ??= BuildConstructorsInternal();

private IReadOnlyList<FieldProvider>? _fields;
public IReadOnlyList<FieldProvider> Fields => _fields ??= BuildFields();
public IReadOnlyList<FieldProvider> Fields => _fields ??= FilterCustomizedFields(BuildFields());

private IReadOnlyList<TypeProvider>? _nestedTypes;
public IReadOnlyList<TypeProvider> NestedTypes => _nestedTypes ??= BuildNestedTypes();
Expand Down Expand Up @@ -240,6 +240,31 @@ private protected virtual PropertyProvider[] FilterCustomizedProperties(Property
return [..properties];
}

private protected virtual FieldProvider[] FilterCustomizedFields(FieldProvider[] specFields)
{
var fields = new List<FieldProvider>();
var customFields = new HashSet<string>();

foreach (var customField in GetAllCustomFields())
{
customFields.Add(customField.Name);
if (customField.OriginalName != null)
{
customFields.Add(customField.OriginalName);
}
}

foreach (var field in specFields)
{
if (ShouldGenerate(field, customFields))
{
fields.Add(field);
}
}

return [.. fields];
}

private MethodProvider[] BuildMethodsInternal()
{
var methods = new List<MethodProvider>();
Expand Down Expand Up @@ -423,13 +448,33 @@ private bool ShouldGenerate(PropertyProvider property, HashSet<string> customPro
return !customProperties.Contains(property.Name);
}

private bool ShouldGenerate(FieldProvider field, HashSet<string> customFields)
{
foreach (var attribute in GetMemberSuppressionAttributes())
{
if (IsMatch(field, attribute))
{
return false;
}
}

return !customFields.Contains(field.Name);
}

private static bool IsMatch(PropertyProvider propertyProvider, AttributeData attribute)
{
ValidateArguments(propertyProvider.EnclosingType, attribute);
var name = attribute.ConstructorArguments[0].Value as string;
return name == propertyProvider.Name;
}

private static bool IsMatch(FieldProvider fieldProvider, AttributeData attribute)
{
ValidateArguments(fieldProvider.EnclosingType, attribute);
var name = attribute.ConstructorArguments[0].Value as string;
return name == fieldProvider.Name;
}

private static bool IsMatch(TypeProvider enclosingType, MethodSignatureBase signature, AttributeData attribute)
{
ValidateArguments(enclosingType, attribute);
Expand Down Expand Up @@ -501,13 +546,13 @@ private static void ValidateArguments(TypeProvider type, AttributeData attribute
var arguments = attributeData.ConstructorArguments;
if (arguments.Length == 0)
{
throw new InvalidOperationException($"CodeGenSuppress attribute on {type.Name} must specify a method, constructor, or property name as its first argument.");
throw new InvalidOperationException($"CodeGenSuppress attribute on {type.Name} must specify a method, constructor, field, or property name as its first argument.");
}

if (arguments[0].Kind != TypedConstantKind.Primitive || arguments[0].Value is not string)
{
var attribute = GetText(attributeData.ApplicationSyntaxReference);
throw new InvalidOperationException($"{attribute} attribute on {type.Name} must specify a method, constructor, or property name as its first argument.");
throw new InvalidOperationException($"{attribute} attribute on {type.Name} must specify a method, constructor, field, or property name as its first argument.");
}

if (arguments.Length == 2 && arguments[1].Kind == TypedConstantKind.Array)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ public async Task CanRemoveProperty()
Assert.AreEqual(0, plugin.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel").Properties.Count);
}

[Test]
public async Task CanRemoveField()
{
var plugin = await MockHelpers.LoadMockPluginAsync(
inputModelTypes: [InputFactory.Model("mockInputModel", properties: [])],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
var csharpGen = new CSharpGen();
await csharpGen.ExecuteAsync();
Assert.AreEqual(0, plugin.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel").Fields.Count);
}

[Test]
public async Task CanChangeEnumMemberName()
{
Expand All @@ -342,12 +353,12 @@ await MockHelpers.LoadMockPluginAsync(
Assert.IsNotNull(enumProvider);

// validate the enum provider uses the custom member name
Assert.AreEqual(3, enumProvider!.Fields.Count);
Assert.AreEqual("Red", enumProvider.Fields[0].Name);
Assert.AreEqual("Green", enumProvider.Fields[1].Name);
Assert.AreEqual("SkyBlue", enumProvider.Fields[2].Name);
Assert.AreEqual(3, enumProvider!.EnumValues.Count);
Assert.AreEqual("Red", enumProvider.EnumValues[0].Name);
Assert.AreEqual("Green", enumProvider.EnumValues[1].Name);
Assert.AreEqual("SkyBlue", enumProvider.EnumValues[2].Name);

// the members should also be added to the custom code view
// the members should be added to the custom code view with the custom member names
Assert.IsNotNull(customCodeView);
Assert.AreEqual(3, customCodeView?.Fields.Count);
Assert.AreEqual("Red", customCodeView?.Fields[0].Name);
Expand Down Expand Up @@ -480,6 +491,26 @@ public async Task CanReplaceConstructor()
Assert.IsEmpty(plugin.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel").Constructors);
}

[Test]
public async Task CanReplaceField()
{
var plugin = await MockHelpers.LoadMockPluginAsync(
inputModelTypes: new[] {
InputFactory.Model(
"mockInputModel",
// use Input so that we generate a public ctor
usage: InputModelTypeUsage.Input,
properties: []),
},
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
var csharpGen = new CSharpGen();

await csharpGen.ExecuteAsync();

// The generated code should not contain any fields
Assert.IsEmpty(plugin.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel").Fields);
}

[Test]
public async Task DoesNotReplaceDefaultConstructorIfNotCustomized()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Generator.CSharp.Customization;

namespace Sample.Models;

[CodeGenSuppress("_additionalBinaryDataProperties")]
public partial class MockInputModel
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable disable

using Sample;
using System.ClientModel;
using System.ClientModel.Primitives;
using Microsoft.Generator.CSharp.Customization;

namespace Sample.Models
{
public partial class MockInputModel
{
private BinaryData _additionalBinaryDataProperties;
}
}

0 comments on commit a8c67ff

Please sign in to comment.