Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow camelCase JSON properties to be accessed with PascalCase DynamicJson property accessors #34082

Merged
merged 12 commits into from
Feb 28, 2023
10 changes: 9 additions & 1 deletion sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ public static class BinaryDataExtensions
/// </summary>
public static dynamic ToDynamic(this BinaryData data)
{
return new DynamicJson(MutableJsonDocument.Parse(data).RootElement);
return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, new DynamicJsonOptions());
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Return the content of the BinaryData as a dynamic type.
/// </summary>
public static dynamic ToDynamic(this BinaryData data, DynamicJsonOptions options)
{
return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, options);
}
}
}
20 changes: 19 additions & 1 deletion sdk/core/Azure.Core.Experimental/src/DynamicJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ public sealed partial class DynamicJson : DynamicData, IDisposable
private static readonly MethodInfo SetViaIndexerMethod = typeof(DynamicJson).GetMethod(nameof(SetViaIndexer), BindingFlags.NonPublic | BindingFlags.Instance)!;

private MutableJsonElement _element;
private DynamicJsonOptions _options;

internal DynamicJson(MutableJsonElement element)
internal DynamicJson(MutableJsonElement element, DynamicJsonOptions options = default)
{
_element = element;
_options = options;
}

internal override void WriteTo(Stream stream)
Expand All @@ -39,14 +41,30 @@ internal override void WriteTo(Stream stream)

private object? GetProperty(string name)
{
Argument.AssertNotNullOrEmpty(name, nameof(name));

if (_element.TryGetProperty(name, out MutableJsonElement element))
{
return new DynamicJson(element);
}

if (_options.PropertyCasing.ExistingPropertyAccess == ExistingPropertyCasing.AllowPascalCase &&
char.IsUpper(name[0]))
{
if (_element.TryGetProperty(GetAsCamelCase(name), out element))
{
return new DynamicJson(element);
}
}

return null;
}

private static string GetAsCamelCase(string value)
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}";
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

private object? GetViaIndexer(object index)
{
switch (index)
Expand Down
81 changes: 81 additions & 0 deletions sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Azure.Core.Dynamic
{
/// <summary>
/// Provides the ability for the user to define custom behavior when accessing JSON through a dynamic layer.
/// </summary>
public struct DynamicJsonOptions
{
/// <summary>
/// Creates a new instance of DynamicJsonOptions.
/// </summary>
public DynamicJsonOptions() { }

/// <summary>
/// Specifies how properties on <see cref="DynamicJson"/> will get accessed in the underlying JSON buffer.
/// </summary>
public DynamicJsonPropertyCasing PropertyCasing { get; set; } = DynamicJsonPropertyCasing.Default;
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Casing options for property access on DynamicJson.
/// </summary>
public struct DynamicJsonPropertyCasing
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Default settings for property access casing in DynamicJson.
/// </summary>
public static readonly DynamicJsonPropertyCasing Default = new()
{
ExistingPropertyAccess = ExistingPropertyCasing.AllowPascalCase,
NewPropertyAccess = NewPropertyCasing.WriteCamelCase
};

/// <summary>
/// How DynamicJson property accessors will map to properties in the JSON buffer.
/// </summary>
public ExistingPropertyCasing ExistingPropertyAccess { get; set; }

/// <summary>
/// How DynamicJson property accessors will create new properties in the JSON buffer.
/// </summary>
public NewPropertyCasing NewPropertyAccess { get; set; }
}

/// <summary>
/// Options for setting new DyanmicJson properties.
/// </summary>
public enum NewPropertyCasing
{
/// <summary>
/// New properties are written with the same casing as the DynamicJson property.
/// </summary>
CaseSensitive = 0,

/// <summary>
/// A "PascalCase" DynamicJson property will be written as a "camelCase" property in the JSON buffer.
/// "camelCase" DynamicJson properties will be written in the JSON buffer unchanged.
/// </summary>
WriteCamelCase = 1
}

/// <summary>
/// Options for accessing existing DyanmicJson properties.
/// </summary>
public enum ExistingPropertyCasing
{
/// <summary>
/// The DynamicJson property matches the casing in the JSON buffer exactly.
/// </summary>
CaseSensitive = 0,

/// <summary>
/// A "PascalCase" DynamicJson property can read a "camelCase" property from the JSON buffer.
/// </summary>
AllowPascalCase = 1
}
}
87 changes: 87 additions & 0 deletions sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,93 @@ public void DisposingAChildDisposesTheParent()
Assert.Throws<ObjectDisposedException>(() => { var foo = json.Foo; });
}

[Test]
public void CanGetCamelCasePropertyEitherCase()
{
string json = @"{ ""foo"" : 1 }";

dynamic dynamicJson = new BinaryData(json).ToDynamic();

Assert.AreEqual(1, (int)dynamicJson.foo);
Assert.AreEqual(1, (int)dynamicJson.Foo);
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void CanGetCamelCasePropertyStrictCasing()
{
string json = @"{ ""foo"" : 1 }";

DynamicJsonOptions options = new()
{
PropertyCasing = new DynamicJsonPropertyCasing()
{
ExistingPropertyAccess = ExistingPropertyCasing.CaseSensitive,
NewPropertyAccess = NewPropertyCasing.CaseSensitive
}
};

dynamic dynamicJson = new BinaryData(json).ToDynamic(options);
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

Assert.AreEqual(1, (int)dynamicJson.foo);
Assert.AreEqual(null, dynamicJson.Foo);
}

[Test]
public void CannotGetPascalCasePropertyEitherCase()
{
string json = @"{ ""Foo"" : 1 }";

dynamic dynamicJson = new BinaryData(json).ToDynamic();

Assert.AreEqual(1, (int)dynamicJson.Foo);
Assert.AreEqual(null, dynamicJson.foo);
}

[Test]
public void CanSetPascalCaseStrictCasing()
{
string json = @"{ ""Foo"" : 1 }";

DynamicJsonOptions options = new()
{
PropertyCasing = new DynamicJsonPropertyCasing()
{
ExistingPropertyAccess = ExistingPropertyCasing.CaseSensitive,
NewPropertyAccess = NewPropertyCasing.CaseSensitive
}
};
dynamic dynamicJson = new BinaryData(json).ToDynamic(options);

dynamicJson.foo = 2;

Assert.AreEqual(2, (int)dynamicJson.foo);
Assert.AreEqual(null, dynamicJson.Foo);

dynamicJson.Foo = 3;

Assert.AreEqual(2, (int)dynamicJson.foo);
Assert.AreEqual(3, (int)dynamicJson.Foo);
}

[Test]
public void CanSetPascalCasePropertyEitherCase()
{
string json = @"{ ""Foo"" : 1 }";

DynamicJsonOptions options = new();
dynamic dynamicJson = new BinaryData(json).ToDynamic(options);

dynamicJson.foo = 2;

Assert.AreEqual(2, (int)dynamicJson.foo);
Assert.AreEqual(2, (int)dynamicJson.Foo);

dynamicJson.Foo = 3;
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

Assert.AreEqual(3, (int)dynamicJson.foo);
Assert.AreEqual(3, (int)dynamicJson.Foo);
}

#region Helpers
internal static dynamic GetDynamicJson(string json)
{
Expand Down