Skip to content

Commit

Permalink
Allow camelCase JSON properties to be accessed with PascalCase Dynami…
Browse files Browse the repository at this point in the history
…cJson property accessors (#34082)

* Initial work on JsonData property name casing

* pr fb

* More expressive casing options; behave as standard Azure SDK model type by default.

* Export API

* PR fb and implement Set

* tidy

* Add comments for tricky tests.

* Add overload taking casing enum directly

* Change default case mapping

* update API
  • Loading branch information
annelo-msft authored Feb 28, 2023
1 parent a84139d commit d64f1db
Show file tree
Hide file tree
Showing 9 changed files with 472 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ namespace Azure.Core.Dynamic
public static partial class BinaryDataExtensions
{
public static dynamic ToDynamic(this System.BinaryData data) { throw null; }
public static dynamic ToDynamic(this System.BinaryData data, Azure.Core.Dynamic.DynamicJsonNameMapping propertyNameCasing) { throw null; }
public static dynamic ToDynamic(this System.BinaryData data, Azure.Core.Dynamic.DynamicJsonOptions options) { throw null; }
}
public abstract partial class DynamicData
{
Expand Down Expand Up @@ -174,6 +176,20 @@ public readonly partial struct DynamicJsonProperty
public string Name { get { throw null; } }
public Azure.Core.Dynamic.DynamicJson Value { get { throw null; } }
}
public enum DynamicJsonNameMapping
{
None = 0,
PascalCaseGetters = 1,
PascalCaseGettersCamelCaseSetters = 2,
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public partial struct DynamicJsonOptions
{
private int _dummyPrimitive;
public static readonly Azure.Core.Dynamic.DynamicJsonOptions AzureDefault;
public DynamicJsonOptions() { throw null; }
public Azure.Core.Dynamic.DynamicJsonNameMapping PropertyNameCasing { get { throw null; } set { } }
}
}
namespace Azure.Core.Json
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ namespace Azure.Core.Dynamic
public static partial class BinaryDataExtensions
{
public static dynamic ToDynamic(this System.BinaryData data) { throw null; }
public static dynamic ToDynamic(this System.BinaryData data, Azure.Core.Dynamic.DynamicJsonNameMapping propertyNameCasing) { throw null; }
public static dynamic ToDynamic(this System.BinaryData data, Azure.Core.Dynamic.DynamicJsonOptions options) { throw null; }
}
public abstract partial class DynamicData
{
Expand Down Expand Up @@ -174,6 +176,20 @@ public readonly partial struct DynamicJsonProperty
public string Name { get { throw null; } }
public Azure.Core.Dynamic.DynamicJson Value { get { throw null; } }
}
public enum DynamicJsonNameMapping
{
None = 0,
PascalCaseGetters = 1,
PascalCaseGettersCamelCaseSetters = 2,
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public partial struct DynamicJsonOptions
{
private int _dummyPrimitive;
public static readonly Azure.Core.Dynamic.DynamicJsonOptions AzureDefault;
public DynamicJsonOptions() { throw null; }
public Azure.Core.Dynamic.DynamicJsonNameMapping PropertyNameCasing { get { throw null; } set { } }
}
}
namespace Azure.Core.Json
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ namespace Azure.Core.Dynamic
public static partial class BinaryDataExtensions
{
public static dynamic ToDynamic(this System.BinaryData data) { throw null; }
public static dynamic ToDynamic(this System.BinaryData data, Azure.Core.Dynamic.DynamicJsonNameMapping propertyNameCasing) { throw null; }
public static dynamic ToDynamic(this System.BinaryData data, Azure.Core.Dynamic.DynamicJsonOptions options) { throw null; }
}
public abstract partial class DynamicData
{
Expand Down Expand Up @@ -174,6 +176,20 @@ public readonly partial struct DynamicJsonProperty
public string Name { get { throw null; } }
public Azure.Core.Dynamic.DynamicJson Value { get { throw null; } }
}
public enum DynamicJsonNameMapping
{
None = 0,
PascalCaseGetters = 1,
PascalCaseGettersCamelCaseSetters = 2,
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public partial struct DynamicJsonOptions
{
private int _dummyPrimitive;
public static readonly Azure.Core.Dynamic.DynamicJsonOptions AzureDefault;
public DynamicJsonOptions() { throw null; }
public Azure.Core.Dynamic.DynamicJsonNameMapping PropertyNameCasing { get { throw null; } set { } }
}
}
namespace Azure.Core.Json
{
Expand Down
18 changes: 17 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,23 @@ 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());
}

/// <summary>
/// Return the content of the BinaryData as a dynamic type.
/// </summary>
public static dynamic ToDynamic(this BinaryData data, DynamicJsonNameMapping propertyNameCasing)
{
return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, new DynamicJsonOptions() { PropertyNameCasing = propertyNameCasing });
}

/// <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);
}
}
}
67 changes: 66 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,46 @@ 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 (PascalCaseGetters() && char.IsUpper(name[0]))
{
if (_element.TryGetProperty(GetAsCamelCase(name), out element))
{
return new DynamicJson(element);
}
}

return null;
}

private bool PascalCaseGetters()
{
return
_options.PropertyNameCasing == DynamicJsonNameMapping.PascalCaseGetters ||
_options.PropertyNameCasing == DynamicJsonNameMapping.PascalCaseGettersCamelCaseSetters;
}

private bool CamelCaseSetters()
{
return _options.PropertyNameCasing == DynamicJsonNameMapping.PascalCaseGettersCamelCaseSetters;
}

private static string GetAsCamelCase(string value)
{
if (value.Length < 2)
{
return value.ToLowerInvariant();
}

return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}";
}

private object? GetViaIndexer(object index)
{
switch (index)
Expand All @@ -72,6 +106,37 @@ private IEnumerable GetEnumerable()

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

if (_options.PropertyNameCasing == DynamicJsonNameMapping.None)
{
_element = _element.SetProperty(name, value);
return null;
}

if (!char.IsUpper(name[0]))
{
// Lookup name is camelCase, so set unchanged.
_element = _element.SetProperty(name, value);
return null;
}

// Other mappings have PascalCase getters, and lookup name is PascalCase.
// So, if it exists in either form, we'll set it in that form.
if (_element.TryGetProperty(name, out MutableJsonElement element))
{
element.Set(value);
return null;
}

if (_element.TryGetProperty(GetAsCamelCase(name), out element))
{
element.Set(value);
return null;
}

// It's a new property, so set according to the mapping.
name = CamelCaseSetters() ? GetAsCamelCase(name) : name;
_element = _element.SetProperty(name, value);

// Binding machinery expects the call site signature to return an object
Expand Down
29 changes: 29 additions & 0 deletions sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Core.Dynamic
{
/// <summary>
/// Options for setting new DynamicJson properties.
/// </summary>
public enum DynamicJsonNameMapping
{
/// <summary>
/// Properties are accessed and written in the JSON buffer with the same casing as the DynamicJson property.
/// </summary>
None = 0,

/// <summary>
/// A "PascalCase" DynamicJson property can be used to read and set "camelCase" properties that exist in the JSON buffer.
/// New properties are written to the JSON buffer with the same casing as the DynamicJson property.
/// </summary>
PascalCaseGetters = 1,

/// <summary>
/// Default settings for Azure services.
/// A "PascalCase" DynamicJson property can be used to read and set "camelCase" properties that exist in the JSON buffer.
/// New properties are written to the JSON buffer using "camelCase" property names.
/// </summary>
PascalCaseGettersCamelCaseSetters = 2
}
}
29 changes: 29 additions & 0 deletions sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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>
/// Gets the default <see cref="DynamicJsonOptions"/> for Azure services.
/// </summary>
public static readonly DynamicJsonOptions AzureDefault = new()
{
PropertyNameCasing = DynamicJsonNameMapping.PascalCaseGettersCamelCaseSetters
};

/// <summary>
/// Creates a new instance of DynamicJsonOptions.
/// </summary>
public DynamicJsonOptions() { }

/// <summary>
/// Specifies how properties on <see cref="DynamicJson"/> will be accessed in the underlying JSON buffer.
/// </summary>
public DynamicJsonNameMapping PropertyNameCasing { get; set; }
}
}
1 change: 0 additions & 1 deletion sdk/core/Azure.Core.Experimental/src/MutableJsonChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

using System;
using System.Text;
using System.Text.Json;

namespace Azure.Core.Json
Expand Down
Loading

0 comments on commit d64f1db

Please sign in to comment.