From 7a618ffff0d6685f851990731f86dafcc4bdb934 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 7 Feb 2023 11:12:34 -0800 Subject: [PATCH 01/10] Initial work on JsonData property name casing --- .../src/BinaryDataExtensions.cs | 8 +++ .../src/DynamicJson.cs | 38 ++++++++++++- .../src/DynamicJsonOptions.cs | 20 +++++++ .../src/MutableJsonElement.cs | 2 +- .../tests/DynamicJsonTests.cs | 53 +++++++++++++++++++ 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs diff --git a/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs b/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs index f34689fa4365c..cef06f43c32e8 100644 --- a/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs +++ b/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs @@ -17,5 +17,13 @@ public static dynamic ToDynamic(this BinaryData data) { return new DynamicJson(MutableJsonDocument.Parse(data).RootElement); } + + /// + /// Return the content of the BinaryData as a dynamic type. + /// + public static dynamic ToDynamic(this BinaryData data, DynamicJsonOptions options) + { + return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, options); + } } } diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs index 7eac236c00c01..ffeab8fb91b9d 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs @@ -23,10 +23,12 @@ public partial class DynamicJson : DynamicData 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) @@ -38,7 +40,39 @@ internal override void WriteTo(Stream stream) private object GetProperty(string name) { - return new DynamicJson(_element.GetProperty(name)); + if (!_options.AccessPropertyNamesPascalOrCamelCase) + { + return new DynamicJson(_element.GetProperty(name)); + } + + if (_element.TryGetProperty(name, out MutableJsonElement property)) + { + return new DynamicJson(property); + } + + // Either PascalCase or camelCase lookup failed. Try the other. + string otherCaseName = GetAsOtherCasing(name); + if (_element.TryGetProperty(otherCaseName, out property)) + { + return new DynamicJson(property); + } + + return new InvalidOperationException($"JSON does not contain property called {name}"); + } + + private static string GetAsOtherCasing(string value) + { + if (value.Length < 1) + { + throw new InvalidOperationException($"Invalid property name: {value}"); + } + + if (char.IsUpper(value[0])) + { + return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; + } + + return $"{char.ToUpperInvariant(value[0])}{value.Substring(1)}"; } private object GetViaIndexer(object index) diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs new file mode 100644 index 0000000000000..d0c2862787abe --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core.Dynamic +{ + /// + /// Provides the ability for the user to define custom behavior when accessing JSON through a dynamic layer. + /// + public struct DynamicJsonOptions + { + /// + /// Specifies whether properties in the can be read + /// as with either "PascalCase" or "camelCase" property names. If set to false, + /// property reads are case sensitive. + /// + public bool AccessPropertyNamesPascalOrCamelCase { get; set; } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/MutableJsonElement.cs b/sdk/core/Azure.Core.Experimental/src/MutableJsonElement.cs index 2162815df73fc..acabe863b94a1 100644 --- a/sdk/core/Azure.Core.Experimental/src/MutableJsonElement.cs +++ b/sdk/core/Azure.Core.Experimental/src/MutableJsonElement.cs @@ -50,7 +50,7 @@ public MutableJsonElement GetProperty(string name) { if (!TryGetProperty(name, out MutableJsonElement value)) { - throw new InvalidOperationException($"{_path} does not containe property called {name}"); + throw new InvalidOperationException($"{_path} does not contain property called {name}"); } return value; diff --git a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs index c97cfef5d3a43..c0b68146a16c3 100644 --- a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs @@ -278,6 +278,59 @@ public void CanMakeChangesAndAddNewProperty() Assert.AreEqual(2, (int)jsonData.Bar); } + [Test] + public void CanGetCamelCasePropertyEitherCase() + { + string json = @"{ ""foo"" : 1 }"; + DynamicJsonOptions options = new() + { + AccessPropertyNamesPascalOrCamelCase = true + }; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + + Assert.AreEqual(1, (int)dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + + // TODO: Handle sets ... + } + + [Test] + public void CanGetPascalCasePropertyEitherCase() + { + string json = @"{ ""Foo"" : 1 }"; + DynamicJsonOptions options = new() + { + AccessPropertyNamesPascalOrCamelCase = true + }; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + + Assert.AreEqual(1, (int)dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + } + + [Test] + public void CanSetPascalCasePropertyEitherCase() + { + string json = @"{ ""Foo"" : 1 }"; + DynamicJsonOptions options = new() + { + AccessPropertyNamesPascalOrCamelCase = true + }; + + 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; + + Assert.AreEqual(3, (int)dynamicJson.foo); + Assert.AreEqual(3, (int)dynamicJson.Foo); + } + #region Helpers internal static dynamic GetDynamicJson(string json) { From cb9ac1bde549dfdf5c1020aa7fd7dc9f729bdc23 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 14 Feb 2023 09:20:45 -0800 Subject: [PATCH 02/10] pr fb --- .../src/DynamicJson.cs | 25 ++++++++----------- .../src/DynamicJsonOptions.cs | 4 +-- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs index 9b3f0cb466f63..ba49558914229 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs @@ -41,21 +41,23 @@ internal override void WriteTo(Stream stream) private object? GetProperty(string name) { + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + MutableJsonElement element; if (!_options.AccessPropertyNamesPascalOrCamelCase) { - if (_element.TryGetProperty(name, out MutableJsonElement element)) - { - return new DynamicJson(element); - } - - return null; - } + if (_element.TryGetProperty(name, out element)) + { + return new DynamicJson(element); + } + return null; + } string otherCaseName = GetAsOtherCasing(name); - if (_element.TryGetProperty(otherCaseName, out property)) + if (_element.TryGetProperty(otherCaseName, out element)) { - return new DynamicJson(property); + return new DynamicJson(element); } return null; @@ -63,11 +65,6 @@ internal override void WriteTo(Stream stream) private static string GetAsOtherCasing(string value) { - if (value.Length < 1) - { - throw new InvalidOperationException($"Invalid property name: {value}"); - } - if (char.IsUpper(value[0])) { return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs index d0c2862787abe..63308466fb46b 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs @@ -12,8 +12,8 @@ public struct DynamicJsonOptions { /// /// Specifies whether properties in the can be read - /// as with either "PascalCase" or "camelCase" property names. If set to false, - /// property reads are case sensitive. + /// with either "PascalCase" or "camelCase" property names. If set to false, + /// property reads are strictly case sensitive. /// public bool AccessPropertyNamesPascalOrCamelCase { get; set; } } From 7c6ed90e3f52ee20264c321e86c44b8a2880f908 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 14 Feb 2023 10:15:03 -0800 Subject: [PATCH 03/10] More expressive casing options; behave as standard Azure SDK model type by default. --- .../src/BinaryDataExtensions.cs | 2 +- .../src/DynamicJson.cs | 28 +++----- .../src/DynamicJsonOptions.cs | 69 +++++++++++++++++-- .../tests/DynamicJsonTests.cs | 58 ++++++++++++---- 4 files changed, 122 insertions(+), 35 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs b/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs index 87e7f7256d49b..c1f79b968df86 100644 --- a/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs +++ b/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs @@ -16,7 +16,7 @@ public static class BinaryDataExtensions /// public static dynamic ToDynamic(this BinaryData data) { - return new DynamicJson(MutableJsonDocument.Parse(data).RootElement); + return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, new DynamicJsonOptions()); } /// diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs index ba49558914229..076dce0affe86 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs @@ -43,34 +43,26 @@ internal override void WriteTo(Stream stream) { Argument.AssertNotNullOrEmpty(name, nameof(name)); - MutableJsonElement element; - if (!_options.AccessPropertyNamesPascalOrCamelCase) + if (_element.TryGetProperty(name, out MutableJsonElement element)) { - if (_element.TryGetProperty(name, out element)) - { - return new DynamicJson(element); - } - - return null; + return new DynamicJson(element); } - string otherCaseName = GetAsOtherCasing(name); - if (_element.TryGetProperty(otherCaseName, out element)) + if (_options.PropertyCasing.ExistingPropertyAccess == ExistingPropertyCasing.AllowPascalCase && + char.IsUpper(name[0])) { - return new DynamicJson(element); + if (_element.TryGetProperty(GetAsCamelCase(name), out element)) + { + return new DynamicJson(element); + } } return null; } - private static string GetAsOtherCasing(string value) + private static string GetAsCamelCase(string value) { - if (char.IsUpper(value[0])) - { - return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; - } - - return $"{char.ToUpperInvariant(value[0])}{value.Substring(1)}"; + return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; } private object? GetViaIndexer(object index) diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs index 63308466fb46b..e7aa4d76481a3 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs @@ -11,10 +11,71 @@ namespace Azure.Core.Dynamic public struct DynamicJsonOptions { /// - /// Specifies whether properties in the can be read - /// with either "PascalCase" or "camelCase" property names. If set to false, - /// property reads are strictly case sensitive. + /// Creates a new instance of DynamicJsonOptions. /// - public bool AccessPropertyNamesPascalOrCamelCase { get; set; } + public DynamicJsonOptions() { } + + /// + /// Specifies how properties on will get accessed in the underlying JSON buffer. + /// + public DynamicJsonPropertyCasing PropertyCasing { get; set; } = DynamicJsonPropertyCasing.Default; + } + + /// + /// Casing options for property access on DynamicJson. + /// + public struct DynamicJsonPropertyCasing + { + /// + /// Default settings for property access casing in DynamicJson. + /// + public static readonly DynamicJsonPropertyCasing Default = new() + { + ExistingPropertyAccess = ExistingPropertyCasing.AllowPascalCase, + NewPropertyAccess = NewPropertyCasing.WriteCamelCase + }; + + /// + /// How DynamicJson property accessors will map to properties in the JSON buffer. + /// + public ExistingPropertyCasing ExistingPropertyAccess { get; set; } + + /// + /// How DynamicJson property accessors will create new properties in the JSON buffer. + /// + public NewPropertyCasing NewPropertyAccess { get; set; } + } + + /// + /// Options for setting new DyanmicJson properties. + /// + public enum NewPropertyCasing + { + /// + /// New properties are written with the same casing as the DynamicJson property. + /// + CaseSensitive = 0, + + /// + /// 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. + /// + WriteCamelCase = 1 + } + + /// + /// Options for accessing existing DyanmicJson properties. + /// + public enum ExistingPropertyCasing + { + /// + /// The DynamicJson property matches the casing in the JSON buffer exactly. + /// + CaseSensitive = 0, + + /// + /// A "PascalCase" DynamicJson property can read a "camelCase" property from the JSON buffer. + /// + AllowPascalCase = 1 } } diff --git a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs index c81798c445c04..ef3832e55ade5 100644 --- a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs @@ -409,44 +409,78 @@ public void DisposingAChildDisposesTheParent() public void CanGetCamelCasePropertyEitherCase() { string json = @"{ ""foo"" : 1 }"; - DynamicJsonOptions options = new() - { - AccessPropertyNamesPascalOrCamelCase = true - }; - dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + dynamic dynamicJson = new BinaryData(json).ToDynamic(); Assert.AreEqual(1, (int)dynamicJson.foo); Assert.AreEqual(1, (int)dynamicJson.Foo); - - // TODO: Handle sets ... } [Test] - public void CanGetPascalCasePropertyEitherCase() + public void CanGetCamelCasePropertyStrictCasing() { - string json = @"{ ""Foo"" : 1 }"; + string json = @"{ ""foo"" : 1 }"; + DynamicJsonOptions options = new() { - AccessPropertyNamesPascalOrCamelCase = true + PropertyCasing = new DynamicJsonPropertyCasing() + { + ExistingPropertyAccess = ExistingPropertyCasing.CaseSensitive, + NewPropertyAccess = NewPropertyCasing.CaseSensitive + } }; dynamic dynamicJson = new BinaryData(json).ToDynamic(options); 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 CanSetPascalCasePropertyEitherCase() + public void CanSetPascalCaseStrictCasing() { string json = @"{ ""Foo"" : 1 }"; + DynamicJsonOptions options = new() { - AccessPropertyNamesPascalOrCamelCase = true + 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); From 29dc9a74822cdebfdf76ec4593ae8bec93d2c072 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 14 Feb 2023 10:19:26 -0800 Subject: [PATCH 04/10] Export API --- .../api/Azure.Core.Experimental.net461.cs | 26 +++++++++++++++++++ .../api/Azure.Core.Experimental.net6.0.cs | 26 +++++++++++++++++++ .../Azure.Core.Experimental.netstandard2.0.cs | 26 +++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs index a1141baaec9c3..2c0defbb1860f 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs @@ -111,6 +111,7 @@ 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.DynamicJsonOptions options) { throw null; } } public abstract partial class DynamicData { @@ -151,6 +152,31 @@ public void Reset() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct DynamicJsonOptions + { + private int _dummyPrimitive; + public DynamicJsonOptions() { throw null; } + public Azure.Core.Dynamic.DynamicJsonPropertyCasing PropertyCasing { get { throw null; } set { } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct DynamicJsonPropertyCasing + { + private int _dummyPrimitive; + public static readonly Azure.Core.Dynamic.DynamicJsonPropertyCasing Default; + public Azure.Core.Dynamic.ExistingPropertyCasing ExistingPropertyAccess { get { throw null; } set { } } + public Azure.Core.Dynamic.NewPropertyCasing NewPropertyAccess { get { throw null; } set { } } + } + public enum ExistingPropertyCasing + { + CaseSensitive = 0, + AllowPascalCase = 1, + } + public enum NewPropertyCasing + { + CaseSensitive = 0, + WriteCamelCase = 1, + } } namespace Azure.Core.Json { diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs index a1141baaec9c3..2c0defbb1860f 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs @@ -111,6 +111,7 @@ 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.DynamicJsonOptions options) { throw null; } } public abstract partial class DynamicData { @@ -151,6 +152,31 @@ public void Reset() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct DynamicJsonOptions + { + private int _dummyPrimitive; + public DynamicJsonOptions() { throw null; } + public Azure.Core.Dynamic.DynamicJsonPropertyCasing PropertyCasing { get { throw null; } set { } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct DynamicJsonPropertyCasing + { + private int _dummyPrimitive; + public static readonly Azure.Core.Dynamic.DynamicJsonPropertyCasing Default; + public Azure.Core.Dynamic.ExistingPropertyCasing ExistingPropertyAccess { get { throw null; } set { } } + public Azure.Core.Dynamic.NewPropertyCasing NewPropertyAccess { get { throw null; } set { } } + } + public enum ExistingPropertyCasing + { + CaseSensitive = 0, + AllowPascalCase = 1, + } + public enum NewPropertyCasing + { + CaseSensitive = 0, + WriteCamelCase = 1, + } } namespace Azure.Core.Json { diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index a1141baaec9c3..2c0defbb1860f 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -111,6 +111,7 @@ 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.DynamicJsonOptions options) { throw null; } } public abstract partial class DynamicData { @@ -151,6 +152,31 @@ public void Reset() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct DynamicJsonOptions + { + private int _dummyPrimitive; + public DynamicJsonOptions() { throw null; } + public Azure.Core.Dynamic.DynamicJsonPropertyCasing PropertyCasing { get { throw null; } set { } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct DynamicJsonPropertyCasing + { + private int _dummyPrimitive; + public static readonly Azure.Core.Dynamic.DynamicJsonPropertyCasing Default; + public Azure.Core.Dynamic.ExistingPropertyCasing ExistingPropertyAccess { get { throw null; } set { } } + public Azure.Core.Dynamic.NewPropertyCasing NewPropertyAccess { get { throw null; } set { } } + } + public enum ExistingPropertyCasing + { + CaseSensitive = 0, + AllowPascalCase = 1, + } + public enum NewPropertyCasing + { + CaseSensitive = 0, + WriteCamelCase = 1, + } } namespace Azure.Core.Json { From f3fbd5c5007b256cfedbf802ae8bc60f36f271d6 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Feb 2023 17:16:51 -0800 Subject: [PATCH 05/10] PR fb and implement Set --- .../api/Azure.Core.Experimental.net461.cs | 26 +- .../api/Azure.Core.Experimental.net6.0.cs | 26 +- .../Azure.Core.Experimental.netstandard2.0.cs | 26 +- .../src/DynamicJson.cs | 47 +++- .../src/DynamicJsonNameMapping.cs | 31 +++ .../src/DynamicJsonOptions.cs | 62 +---- .../tests/DynamicJsonTests.cs | 248 ++++++++++++++++-- 7 files changed, 322 insertions(+), 144 deletions(-) create mode 100644 sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs index 2c0defbb1860f..aa41eee892868 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs @@ -152,30 +152,18 @@ public void Reset() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } } } - [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public partial struct DynamicJsonOptions + public enum DynamicJsonNameMapping { - private int _dummyPrimitive; - public DynamicJsonOptions() { throw null; } - public Azure.Core.Dynamic.DynamicJsonPropertyCasing PropertyCasing { get { throw null; } set { } } + None = 0, + PascalCaseGetters = 1, + PascalCaseGettersCamelCaseSetters = 2, } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public partial struct DynamicJsonPropertyCasing + public partial struct DynamicJsonOptions { private int _dummyPrimitive; - public static readonly Azure.Core.Dynamic.DynamicJsonPropertyCasing Default; - public Azure.Core.Dynamic.ExistingPropertyCasing ExistingPropertyAccess { get { throw null; } set { } } - public Azure.Core.Dynamic.NewPropertyCasing NewPropertyAccess { get { throw null; } set { } } - } - public enum ExistingPropertyCasing - { - CaseSensitive = 0, - AllowPascalCase = 1, - } - public enum NewPropertyCasing - { - CaseSensitive = 0, - WriteCamelCase = 1, + public DynamicJsonOptions() { throw null; } + public Azure.Core.Dynamic.DynamicJsonNameMapping PropertyNameCasing { get { throw null; } set { } } } } namespace Azure.Core.Json diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs index 2c0defbb1860f..aa41eee892868 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs @@ -152,30 +152,18 @@ public void Reset() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } } } - [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public partial struct DynamicJsonOptions + public enum DynamicJsonNameMapping { - private int _dummyPrimitive; - public DynamicJsonOptions() { throw null; } - public Azure.Core.Dynamic.DynamicJsonPropertyCasing PropertyCasing { get { throw null; } set { } } + None = 0, + PascalCaseGetters = 1, + PascalCaseGettersCamelCaseSetters = 2, } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public partial struct DynamicJsonPropertyCasing + public partial struct DynamicJsonOptions { private int _dummyPrimitive; - public static readonly Azure.Core.Dynamic.DynamicJsonPropertyCasing Default; - public Azure.Core.Dynamic.ExistingPropertyCasing ExistingPropertyAccess { get { throw null; } set { } } - public Azure.Core.Dynamic.NewPropertyCasing NewPropertyAccess { get { throw null; } set { } } - } - public enum ExistingPropertyCasing - { - CaseSensitive = 0, - AllowPascalCase = 1, - } - public enum NewPropertyCasing - { - CaseSensitive = 0, - WriteCamelCase = 1, + public DynamicJsonOptions() { throw null; } + public Azure.Core.Dynamic.DynamicJsonNameMapping PropertyNameCasing { get { throw null; } set { } } } } namespace Azure.Core.Json diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 2c0defbb1860f..aa41eee892868 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -152,30 +152,18 @@ public void Reset() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } } } - [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public partial struct DynamicJsonOptions + public enum DynamicJsonNameMapping { - private int _dummyPrimitive; - public DynamicJsonOptions() { throw null; } - public Azure.Core.Dynamic.DynamicJsonPropertyCasing PropertyCasing { get { throw null; } set { } } + None = 0, + PascalCaseGetters = 1, + PascalCaseGettersCamelCaseSetters = 2, } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public partial struct DynamicJsonPropertyCasing + public partial struct DynamicJsonOptions { private int _dummyPrimitive; - public static readonly Azure.Core.Dynamic.DynamicJsonPropertyCasing Default; - public Azure.Core.Dynamic.ExistingPropertyCasing ExistingPropertyAccess { get { throw null; } set { } } - public Azure.Core.Dynamic.NewPropertyCasing NewPropertyAccess { get { throw null; } set { } } - } - public enum ExistingPropertyCasing - { - CaseSensitive = 0, - AllowPascalCase = 1, - } - public enum NewPropertyCasing - { - CaseSensitive = 0, - WriteCamelCase = 1, + public DynamicJsonOptions() { throw null; } + public Azure.Core.Dynamic.DynamicJsonNameMapping PropertyNameCasing { get { throw null; } set { } } } } namespace Azure.Core.Json diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs index 076dce0affe86..97a53a0b77ff4 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using System.Xml.Linq; using Azure.Core.Json; namespace Azure.Core.Dynamic @@ -48,8 +49,7 @@ internal override void WriteTo(Stream stream) return new DynamicJson(element); } - if (_options.PropertyCasing.ExistingPropertyAccess == ExistingPropertyCasing.AllowPascalCase && - char.IsUpper(name[0])) + if (PascalCaseGetters() && char.IsUpper(name[0])) { if (_element.TryGetProperty(GetAsCamelCase(name), out element)) { @@ -60,6 +60,18 @@ internal override void WriteTo(Stream stream) 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) { return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; @@ -85,6 +97,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 diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs new file mode 100644 index 0000000000000..b077dbc5be3ee --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core.Dynamic +{ + /// + /// Options for setting new DynamicJson properties. + /// + public enum DynamicJsonNameMapping + { + /// + /// Properties are accessed and written in the JSON buffer with the same casing as the DynamicJson property. + /// + None = 0, + + /// + /// 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. + /// + PascalCaseGetters = 1, + + /// + /// 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. + /// + PascalCaseGettersCamelCaseSetters = 2 + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs index e7aa4d76481a3..56bc5224a97f2 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs @@ -16,66 +16,8 @@ public struct DynamicJsonOptions public DynamicJsonOptions() { } /// - /// Specifies how properties on will get accessed in the underlying JSON buffer. + /// Specifies how properties on will be accessed in the underlying JSON buffer. /// - public DynamicJsonPropertyCasing PropertyCasing { get; set; } = DynamicJsonPropertyCasing.Default; - } - - /// - /// Casing options for property access on DynamicJson. - /// - public struct DynamicJsonPropertyCasing - { - /// - /// Default settings for property access casing in DynamicJson. - /// - public static readonly DynamicJsonPropertyCasing Default = new() - { - ExistingPropertyAccess = ExistingPropertyCasing.AllowPascalCase, - NewPropertyAccess = NewPropertyCasing.WriteCamelCase - }; - - /// - /// How DynamicJson property accessors will map to properties in the JSON buffer. - /// - public ExistingPropertyCasing ExistingPropertyAccess { get; set; } - - /// - /// How DynamicJson property accessors will create new properties in the JSON buffer. - /// - public NewPropertyCasing NewPropertyAccess { get; set; } - } - - /// - /// Options for setting new DyanmicJson properties. - /// - public enum NewPropertyCasing - { - /// - /// New properties are written with the same casing as the DynamicJson property. - /// - CaseSensitive = 0, - - /// - /// 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. - /// - WriteCamelCase = 1 - } - - /// - /// Options for accessing existing DyanmicJson properties. - /// - public enum ExistingPropertyCasing - { - /// - /// The DynamicJson property matches the casing in the JSON buffer exactly. - /// - CaseSensitive = 0, - - /// - /// A "PascalCase" DynamicJson property can read a "camelCase" property from the JSON buffer. - /// - AllowPascalCase = 1 + public DynamicJsonNameMapping PropertyNameCasing { get; set; } = DynamicJsonNameMapping.PascalCaseGettersCamelCaseSetters; } } diff --git a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs index ef3832e55ade5..655832510d4c1 100644 --- a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs @@ -408,7 +408,7 @@ public void DisposingAChildDisposesTheParent() [Test] public void CanGetCamelCasePropertyEitherCase() { - string json = @"{ ""foo"" : 1 }"; + string json = """{ "foo" : 1 }"""; dynamic dynamicJson = new BinaryData(json).ToDynamic(); @@ -417,17 +417,13 @@ public void CanGetCamelCasePropertyEitherCase() } [Test] - public void CanGetCamelCasePropertyStrictCasing() + public void CanGetCamelCasePropertyNoMapping() { - string json = @"{ ""foo"" : 1 }"; + string json = """{ "foo" : 1 }"""; DynamicJsonOptions options = new() { - PropertyCasing = new DynamicJsonPropertyCasing() - { - ExistingPropertyAccess = ExistingPropertyCasing.CaseSensitive, - NewPropertyAccess = NewPropertyCasing.CaseSensitive - } + PropertyNameCasing = DynamicJsonNameMapping.None }; dynamic dynamicJson = new BinaryData(json).ToDynamic(options); @@ -437,59 +433,261 @@ public void CanGetCamelCasePropertyStrictCasing() } [Test] - public void CannotGetPascalCasePropertyEitherCase() + public void CanGetCamelCasePropertyPascalGetters() + { + string json = """{ "foo" : 1 }"""; + + DynamicJsonOptions options = new() + { + PropertyNameCasing = DynamicJsonNameMapping.PascalCaseGetters + }; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + + Assert.AreEqual(1, (int)dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + } + + [Test] + public void CanGetCamelCasePropertyPascalGettersCamelSetters() { - string json = @"{ ""Foo"" : 1 }"; + string json = """{ "foo" : 1 }"""; + // This is the default setting. dynamic dynamicJson = new BinaryData(json).ToDynamic(); + Assert.AreEqual(1, (int)dynamicJson.foo); Assert.AreEqual(1, (int)dynamicJson.Foo); + } + + [Test] + public void CanGetPascalCasePropertyNoMapping() + { + string json = """{ "Foo" : 1 }"""; + + DynamicJsonOptions options = new() + { + PropertyNameCasing = DynamicJsonNameMapping.None + }; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + Assert.AreEqual(null, dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); } [Test] - public void CanSetPascalCaseStrictCasing() + public void CanGetPascalCasePropertyPascalGetters() { - string json = @"{ ""Foo"" : 1 }"; + string json = """{ "Foo" : 1 }"""; DynamicJsonOptions options = new() { - PropertyCasing = new DynamicJsonPropertyCasing() - { - ExistingPropertyAccess = ExistingPropertyCasing.CaseSensitive, - NewPropertyAccess = NewPropertyCasing.CaseSensitive - } + PropertyNameCasing = DynamicJsonNameMapping.PascalCaseGetters }; + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + Assert.AreEqual(null, dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + } + + [Test] + public void CanGetPascalCasePropertyPascalGettersCamelSetters() + { + string json = """{ "Foo" : 1 }"""; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(); + + Assert.AreEqual(null, dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + } + + [Test] + public void CanSetCamelCaseNoMapping() + { + string json = """{ "foo": 1 }"""; + + DynamicJsonOptions options = new() + { + PropertyNameCasing = DynamicJsonNameMapping.None + }; + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + + // Existing property access dynamicJson.foo = 2; + // New property access + dynamicJson.bar = 3; + Assert.AreEqual(2, (int)dynamicJson.foo); Assert.AreEqual(null, dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(null, dynamicJson.Bar); - dynamicJson.Foo = 3; + dynamicJson.Foo = 4; + dynamicJson.Bar = 5; Assert.AreEqual(2, (int)dynamicJson.foo); - Assert.AreEqual(3, (int)dynamicJson.Foo); + Assert.AreEqual(4, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(5, (int)dynamicJson.Bar); } [Test] - public void CanSetPascalCasePropertyEitherCase() + public void CanSetCamelCasePascalGetters() { - string json = @"{ ""Foo"" : 1 }"; + string json = """{ "foo": 1 }"""; - DynamicJsonOptions options = new(); + DynamicJsonOptions options = new() + { + PropertyNameCasing = DynamicJsonNameMapping.PascalCaseGetters + }; dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + // Existing property access + dynamicJson.foo = 2; + + // New property is created as camelCase + dynamicJson.bar = 3; + + Assert.AreEqual(2, (int)dynamicJson.foo); + Assert.AreEqual(2, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(3, (int)dynamicJson.Bar); + + dynamicJson.Foo = 4; + dynamicJson.Bar = 5; + + // New property is created as PascalCase + dynamicJson.Baz = 6; + + Assert.AreEqual(4, (int)dynamicJson.foo); + Assert.AreEqual(4, (int)dynamicJson.Foo); + Assert.AreEqual(5, (int)dynamicJson.bar); + Assert.AreEqual(5, (int)dynamicJson.Bar); + Assert.AreEqual(null, dynamicJson.baz); + Assert.AreEqual(6, (int)dynamicJson.Baz); + } + + [Test] + public void CanSetCamelCasePascalGettersCamelSetters() + { + string json = """{ "foo": 1 }"""; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(); + + // Existing property access dynamicJson.foo = 2; + // New property is created as camelCase + dynamicJson.bar = 3; + Assert.AreEqual(2, (int)dynamicJson.foo); Assert.AreEqual(2, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(3, (int)dynamicJson.Bar); + + dynamicJson.Foo = 4; + dynamicJson.Bar = 5; - dynamicJson.Foo = 3; + // New property is created as camelCase + dynamicJson.Baz = 6; - Assert.AreEqual(3, (int)dynamicJson.foo); - Assert.AreEqual(3, (int)dynamicJson.Foo); + Assert.AreEqual(4, (int)dynamicJson.foo); + Assert.AreEqual(4, (int)dynamicJson.Foo); + Assert.AreEqual(5, (int)dynamicJson.bar); + Assert.AreEqual(5, (int)dynamicJson.Bar); + Assert.AreEqual(6, (int)dynamicJson.baz); + Assert.AreEqual(6, (int)dynamicJson.Baz); + } + + [Test] + public void CanSetPascalCaseNoMapping() + { + string json = """{ "Foo": 1 }"""; + + DynamicJsonOptions options = new() + { + PropertyNameCasing = DynamicJsonNameMapping.None + }; + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + + // Existing property access + dynamicJson.foo = 2; + + // New property access + dynamicJson.bar = 3; + + Assert.AreEqual(2, (int)dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(null, dynamicJson.Bar); + + dynamicJson.Foo = 4; + dynamicJson.Bar = 5; + + Assert.AreEqual(2, (int)dynamicJson.foo); + Assert.AreEqual(4, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(5, (int)dynamicJson.Bar); + } + + [Test] + public void CanSetPascalCasePascalGetters() + { + string json = """{ "Foo": 1 }"""; + + DynamicJsonOptions options = new() + { + PropertyNameCasing = DynamicJsonNameMapping.PascalCaseGetters + }; + dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + + // This property doesn't exist, so it creates a new camelCase property. + dynamicJson.foo = 2; + + // New property is created as camelCase + dynamicJson.bar = 3; + + Assert.AreEqual(2, (int)dynamicJson.foo); + Assert.AreEqual(1, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(3, (int)dynamicJson.Bar); + + dynamicJson.Foo = 4; + + // This property exists now as `bar`, so it updates the camelCase property. + dynamicJson.Bar = 5; + + // New property is created as PascalCase + dynamicJson.Baz = 6; + + Assert.AreEqual(2, (int)dynamicJson.foo); + Assert.AreEqual(4, (int)dynamicJson.Foo); + Assert.AreEqual(5, (int)dynamicJson.bar); + Assert.AreEqual(5, (int)dynamicJson.Bar); + Assert.AreEqual(null, dynamicJson.baz); + Assert.AreEqual(6, (int)dynamicJson.Baz); + } + + [Test] + public void CanSetPascalCasePascalGettersCamelSetters() + { + string json = """{ "Foo": 1 }"""; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(); + + // Existing property access + dynamicJson.Foo = 2; + + // New property is created as camelCase + dynamicJson.Bar = 3; + + Assert.AreEqual(null, dynamicJson.foo); + Assert.AreEqual(2, (int)dynamicJson.Foo); + Assert.AreEqual(3, (int)dynamicJson.bar); + Assert.AreEqual(3, (int)dynamicJson.Bar); } #region Helpers From 9b6dde103b664fe2c489f5afc09d6df8ad5e8347 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Feb 2023 17:22:31 -0800 Subject: [PATCH 06/10] tidy --- sdk/core/Azure.Core.Experimental/src/DynamicJson.cs | 1 - sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs | 2 -- sdk/core/Azure.Core.Experimental/src/MutableJsonChange.cs | 1 - 3 files changed, 4 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs index 97a53a0b77ff4..a71dca854d389 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -using System.Xml.Linq; using Azure.Core.Json; namespace Azure.Core.Dynamic diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs index b077dbc5be3ee..71a0d290c454a 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonNameMapping.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; - namespace Azure.Core.Dynamic { /// diff --git a/sdk/core/Azure.Core.Experimental/src/MutableJsonChange.cs b/sdk/core/Azure.Core.Experimental/src/MutableJsonChange.cs index 88bfd050dee98..8d5b1a63238e8 100644 --- a/sdk/core/Azure.Core.Experimental/src/MutableJsonChange.cs +++ b/sdk/core/Azure.Core.Experimental/src/MutableJsonChange.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Text; using System.Text.Json; namespace Azure.Core.Json From c2467b2df31128d2b7578eb5e7b5a74f165a7b24 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Feb 2023 17:33:29 -0800 Subject: [PATCH 07/10] Add comments for tricky tests. --- .../tests/DynamicJsonTests.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs index 655832510d4c1..1b5ada82cb934 100644 --- a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs @@ -556,6 +556,7 @@ public void CanSetCamelCasePascalGetters() Assert.AreEqual(3, (int)dynamicJson.bar); Assert.AreEqual(3, (int)dynamicJson.Bar); + // PascalCase getters find camelCase properties and sets them. dynamicJson.Foo = 4; dynamicJson.Bar = 5; @@ -588,6 +589,7 @@ public void CanSetCamelCasePascalGettersCamelSetters() Assert.AreEqual(3, (int)dynamicJson.bar); Assert.AreEqual(3, (int)dynamicJson.Bar); + // PascalCase getters find camelCase properties and sets them. dynamicJson.Foo = 4; dynamicJson.Bar = 5; @@ -613,7 +615,7 @@ public void CanSetPascalCaseNoMapping() }; dynamic dynamicJson = new BinaryData(json).ToDynamic(options); - // Existing property access + // This adds a new property, since it doesn't find `Foo`. dynamicJson.foo = 2; // New property access @@ -624,7 +626,10 @@ public void CanSetPascalCaseNoMapping() Assert.AreEqual(3, (int)dynamicJson.bar); Assert.AreEqual(null, dynamicJson.Bar); + // This updates the PascalCase property and not the camelCase one. dynamicJson.Foo = 4; + + // This creates a new PascalCase property. dynamicJson.Bar = 5; Assert.AreEqual(2, (int)dynamicJson.foo); @@ -655,9 +660,10 @@ public void CanSetPascalCasePascalGetters() Assert.AreEqual(3, (int)dynamicJson.bar); Assert.AreEqual(3, (int)dynamicJson.Bar); + // This updates the PascalCase property and not the camelCase one. dynamicJson.Foo = 4; - // This property exists now as `bar`, so it updates the camelCase property. + // The PascalCase getter finds `bar`, so it updates the camelCase property. dynamicJson.Bar = 5; // New property is created as PascalCase @@ -678,7 +684,7 @@ public void CanSetPascalCasePascalGettersCamelSetters() dynamic dynamicJson = new BinaryData(json).ToDynamic(); - // Existing property access + // Existing property access does not add a camelCase property. dynamicJson.Foo = 2; // New property is created as camelCase From 169690202a1f7af283a6b06d3c91b05fcf96f3d4 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 27 Feb 2023 10:13:42 -0800 Subject: [PATCH 08/10] Add overload taking casing enum directly --- .../api/Azure.Core.Experimental.net461.cs | 1 + .../api/Azure.Core.Experimental.net6.0.cs | 1 + .../api/Azure.Core.Experimental.netstandard2.0.cs | 1 + .../src/BinaryDataExtensions.cs | 8 ++++++++ .../Azure.Core.Experimental/tests/DynamicJsonTests.cs | 11 +++++++++++ 5 files changed, 22 insertions(+) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs index aa41eee892868..5d5cbc9e38812 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs @@ -111,6 +111,7 @@ 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 diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs index aa41eee892868..5d5cbc9e38812 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs @@ -111,6 +111,7 @@ 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 diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index aa41eee892868..5d5cbc9e38812 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -111,6 +111,7 @@ 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 diff --git a/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs b/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs index c1f79b968df86..78cf716765df9 100644 --- a/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs +++ b/sdk/core/Azure.Core.Experimental/src/BinaryDataExtensions.cs @@ -19,6 +19,14 @@ public static dynamic ToDynamic(this BinaryData data) return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, new DynamicJsonOptions()); } + /// + /// Return the content of the BinaryData as a dynamic type. + /// + public static dynamic ToDynamic(this BinaryData data, DynamicJsonNameMapping propertyNameCasing) + { + return new DynamicJson(MutableJsonDocument.Parse(data).RootElement, new DynamicJsonOptions() { PropertyNameCasing = propertyNameCasing }); + } + /// /// Return the content of the BinaryData as a dynamic type. /// diff --git a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs index 1b5ada82cb934..5132d2bf0bd17 100644 --- a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs @@ -696,6 +696,17 @@ public void CanSetPascalCasePascalGettersCamelSetters() Assert.AreEqual(3, (int)dynamicJson.Bar); } + [Test] + public void CanPassPropertyNameCasingEnumDirectly() + { + string json = """{ "foo" : 1 }"""; + + dynamic dynamicJson = new BinaryData(json).ToDynamic(DynamicJsonNameMapping.None); + + Assert.AreEqual(1, (int)dynamicJson.foo); + Assert.AreEqual(null, dynamicJson.Foo); + } + #region Helpers internal static dynamic GetDynamicJson(string json) { From 61e075f6ac57cbf3cf4e76b424e0bb8a99fdf798 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 28 Feb 2023 13:35:52 -0800 Subject: [PATCH 09/10] Change default case mapping --- .../src/DynamicJson.cs | 5 +++ .../src/DynamicJsonOptions.cs | 12 ++++-- .../tests/DynamicJsonTests.cs | 37 +++++-------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs index a71dca854d389..11ec432fed4a7 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJson.cs @@ -73,6 +73,11 @@ private bool CamelCaseSetters() private static string GetAsCamelCase(string value) { + if (value.Length < 2) + { + return value.ToLowerInvariant(); + } + return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; } diff --git a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs index 56bc5224a97f2..bfad8b64c0792 100644 --- a/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/DynamicJsonOptions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; - namespace Azure.Core.Dynamic { /// @@ -10,6 +8,14 @@ namespace Azure.Core.Dynamic /// public struct DynamicJsonOptions { + /// + /// Gets the default for Azure services. + /// + public static readonly DynamicJsonOptions AzureDefault = new() + { + PropertyNameCasing = DynamicJsonNameMapping.PascalCaseGettersCamelCaseSetters + }; + /// /// Creates a new instance of DynamicJsonOptions. /// @@ -18,6 +24,6 @@ public DynamicJsonOptions() { } /// /// Specifies how properties on will be accessed in the underlying JSON buffer. /// - public DynamicJsonNameMapping PropertyNameCasing { get; set; } = DynamicJsonNameMapping.PascalCaseGettersCamelCaseSetters; + public DynamicJsonNameMapping PropertyNameCasing { get; set; } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs index 5132d2bf0bd17..f4564bde2bf85 100644 --- a/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/DynamicJsonTests.cs @@ -410,7 +410,7 @@ public void CanGetCamelCasePropertyEitherCase() { string json = """{ "foo" : 1 }"""; - dynamic dynamicJson = new BinaryData(json).ToDynamic(); + dynamic dynamicJson = new BinaryData(json).ToDynamic(DynamicJsonOptions.AzureDefault); Assert.AreEqual(1, (int)dynamicJson.foo); Assert.AreEqual(1, (int)dynamicJson.Foo); @@ -421,12 +421,7 @@ public void CanGetCamelCasePropertyNoMapping() { string json = """{ "foo" : 1 }"""; - DynamicJsonOptions options = new() - { - PropertyNameCasing = DynamicJsonNameMapping.None - }; - - dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + dynamic dynamicJson = new BinaryData(json).ToDynamic(); Assert.AreEqual(1, (int)dynamicJson.foo); Assert.AreEqual(null, dynamicJson.Foo); @@ -453,8 +448,7 @@ public void CanGetCamelCasePropertyPascalGettersCamelSetters() { string json = """{ "foo" : 1 }"""; - // This is the default setting. - dynamic dynamicJson = new BinaryData(json).ToDynamic(); + dynamic dynamicJson = new BinaryData(json).ToDynamic(DynamicJsonOptions.AzureDefault); Assert.AreEqual(1, (int)dynamicJson.foo); Assert.AreEqual(1, (int)dynamicJson.Foo); @@ -465,12 +459,7 @@ public void CanGetPascalCasePropertyNoMapping() { string json = """{ "Foo" : 1 }"""; - DynamicJsonOptions options = new() - { - PropertyNameCasing = DynamicJsonNameMapping.None - }; - - dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + dynamic dynamicJson = new BinaryData(json).ToDynamic(); Assert.AreEqual(null, dynamicJson.foo); Assert.AreEqual(1, (int)dynamicJson.Foo); @@ -497,7 +486,7 @@ public void CanGetPascalCasePropertyPascalGettersCamelSetters() { string json = """{ "Foo" : 1 }"""; - dynamic dynamicJson = new BinaryData(json).ToDynamic(); + dynamic dynamicJson = new BinaryData(json).ToDynamic(DynamicJsonOptions.AzureDefault); Assert.AreEqual(null, dynamicJson.foo); Assert.AreEqual(1, (int)dynamicJson.Foo); @@ -508,11 +497,7 @@ public void CanSetCamelCaseNoMapping() { string json = """{ "foo": 1 }"""; - DynamicJsonOptions options = new() - { - PropertyNameCasing = DynamicJsonNameMapping.None - }; - dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + dynamic dynamicJson = new BinaryData(json).ToDynamic(); // Existing property access dynamicJson.foo = 2; @@ -576,7 +561,7 @@ public void CanSetCamelCasePascalGettersCamelSetters() { string json = """{ "foo": 1 }"""; - dynamic dynamicJson = new BinaryData(json).ToDynamic(); + dynamic dynamicJson = new BinaryData(json).ToDynamic(DynamicJsonOptions.AzureDefault); // Existing property access dynamicJson.foo = 2; @@ -609,11 +594,7 @@ public void CanSetPascalCaseNoMapping() { string json = """{ "Foo": 1 }"""; - DynamicJsonOptions options = new() - { - PropertyNameCasing = DynamicJsonNameMapping.None - }; - dynamic dynamicJson = new BinaryData(json).ToDynamic(options); + dynamic dynamicJson = new BinaryData(json).ToDynamic(); // This adds a new property, since it doesn't find `Foo`. dynamicJson.foo = 2; @@ -682,7 +663,7 @@ public void CanSetPascalCasePascalGettersCamelSetters() { string json = """{ "Foo": 1 }"""; - dynamic dynamicJson = new BinaryData(json).ToDynamic(); + dynamic dynamicJson = new BinaryData(json).ToDynamic(DynamicJsonOptions.AzureDefault); // Existing property access does not add a camelCase property. dynamicJson.Foo = 2; From 175eba870b1fae1f5e07b0981b359431b356fee2 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 28 Feb 2023 13:48:06 -0800 Subject: [PATCH 10/10] update API --- .../api/Azure.Core.Experimental.net461.cs | 1 + .../api/Azure.Core.Experimental.net6.0.cs | 1 + .../api/Azure.Core.Experimental.netstandard2.0.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs index 5d5cbc9e38812..4d5b00d6e6c33 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs @@ -163,6 +163,7 @@ public enum DynamicJsonNameMapping 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 { } } } diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs index 5d5cbc9e38812..4d5b00d6e6c33 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs @@ -163,6 +163,7 @@ public enum DynamicJsonNameMapping 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 { } } } diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 5d5cbc9e38812..4d5b00d6e6c33 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -163,6 +163,7 @@ public enum DynamicJsonNameMapping 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 { } } }