Skip to content

Commit

Permalink
Merge pull request #528 from juwens/feature/property-name-by-member-info
Browse files Browse the repository at this point in the history
added ability to use a custom name resolving logic PropertyNameResolvingMethods so users can provide their custom …
  • Loading branch information
gregsdennis authored Oct 11, 2023
2 parents e444582 + b64f8ef commit 5cd7bd1
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 16 deletions.
58 changes: 58 additions & 0 deletions JsonSchema.Generation.Tests/PropertyNameResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using NUnit.Framework;

using static Json.Schema.Generation.Tests.AssertionExtensions;

namespace Json.Schema.Generation.Tests;

public class PropertyNameResolverTests
{
class Target
{
[JsonPropertyName("JsonName")]
public string PropertyThatNeeds_Changing { get; set; }
}

class TargetWithOutJsonPropertyName
{
public string PropertyThatNeeds_Changing { get; set; }
}

public static IEnumerable<TestCaseData> TestCases
{
get
{
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.AsDeclared, "PropertyThatNeeds_Changing");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.CamelCase, "propertyThatNeedsChanging");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.PascalCase, "PropertyThatNeedsChanging");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.KebabCase, "property-that-needs-changing");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.UpperKebabCase, "PROPERTY-THAT-NEEDS-CHANGING");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.SnakeCase, "property_that_needs_changing");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.UpperSnakeCase, "PROPERTY_THAT_NEEDS_CHANGING");
yield return new TestCaseData(typeof(Target), PropertyNameResolvers.ByJsonPropertyName, "JsonName");
yield return new TestCaseData(typeof(Target), new PropertyNameResolver(static x => "CustomName"), "CustomName");
yield return new TestCaseData(typeof(TargetWithOutJsonPropertyName), PropertyNameResolvers.ByJsonPropertyName, "PropertyThatNeeds_Changing");
}
}

[TestCaseSource(nameof(TestCases))]
public void VerifyNameChanges(Type type, PropertyNameResolver resolver, string expectedName)
{
var config = new SchemaGeneratorConfiguration
{
PropertyNameResolver = resolver
};
var expected = new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
(expectedName, new JsonSchemaBuilder().Type(SchemaValueType.String))
)
.Build();

var actual = new JsonSchemaBuilder().FromType(type, config).Build();

AssertEqual(expected, actual);
}
}
22 changes: 10 additions & 12 deletions JsonSchema.Generation/Generators/ObjectSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,8 @@ public void AddConstraints(SchemaGenerationContextBase context)

var memberContext = SchemaGenerationContextCache.Get(member.GetMemberType(), unconditionalAttributes);

var name = SchemaGeneratorConfiguration.Current.PropertyNamingMethod(member.Name);
var nameAttribute = unconditionalAttributes.OfType<JsonPropertyNameAttribute>().FirstOrDefault();
if (nameAttribute != null)
name = nameAttribute.Name;
var name = SchemaGeneratorConfiguration.Current.PropertyNameResolver(member);
name ??= member.Name;

if (unconditionalAttributes.OfType<ObsoleteAttribute>().Any())
{
Expand Down Expand Up @@ -113,7 +111,7 @@ public void AddConstraints(SchemaGenerationContextBase context)
var conditionGroups = context.Type.GetCustomAttributes()
.OfType<IConditionAttribute>()
.SelectMany(x => ExpandEnumConditions(x, membersToGenerate))
.GroupBy(x => x.ConditionGroup)
.GroupBy(x => x.Attribute.ConditionGroup)
.ToList();

if (!conditionGroups.Any()) return;
Expand Down Expand Up @@ -164,7 +162,7 @@ public void AddConstraints(SchemaGenerationContextBase context)
context.Intents.Add(new UnevaluatedPropertiesIntent());
}

private static IEnumerable<IConditionAttribute> ExpandEnumConditions(IConditionAttribute condition, IEnumerable<MemberInfo> members)
private static IEnumerable<(IConditionAttribute Attribute, MemberInfo Member)> ExpandEnumConditions(IConditionAttribute condition, IEnumerable<MemberInfo> members)
{
var member = members.FirstOrDefault(x => x.Name == condition.PropertyName);
if (member == null) yield break;
Expand All @@ -184,24 +182,24 @@ private static IEnumerable<IConditionAttribute> ExpandEnumConditions(IConditionA
var values = Enum.GetValues(memberType);
foreach (var value in values)
{
yield return new IfAttribute(ifEnumAttribute.PropertyName, ifEnumAttribute.UseNumbers ? value : value.ToString(), value);
yield return (new IfAttribute(ifEnumAttribute.PropertyName, ifEnumAttribute.UseNumbers ? value : value.ToString(), value), member);
}

yield break;
}
yield return condition;
yield return (condition, member);
}

private static IfIntent GenerateIf(IEnumerable<IConditionAttribute> conditions)
private static IfIntent GenerateIf(IEnumerable<(IConditionAttribute Attribute, MemberInfo Member)> conditions)
{
var properties = new Dictionary<string, SchemaGenerationContextBase>();
var required = new List<string>();
foreach (var condition in conditions)
{
var name = SchemaGeneratorConfiguration.Current.PropertyNamingMethod(condition.PropertyName);
var name = SchemaGeneratorConfiguration.Current.PropertyNameResolver(condition.Member);
if (!properties.TryGetValue(name, out var context))

Check warning on line 200 in JsonSchema.Generation/Generators/ObjectSchemaGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, SchemaGenerationContextBase>.TryGetValue(string key, out SchemaGenerationContextBase value)'.

Check warning on line 200 in JsonSchema.Generation/Generators/ObjectSchemaGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, SchemaGenerationContextBase>.TryGetValue(string key, out SchemaGenerationContextBase value)'.
properties[name] = context = new AdHocGenerationContext();
switch (condition)
switch (condition.Attribute)
{
case IfAttribute ifAtt:
context.Intents.Add(new ConstIntent(ifAtt.Value));
Expand Down Expand Up @@ -240,7 +238,7 @@ private static IfIntent GenerateIf(IEnumerable<IConditionAttribute> conditions)
var properties = prebuiltMemberContexts;
foreach (var consequence in applicable.GroupBy(x => x.member))
{
var name = SchemaGeneratorConfiguration.Current.PropertyNamingMethod(consequence.Key.Name);
var name = SchemaGeneratorConfiguration.Current.PropertyNameResolver(consequence.Key);
if (properties.TryGetValue(name, out var localContext))

Check warning on line 242 in JsonSchema.Generation/Generators/ObjectSchemaGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, SchemaGenerationContextBase>.TryGetValue(string key, out SchemaGenerationContextBase value)'.

Check warning on line 242 in JsonSchema.Generation/Generators/ObjectSchemaGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, SchemaGenerationContextBase>.TryGetValue(string key, out SchemaGenerationContextBase value)'.
localContext = new MemberGenerationContext(localContext, new List<Attribute>());
else
Expand Down
56 changes: 56 additions & 0 deletions JsonSchema.Generation/PropertyNameResolvers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
using Humanizer;

namespace Json.Schema.Generation;

/// <summary>
/// Declares a property name resolution which is used to provide a property name.
/// </summary>
/// <param name="input">The property.</param>
/// <returns>The property name</returns>
public delegate string? PropertyNameResolver(MemberInfo input);

/// <summary>
/// Defines a set of predefined property name resolution methods.
/// </summary>
public static class PropertyNameResolvers
{
/// <summary>
/// Makes no changes. Properties are generated with the name of the property in code.
/// </summary>
public static readonly PropertyNameResolver AsDeclared = x => x.Name;
/// <summary>
/// Property names to camel case (e.g. `camelCase`).
/// </summary>
public static readonly PropertyNameResolver CamelCase = x => x.Name.Camelize();
/// <summary>
/// Property names to pascal case (e.g. `PascalCase`).
/// </summary>
public static readonly PropertyNameResolver PascalCase = x => x.Name.Pascalize();
/// <summary>
/// Property names to snake case (e.g. `Snake_Case`).
/// </summary>
public static readonly PropertyNameResolver SnakeCase = x => x.Name.Underscore();
/// <summary>
/// Property names to lower snake case (e.g. `lower_snake_case`).
/// </summary>
public static readonly PropertyNameResolver LowerSnakeCase = x => x.Name.Underscore().ToLowerInvariant();
/// <summary>
/// Property names to upper snake case (e.g. `UPPER_SNAKE_CASE`).
/// </summary>
public static readonly PropertyNameResolver UpperSnakeCase = x => x.Name.Underscore().ToUpperInvariant();
/// <summary>
/// Property names to kebab case (e.g. `Kebab-Case`).
/// </summary>
public static readonly PropertyNameResolver KebabCase = x => x.Name.Kebaberize();
/// <summary>
/// Property names to upper kebab case (e.g. `UPPER-KEBAB-CASE`).
/// </summary>
public static readonly PropertyNameResolver UpperKebabCase = x => x.Name.Kebaberize().ToUpperInvariant();
/// <summary>
/// Property name is read from <see cref="JsonPropertyNameAttribute"/>.
/// </summary>
public static readonly PropertyNameResolver ByJsonPropertyName = x => x.GetCustomAttributes<JsonPropertyNameAttribute>().FirstOrDefault()?.Name /* TODO: what do we do with the nullable here? */;
}
4 changes: 3 additions & 1 deletion JsonSchema.Generation/PropertyNamingMethods.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Humanizer;
using System;
using Humanizer;

namespace Json.Schema.Generation;

Expand All @@ -12,6 +13,7 @@ namespace Json.Schema.Generation;
/// <summary>
/// Defines a set of predefined property naming methods.
/// </summary>
[Obsolete($"Use {nameof(PropertyNameResolvers)} instead.")]
public static class PropertyNamingMethods
{
/// <summary>
Expand Down
39 changes: 36 additions & 3 deletions JsonSchema.Generation/SchemaGeneratorConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;
using Json.Schema.Generation.Generators;

Expand All @@ -11,7 +12,6 @@ namespace Json.Schema.Generation;
public class SchemaGeneratorConfiguration
{
private PropertyNamingMethod? _propertyNamingMethod;

/// <summary>
/// A collection of refiners.
/// </summary>
Expand All @@ -34,11 +34,21 @@ public class SchemaGeneratorConfiguration
/// <remarks>
/// This can be replaced with any `Func&lt;string, string&gt;`.
/// </remarks>
public PropertyNamingMethod PropertyNamingMethod
[Obsolete($"Use {nameof(PropertyNameResolver)} instead.")]
public PropertyNamingMethod? PropertyNamingMethod
{
get => _propertyNamingMethod ??= PropertyNamingMethods.AsDeclared;
get => x => PropertyNameResolver(new DummyInfo(x));

Check warning on line 40 in JsonSchema.Generation/SchemaGeneratorConfiguration.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 40 in JsonSchema.Generation/SchemaGeneratorConfiguration.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
set => _propertyNamingMethod = value;
}

/// <summary>
/// Gets or sets the property name resolving method. Default is <see cref="PropertyNameResolvers.ByJsonPropertyName"/>.
/// </summary>
/// <remarks>
/// This can be replaced with any `Func&lt;MemberInfo, string&gt;`.
/// </remarks>
public PropertyNameResolver PropertyNameResolver { get; set; } = PropertyNameResolvers.AsDeclared;

/// <summary>
/// Gets or sets whether to include `null` in the `type` keyword.
/// Default is <see cref="Nullability.Disabled"/> which means that it will
Expand Down Expand Up @@ -69,4 +79,27 @@ public PropertyNamingMethod PropertyNamingMethod
[field: ThreadStatic]
public static SchemaGeneratorConfiguration Current { get; internal set; }
#pragma warning restore CS8618

/// <summary>
/// A shim while <see cref="PropertyNamingMethod"/> is not yet removed.
/// Makes it Possible to call <see cref="PropertyNameResolver"/> from <see cref="PropertyNamingMethod"/>.
/// </summary>
private sealed class DummyInfo : MemberInfo
{
public override object[] GetCustomAttributes(bool inherit) => Array.Empty<object>();

public override object[] GetCustomAttributes(Type attributeType, bool inherit) => Array.Empty<object>();

public override bool IsDefined(Type attributeType, bool inherit) => false;

public override Type DeclaringType { get; } = typeof(DummyInfo);
public override MemberTypes MemberType { get; } = MemberTypes.Property;
public override string Name { get; }
public override Type? ReflectedType { get; } = null;

public DummyInfo(string name)
{
Name = name;
}
}
}

0 comments on commit 5cd7bd1

Please sign in to comment.