Skip to content

Commit

Permalink
Merge pull request #545 from gregsdennis/pointer/expression-last-and-…
Browse files Browse the repository at this point in the history
…naming-options

Extended JSON Pointer support
  • Loading branch information
gregsdennis authored Oct 28, 2023
2 parents 98cb6c4 + 7850928 commit 1ae3cd1
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 17 deletions.
24 changes: 24 additions & 0 deletions JsonPatch.Tests/GithubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,28 @@ public void Issue397_ReplaceShouldThrowForMissingValue()
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
Assert.IsNotNull(patchConfig.Apply(singleObject).Error);
}

public class Target543
{
public List<int> Values { get; set; }
}

[Test]
public void Issue543_CreatePatchToAddItem()
{
var targetObj = new Target543 { Values = new List<int> { 1, 2, 3, 4 } };
var target = JsonSerializer.SerializeToNode(targetObj);

var jsonPointer = JsonPointer.Create<Target543>(x => x.Values.Last());
var jsonPatch = new JsonPatch(PatchOperation.Add(jsonPointer, (JsonNode)42));

var expected = new JsonObject
{
["Values"] = new JsonArray { 1, 2, 3, 4, 42 }
};

var patchResult = jsonPatch.Apply(target);

Assert.IsTrue(expected.IsEquivalentTo(patchResult.Result));
}
}
54 changes: 54 additions & 0 deletions JsonPointer.Tests/ExpressionCreationTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using NUnit.Framework;
// ReSharper disable CollectionNeverUpdated.Local

namespace Json.Pointer.Tests;

Expand All @@ -8,6 +11,8 @@ public class ExpressionCreationTests
private class TestClass
{
public string String { get; set; }
[JsonPropertyName("customName")]
public string Other { get; set; }
public List<int> Ints { get; set; }
public TestClass Nest { get; set; }
public List<TestClass> NestMore { get; set; }
Expand All @@ -23,6 +28,37 @@ public void SimpleProperty()
Assert.AreEqual(expected, actual.ToString());
}

public static IEnumerable<TestCaseData> NamingOptions
{
get
{
yield return new TestCaseData(PropertyNameResolvers.AsDeclared, "/NestMore");
yield return new TestCaseData(PropertyNameResolvers.CamelCase, "/nestMore");
yield return new TestCaseData(PropertyNameResolvers.KebabCase, "/nest-more");
yield return new TestCaseData(PropertyNameResolvers.PascalCase, "/NestMore");
yield return new TestCaseData(PropertyNameResolvers.SnakeCase, "/nest_more");
yield return new TestCaseData(PropertyNameResolvers.UpperKebabCase, "/NEST-MORE");
yield return new TestCaseData(PropertyNameResolvers.UpperSnakeCase, "/NEST_MORE");
}
}

[TestCaseSource(nameof(NamingOptions))]
public void SimplePropertyWithOptions(PropertyNameResolver resolver, string expected)
{
var actual = JsonPointer.Create<TestClass>(x => x.NestMore, new PointerCreationOptions { PropertyNameResolver = resolver });

Assert.AreEqual(expected, actual.ToString());
}

[Test]
public void JsonProperty()
{
var expected = "/customName";
var actual = JsonPointer.Create<TestClass>(x => x.Other);

Assert.AreEqual(expected, actual.ToString());
}

[Test]
public void SimpleArrayIndex()
{
Expand Down Expand Up @@ -67,4 +103,22 @@ public void ArrayIndexWithNestProperty()

Assert.AreEqual(expected, actual.ToString());
}

[Test]
public void LastArrayIndex()
{
var expected = "/NestMore/-";
var actual = JsonPointer.Create<TestClass>(x => x.NestMore.Last());

Assert.AreEqual(expected, actual.ToString());
}

[Test]
public void LastArrayIndexWithNestProperty()
{
var expected = "/NestMore/-/Nest";
var actual = JsonPointer.Create<TestClass>(x => x.NestMore.Last().Nest);

Assert.AreEqual(expected, actual.ToString());
}
}
38 changes: 27 additions & 11 deletions JsonPointer/JsonPointer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -35,9 +35,7 @@ public class JsonPointer : IEquatable<JsonPointer>
/// </summary>
public PointerSegment[] Segments { get; private set; } = null!;

#pragma warning disable CS8618
private JsonPointer() { }
#pragma warning restore CS8618

/// <summary>
/// Parses a JSON Pointer from a string.
Expand Down Expand Up @@ -158,13 +156,25 @@ public static JsonPointer Create(IEnumerable<PointerSegment> segments)
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <param name="expression">The lambda expression which gives the pointer path.</param>
/// <param name="options">(optional) Options for creating the pointer.</param>
/// <returns>The JSON Pointer.</returns>
/// <exception cref="NotSupportedException">
/// Thrown when the lambda expression contains a node that is not a property access or
/// <see cref="int"/>-valued indexer.
/// </exception>
public static JsonPointer Create<T>(Expression<Func<T, object>> expression)
public static JsonPointer Create<T>(Expression<Func<T, object>> expression, PointerCreationOptions? options = null)
{
PointerSegment GetSegment(MemberInfo member)
{
var attribute = member.GetCustomAttribute<JsonPropertyNameAttribute>();
if (attribute is not null)
return attribute.Name;

return options!.PropertyNameResolver!(member);
}

options ??= PointerCreationOptions.Default;

var body = expression.Body;
var segments = new List<PointerSegment>();
while (body != null)
Expand All @@ -174,16 +184,22 @@ public static JsonPointer Create<T>(Expression<Func<T, object>> expression)

if (body is MemberExpression me)
{
segments.Insert(0, PointerSegment.Create(me.Member.Name));
segments.Insert(0, GetSegment(me.Member));
body = me.Expression;
}
else if (body is MethodCallExpression mce &&
mce.Method.Name.StartsWith("get_") &&
mce.Arguments.Count == 1 &&
mce.Arguments[0].Type == typeof(int))
else if (body is MethodCallExpression mce1 &&
mce1.Method.Name.StartsWith("get_") &&
mce1.Arguments.Count == 1 &&
mce1.Arguments[0].Type == typeof(int))
{
segments.Insert(0, PointerSegment.Create(mce1.Arguments[0].ToString()));
body = mce1.Object;
}
else if (body is MethodCallExpression { Method: { IsStatic: true, Name: nameof(Enumerable.Last) } } mce2 &&
mce2.Method.DeclaringType == typeof(Enumerable))
{
segments.Insert(0, PointerSegment.Create(mce.Arguments[0].ToString()));
body = mce.Object;
segments.Insert(0, PointerSegment.Create("-"));
body = mce2.Arguments[0];
}
else if (body is BinaryExpression { Right: ConstantExpression arrayIndexExpression } binaryExpression
and { NodeType: ExpressionType.ArrayIndex })
Expand Down
11 changes: 5 additions & 6 deletions JsonPointer/JsonPointer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<RootNamespace>Json.Pointer</RootNamespace>
<Version>3.0.3</Version>
<Version>3.1.0</Version>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.3.0</FileVersion>
<FileVersion>3.1.0.0</FileVersion>
<PackageId>JsonPointer.Net</PackageId>
<Authors>Greg Dennis</Authors>
<Product>JsonPointer.Net</Product>
Expand Down Expand Up @@ -34,6 +34,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.11.10" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>

Expand All @@ -54,10 +55,8 @@
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)$(DocumentationFile)" DestinationFolder="..\json-everything.net\wwwroot\xml\"
SkipUnchangedFiles="True" OverwriteReadOnlyFiles="True" />
<Copy SourceFiles="$(TargetDir)$(DocumentationFile)" DestinationFolder="..\doc-tool\xml\"
SkipUnchangedFiles="True" OverwriteReadOnlyFiles="True" />
<Copy SourceFiles="$(TargetDir)$(DocumentationFile)" DestinationFolder="..\json-everything.net\wwwroot\xml\" SkipUnchangedFiles="True" OverwriteReadOnlyFiles="True" />
<Copy SourceFiles="$(TargetDir)$(DocumentationFile)" DestinationFolder="..\doc-tool\xml\" SkipUnchangedFiles="True" OverwriteReadOnlyFiles="True" />
</Target>

</Project>
26 changes: 26 additions & 0 deletions JsonPointer/PointerCreationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Linq.Expressions;

namespace Json.Pointer;

/// <summary>
/// Options for creating pointers using <see cref="JsonPointer.Create{T}(Expression{Func{T, object}}, PointerCreationOptions)"/>.
/// </summary>
public class PointerCreationOptions
{
private PropertyNameResolver? _propertyNameResolver;

/// <summary>
/// Default settings.
/// </summary>
public static readonly PointerCreationOptions Default = new();

/// <summary>
/// Gets or sets the property naming resolver. Default is <see cref="PropertyNameResolvers.AsDeclared"/>.
/// </summary>
public PropertyNameResolver? PropertyNameResolver
{
get => _propertyNameResolver ??= PropertyNameResolvers.AsDeclared;
set => _propertyNameResolver = value;
}
}
46 changes: 46 additions & 0 deletions JsonPointer/PropertyNameResolvers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;
using Humanizer;

namespace Json.Pointer;

/// <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 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();
}
8 changes: 8 additions & 0 deletions tools/ApiDocsGenerator/release-notes/rn-json-pointer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ title: JsonPointer.Net
icon: fas fa-tag
order: "8.10"
---
# [3.1.0](https://github.com/gregsdennis/json-everything/pull/509) {#release-pointer-3.1.0}

Enhanced support for creating pointers via Linq expressions.

- Added support for the Linq method `.Last()` to generate a `-` segment which indicates the index beyond the last item in an array.
- Added support for `[JsonPropertyName]` attribute.
- Added support for custom naming transformations.

# [3.0.3](https://github.com/gregsdennis/json-everything/pull/509) {#release-pointer-3.0.3}

Improved performance for `JsonPointer.ToString()` by caching the string representation so that it's only generated once.
Expand Down

0 comments on commit 1ae3cd1

Please sign in to comment.