diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 9aaf09bb355c..ddfd7039e0dc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -1127,6 +1127,10 @@ public partial interface IModelSerializable T Deserialize(System.BinaryData data, Azure.Core.Serialization.ModelSerializerOptions options); System.BinaryData Serialize(Azure.Core.Serialization.ModelSerializerOptions options); } + public partial interface IPatchModel + { + bool HasChanges { get; } + } public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter { public JsonObjectSerializer() { } @@ -1145,6 +1149,39 @@ public enum JsonPropertyNames UseExact = 0, CamelCase = 1, } + public partial class MergePatchDictionary : Azure.Core.Serialization.IPatchModel, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public MergePatchDictionary(System.Action writeValue) { } + public MergePatchDictionary(System.Collections.Generic.Dictionary dictionary, System.Action writeValue) { } + public bool HasChanges { get { throw null; } } + public T this[string key] { get { throw null; } set { } } + int System.Collections.Generic.ICollection>.Count { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public void Add(string key, T value) { } + public void SerializePatch(System.Text.Json.Utf8JsonWriter writer) { } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.ICollection>.Clear() { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.ContainsKey(string key) { throw null; } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(string key, out T value) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct MergePatchValue + { + private T _value; + private int _dummyPrimitive; + public MergePatchValue(T value) { throw null; } + public bool HasChanged { get { throw null; } } + public T Value { get { throw null; } set { } } + public static implicit operator T (Azure.Core.Serialization.MergePatchValue value) { throw null; } + } public partial class ModelJsonConverter : System.Text.Json.Serialization.JsonConverter> { public ModelJsonConverter() { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index 87a66a878b2f..9daa093b6922 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -1128,6 +1128,10 @@ public partial interface IModelSerializable T Deserialize(System.BinaryData data, Azure.Core.Serialization.ModelSerializerOptions options); System.BinaryData Serialize(Azure.Core.Serialization.ModelSerializerOptions options); } + public partial interface IPatchModel + { + bool HasChanges { get; } + } public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter { public JsonObjectSerializer() { } @@ -1146,6 +1150,39 @@ public enum JsonPropertyNames UseExact = 0, CamelCase = 1, } + public partial class MergePatchDictionary : Azure.Core.Serialization.IPatchModel, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public MergePatchDictionary(System.Action writeValue) { } + public MergePatchDictionary(System.Collections.Generic.Dictionary dictionary, System.Action writeValue) { } + public bool HasChanges { get { throw null; } } + public T this[string key] { get { throw null; } set { } } + int System.Collections.Generic.ICollection>.Count { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public void Add(string key, T value) { } + public void SerializePatch(System.Text.Json.Utf8JsonWriter writer) { } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.ICollection>.Clear() { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.ContainsKey(string key) { throw null; } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T value) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct MergePatchValue + { + private T _value; + private int _dummyPrimitive; + public MergePatchValue(T value) { throw null; } + public bool HasChanged { get { throw null; } } + public T Value { get { throw null; } set { } } + public static implicit operator T (Azure.Core.Serialization.MergePatchValue value) { throw null; } + } public partial class ModelJsonConverter : System.Text.Json.Serialization.JsonConverter> { public ModelJsonConverter() { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs index 274566b11857..d1d0681bb8d7 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -1128,6 +1128,10 @@ public partial interface IModelSerializable T Deserialize(System.BinaryData data, Azure.Core.Serialization.ModelSerializerOptions options); System.BinaryData Serialize(Azure.Core.Serialization.ModelSerializerOptions options); } + public partial interface IPatchModel + { + bool HasChanges { get; } + } public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter { public JsonObjectSerializer() { } @@ -1146,6 +1150,39 @@ public enum JsonPropertyNames UseExact = 0, CamelCase = 1, } + public partial class MergePatchDictionary : Azure.Core.Serialization.IPatchModel, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public MergePatchDictionary(System.Action writeValue) { } + public MergePatchDictionary(System.Collections.Generic.Dictionary dictionary, System.Action writeValue) { } + public bool HasChanges { get { throw null; } } + public T this[string key] { get { throw null; } set { } } + int System.Collections.Generic.ICollection>.Count { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public void Add(string key, T value) { } + public void SerializePatch(System.Text.Json.Utf8JsonWriter writer) { } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.ICollection>.Clear() { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.ContainsKey(string key) { throw null; } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T value) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct MergePatchValue + { + private T _value; + private int _dummyPrimitive; + public MergePatchValue(T value) { throw null; } + public bool HasChanged { get { throw null; } } + public T Value { get { throw null; } set { } } + public static implicit operator T (Azure.Core.Serialization.MergePatchValue value) { throw null; } + } [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("The constructors of the type being deserialized are dynamically accessed and may be trimmed.")] public partial class ModelJsonConverter : System.Text.Json.Serialization.JsonConverter> { diff --git a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs index 9aaf09bb355c..ddfd7039e0dc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs @@ -1127,6 +1127,10 @@ public partial interface IModelSerializable T Deserialize(System.BinaryData data, Azure.Core.Serialization.ModelSerializerOptions options); System.BinaryData Serialize(Azure.Core.Serialization.ModelSerializerOptions options); } + public partial interface IPatchModel + { + bool HasChanges { get; } + } public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter { public JsonObjectSerializer() { } @@ -1145,6 +1149,39 @@ public enum JsonPropertyNames UseExact = 0, CamelCase = 1, } + public partial class MergePatchDictionary : Azure.Core.Serialization.IPatchModel, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public MergePatchDictionary(System.Action writeValue) { } + public MergePatchDictionary(System.Collections.Generic.Dictionary dictionary, System.Action writeValue) { } + public bool HasChanges { get { throw null; } } + public T this[string key] { get { throw null; } set { } } + int System.Collections.Generic.ICollection>.Count { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public void Add(string key, T value) { } + public void SerializePatch(System.Text.Json.Utf8JsonWriter writer) { } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.ICollection>.Clear() { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.ContainsKey(string key) { throw null; } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(string key, out T value) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct MergePatchValue + { + private T _value; + private int _dummyPrimitive; + public MergePatchValue(T value) { throw null; } + public bool HasChanged { get { throw null; } } + public T Value { get { throw null; } set { } } + public static implicit operator T (Azure.Core.Serialization.MergePatchValue value) { throw null; } + } public partial class ModelJsonConverter : System.Text.Json.Serialization.JsonConverter> { public ModelJsonConverter() { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index 9aaf09bb355c..ddfd7039e0dc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -1127,6 +1127,10 @@ public partial interface IModelSerializable T Deserialize(System.BinaryData data, Azure.Core.Serialization.ModelSerializerOptions options); System.BinaryData Serialize(Azure.Core.Serialization.ModelSerializerOptions options); } + public partial interface IPatchModel + { + bool HasChanges { get; } + } public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter { public JsonObjectSerializer() { } @@ -1145,6 +1149,39 @@ public enum JsonPropertyNames UseExact = 0, CamelCase = 1, } + public partial class MergePatchDictionary : Azure.Core.Serialization.IPatchModel, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public MergePatchDictionary(System.Action writeValue) { } + public MergePatchDictionary(System.Collections.Generic.Dictionary dictionary, System.Action writeValue) { } + public bool HasChanges { get { throw null; } } + public T this[string key] { get { throw null; } set { } } + int System.Collections.Generic.ICollection>.Count { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public void Add(string key, T value) { } + public void SerializePatch(System.Text.Json.Utf8JsonWriter writer) { } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.ICollection>.Clear() { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.ContainsKey(string key) { throw null; } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(string key, out T value) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct MergePatchValue + { + private T _value; + private int _dummyPrimitive; + public MergePatchValue(T value) { throw null; } + public bool HasChanged { get { throw null; } } + public T Value { get { throw null; } set { } } + public static implicit operator T (Azure.Core.Serialization.MergePatchValue value) { throw null; } + } public partial class ModelJsonConverter : System.Text.Json.Serialization.JsonConverter> { public ModelJsonConverter() { } diff --git a/sdk/core/Azure.Core/src/DynamicData/IPatchModel.cs b/sdk/core/Azure.Core/src/DynamicData/IPatchModel.cs new file mode 100644 index 000000000000..fa924826c61d --- /dev/null +++ b/sdk/core/Azure.Core/src/DynamicData/IPatchModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Core.Serialization +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public interface IPatchModel + { + public bool HasChanges { get; } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/sdk/core/Azure.Core/src/DynamicData/MergePatchDictionary.cs b/sdk/core/Azure.Core/src/DynamicData/MergePatchDictionary.cs new file mode 100644 index 000000000000..eec2b969269c --- /dev/null +++ b/sdk/core/Azure.Core/src/DynamicData/MergePatchDictionary.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Azure.Core.Serialization +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable AZC0014 // STJ + public class MergePatchDictionary : IDictionary, IPatchModel //, IModelJsonSerializable> + { + private bool _checkChanges; + private bool _checkAllChanges; + private bool _hasChanges; + public bool HasChanges => _hasChanges || CheckChanges() || CheckAllChanges(); + + private bool CheckChanges() + { + if (_checkChanges) + { + foreach (KeyValuePair item in _changed) + { + if (item.Value) + { + if (_dictionary.TryGetValue(item.Key, out T? value) && + value is IPatchModel model && + model.HasChanges) + { + return true; + } + } + } + } + + return false; + } + + private bool CheckAllChanges() + { + if (_checkAllChanges) + { + // TODO handle delete case - changed has diff set from _dictionary. + + foreach (KeyValuePair item in _dictionary) + { + if (_dictionary.TryGetValue(item.Key, out T? value) && + value is IPatchModel model && + model.HasChanges) + { + return true; + } + } + } + + return false; + } + + private readonly Dictionary _changed; + private readonly Dictionary _dictionary; + + private readonly Action _writeValue; + + public MergePatchDictionary(Action writeValue) + { + _changed = new Dictionary(); + _dictionary = new Dictionary(); + _writeValue = writeValue; + } + + /// + /// Deserialization constructor. + /// + /// + /// + public MergePatchDictionary(Dictionary dictionary, Action writeValue) : this(writeValue) + { + _changed = new Dictionary(_dictionary.Count); + _dictionary = dictionary; + } + + // TODO: implement IModel serializable? + public void SerializePatch(Utf8JsonWriter writer) + { + if (HasChanges) + { + writer.WriteStartObject(); + + foreach (KeyValuePair kvp in _changed) + { + if (kvp.Value) + { + if (!_dictionary.TryGetValue(kvp.Key, out T? value) || value == null) + { + writer.WritePropertyName(kvp.Key); + writer.WriteNullValue(); + } + else + { + if (value is not IPatchModel model || model.HasChanges) + { + writer.WritePropertyName(kvp.Key); + _writeValue(writer, value); + } + } + } + } + + writer.WriteEndObject(); + } + } + + public T this[string key] + { + get + { + // If the value is read and a reference value, it might get changed + _checkChanges |= _dictionary[key] is IPatchModel; + _changed[key] = _dictionary[key] is IPatchModel; + return _dictionary[key]; + } + + set + { + _hasChanges = true; + _changed[key] = true; + _dictionary[key] = value; + } + } + + ICollection IDictionary.Keys => _dictionary.Keys; + + ICollection IDictionary.Values => _dictionary.Values; + + int ICollection>.Count => _dictionary.Count; + + bool ICollection>.IsReadOnly => false; + + public void Add(string key, T value) + { + _hasChanges = true; + _changed[key] = true; + _dictionary.Add(key, value); + } + + void ICollection>.Add(KeyValuePair item) + { + _hasChanges = true; + _changed[item.Key] = true; + (_dictionary as ICollection>).Add(item); + } + + void ICollection>.Clear() + { + _hasChanges = true; + foreach (string key in _dictionary.Keys) + { + _changed[key] = true; + } + + (_dictionary as ICollection>).Clear(); + } + + bool ICollection>.Contains(KeyValuePair item) => + (_dictionary as ICollection>).Contains(item); + + bool IDictionary.ContainsKey(string key) + => _dictionary.ContainsKey(key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + => (_dictionary as ICollection>).CopyTo(array, arrayIndex); + + IEnumerator> IEnumerable>.GetEnumerator() + { + _checkChanges = true; // IPatchModel + _checkAllChanges = true; + return _dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + _checkChanges = true; // IPatchModel + _checkAllChanges = true; + return (_dictionary as IEnumerable).GetEnumerator(); + } + + bool IDictionary.Remove(string key) + { + _hasChanges = true; + _changed[key] = true; + return _dictionary.Remove(key); + } + + bool ICollection>.Remove(KeyValuePair item) + { + _hasChanges = true; + _changed[item.Key] = true; + return (_dictionary as ICollection>).Remove(item); + } + +#if NET5_0_OR_GREATER + public bool TryGetValue(string key, [MaybeNullWhen(false)] out T value) +#else + public bool TryGetValue(string key, out T value) +#endif + { + if (_dictionary.TryGetValue(key, out value)) + { + _checkChanges |= value is IPatchModel; + _changed[key] = value is IPatchModel; + return true; + } + + return false; + } + + //public void IModelJsonSerializable>.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options) + //{ + // // TODO: PatchModelHelper.ValidateFormat(this, options.Format); + + // switch (options.Format.ToString()) + // { + // case "J": + // case "W": + // SerializeFull(writer); + // break; + // case "P": + // SerializePatch(writer); + // break; + // default: + // // Exception was thrown by ValidateFormat. + // break; + // } + //} + + //public BinaryData IModelSerializable>.Serialize(ModelSerializerOptions options) + //{ + // // TODO: PatchModelHelper.ValidateFormat(this, options.Format); + // return ModelSerializer.SerializeCore(this, options); + //} + + //public MergePatchDictionary IModelJsonSerializable>.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + //{ + // // TODO: PatchModelHelper.ValidateFormat(this, options.Format); + // return Deserialize(ref reader, options); + //} + + //public MergePatchDictionary IModelSerializable>.Deserialize(BinaryData data, ModelSerializerOptions options) + //{ + // // TODO: PatchModelHelper.ValidateFormat(this, options.Format); + // return Deserialize(data, options); + //} + } +#pragma warning restore AZC0014 +#pragma warning restore CS1591 +} diff --git a/sdk/core/Azure.Core/src/DynamicData/MergePatchValue.cs b/sdk/core/Azure.Core/src/DynamicData/MergePatchValue.cs new file mode 100644 index 000000000000..f0f6410338b3 --- /dev/null +++ b/sdk/core/Azure.Core/src/DynamicData/MergePatchValue.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core.Serialization +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public struct MergePatchValue + { + private bool _changed; + private T _value; + + public MergePatchValue(T value) + { + _value = value; + } + + public static implicit operator T(MergePatchValue value) + { + return value.Value; + } + + public T Value + { + get => _value; + set + { + _changed = true; + _value = value; + } + } + + public bool HasChanged => _changed; + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.Serialization.cs new file mode 100644 index 000000000000..aee800dd74c0 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.Serialization.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + public partial class ChildPatchModel : IModelJsonSerializable, IUtf8JsonSerializable + { + internal static ChildPatchModel Deserialize(JsonElement element) + { + string a = default; + string b = default; + + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.NameEquals("a")) + { + a = property.Value.GetString(); + continue; + } + + if (property.NameEquals("b")) + { + b = property.Value.GetString(); + continue; + } + } + + return new ChildPatchModel(a, b); + } + + ChildPatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(ref reader, options); + } + + private static ChildPatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.ParseValue(ref reader).RootElement; + return Deserialize(element); + } + + ChildPatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(data, options); + } + + private static ChildPatchModel Deserialize(BinaryData data, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.Parse(data).RootElement; + return Deserialize(element); + } + + internal void SerializeFull(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WritePropertyName("a"); + writer.WriteStringValue(A); + + writer.WritePropertyName("b"); + writer.WriteStringValue(B); + + writer.WriteEndObject(); + } + + internal void SerializePatch(Utf8JsonWriter writer) + { + if (HasChanges) + { + writer.WriteStartObject(); + + if (_a.HasChanged) + { + writer.WritePropertyName("a"); + writer.WriteStringValue(A); + } + + if (_b.HasChanged) + { + writer.WritePropertyName("b"); + writer.WriteStringValue(B); + } + + writer.WriteEndObject(); + } + } + + void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + switch (options.Format.ToString()) + { + case "J": + case "W": + SerializeFull(writer); + break; + case "P": + SerializePatch(writer); + break; + default: + // Exception was thrown by ValidateFormat. + break; + } + } + + BinaryData IModelSerializable.Serialize(ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return ModelSerializer.SerializeCore(this, options); + } + + public static implicit operator RequestContent(ChildPatchModel model) + => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions); + + public static explicit operator ChildPatchModel(Response response) + { + Argument.AssertNotNull(response, nameof(response)); + return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions); + } + + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.cs new file mode 100644 index 000000000000..8248ff2dafa5 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + /// + /// This model illustrates a nested child model in a parent model. + /// + public partial class ChildPatchModel: IPatchModel + { + /// + /// Serialization constructor. + /// + internal ChildPatchModel() + { + } + + /// Deserialization constructor. + internal ChildPatchModel(string a, string b) + { + _a = new MergePatchValue(a); + _b = new MergePatchValue(b); + } + + public bool HasChanges => _a.HasChanged || _b.HasChanged; + + private MergePatchValue _a; + /// + /// Optional string property corresponding to JSON """{"a": "aaa"}""". + /// + public string A + { + get => _a; + set => _a.Value = value; + } + + private MergePatchValue _b; + /// + /// Optional string property corresponding to JSON """{"b": "bbb"}""". + /// + public string B + { + get => _b; + set => _b.Value = value; + } + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModelTests.cs new file mode 100644 index 000000000000..eeeb7a56a74c --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModelTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Serialization; +using Azure.Core.Tests.PatchModels; +using NUnit.Framework; + +namespace Azure.Core.Tests.Public.ModelSerializationTests +{ + internal class ChildPatchModelTests : ModelJsonTests + { + protected override string JsonPayload => """ + { + "a": "aa", "b": "bb" + } + """; + + protected override string WirePayload => JsonPayload; + + protected override Func ToRequestContent => m => m == null ? null : RequestContent.Create(m, ModelSerializerOptions.DefaultWireOptions); + + protected override Func FromResponse => r => (ChildPatchModel)r; + + protected override void CompareModels(ChildPatchModel model, ChildPatchModel model2, ModelSerializerFormat format) + { + Assert.AreEqual(model.A, model2.A); + Assert.AreEqual(model.B, model2.B); + } + + protected override string GetExpectedResult(ModelSerializerFormat format) + { + return RemoveWhitespace(JsonPayload); + } + + protected override void VerifyModel(ChildPatchModel model, ModelSerializerFormat format) + { + Assert.AreEqual("aa", model.A); + Assert.AreEqual("bb", model.B); + } + + private static string RemoveWhitespace(string value) => value.Replace("\r", "").Replace("\n", "").Replace(" ", ""); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/CollectionPatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/CollectionPatchModel.Serialization.cs new file mode 100644 index 000000000000..f40623c451cd --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/CollectionPatchModel.Serialization.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + public partial class CollectionPatchModel : IModelJsonSerializable, IUtf8JsonSerializable + { + internal static CollectionPatchModel Deserialize(JsonElement element) + { + string id = default; + Dictionary variables = default; + Dictionary children = default; + + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.NameEquals("id")) + { + id = property.Value.GetString(); + continue; + } + + if (property.NameEquals("variables")) + { + variables = new(); + foreach (JsonProperty value in property.Value.EnumerateObject()) + { + variables.Add(value.Name, value.Value.GetString()); + } + } + + if (property.NameEquals("children")) + { + children = new(); + foreach (JsonProperty value in property.Value.EnumerateObject()) + { + ChildPatchModel child = ChildPatchModel.Deserialize(value.Value); + children.Add(value.Name, child); + } + } + } + + return new CollectionPatchModel(id, + new MergePatchDictionary(variables, (w, s) => w.WriteStringValue(s)), + new MergePatchDictionary(children, (w, m) => m.SerializePatch(w))); + } + + CollectionPatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(ref reader, options); + } + + private static CollectionPatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.ParseValue(ref reader).RootElement; + return Deserialize(element); + } + + CollectionPatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(data, options); + } + + private static CollectionPatchModel Deserialize(BinaryData data, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.Parse(data).RootElement; + return Deserialize(element); + } + + private void SerializeFull(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WritePropertyName("id"); + writer.WriteStringValue(Id); + + writer.WritePropertyName("variables"); + + writer.WriteStartObject(); + foreach (KeyValuePair item in Variables) + { + writer.WritePropertyName(item.Key); + writer.WriteStringValue(item.Value); + } + writer.WriteEndObject(); + + // TODO + // Child.SerializeFull(writer); + // The dictionary could know how to serialize itself and its patch + // as an IChangeWriteable. + + writer.WriteEndObject(); + } + + private void SerializePatch(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + if (_id.HasChanged) + { + writer.WritePropertyName("id"); + writer.WriteStringValue(Id); + } + + if (_variables != null && _variables.HasChanges) + { + writer.WritePropertyName("variables"); + _variables.SerializePatch(writer); + } + + if (_children != null && _children.HasChanges) + { + writer.WritePropertyName("children"); + _children.SerializePatch(writer); + } + + writer.WriteEndObject(); + } + + void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + switch (options.Format.ToString()) + { + case "J": + case "W": + SerializeFull(writer); + break; + case "P": + SerializePatch(writer); + break; + default: + // Exception was thrown by ValidateFormat. + break; + } + } + + BinaryData IModelSerializable.Serialize(ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return ModelSerializer.SerializeCore(this, options); + } + + public static implicit operator RequestContent(CollectionPatchModel model) + => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions); + + public static explicit operator CollectionPatchModel(Response response) + { + Argument.AssertNotNull(response, nameof(response)); + return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions); + } + + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/CollectionPatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/CollectionPatchModel.cs new file mode 100644 index 000000000000..92f2538b22de --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/CollectionPatchModel.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + /// + /// This model illustrates collection properties on Patch models. + /// + public partial class CollectionPatchModel + { + /// + /// Public constructor. + /// + public CollectionPatchModel() + { + } + + /// + /// Serialization constructor. + /// + internal CollectionPatchModel(string id, MergePatchDictionary variables, MergePatchDictionary children) + { + _id = new MergePatchValue(id); + _variables = variables; + _children = children; + } + + private MergePatchValue _id; + /// + /// Optional string property corresponding to JSON """{"id": "abc"}""". + /// + public string Id + { + get => _id; + set => _id.Value = value; + } + + private MergePatchDictionary _variables; + /// Environment variables which are defined as a set of <name,value> pairs. + public IDictionary Variables + { + get + { + _variables ??= new MergePatchDictionary((w, s) => w.WriteStringValue(s)); + return _variables; + } + } + + private MergePatchDictionary _children; + /// + public IDictionary Children + { + get + { + _children ??= new MergePatchDictionary((w, m) => m.SerializePatch(w)); + return _children; + } + } + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.Serialization.cs new file mode 100644 index 000000000000..6715fb52643f --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.Serialization.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + public partial class ParentPatchModel : IModelJsonSerializable, IUtf8JsonSerializable + { + internal static ParentPatchModel Deserialize(JsonElement element) + { + string id = default; + ChildPatchModel child = default; + + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.NameEquals("id")) + { + id = property.Value.GetString(); + continue; + } + + if (property.NameEquals("child")) + { + child = ChildPatchModel.Deserialize(property.Value); + continue; + } + } + + return new ParentPatchModel(id, child); + } + + ParentPatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(ref reader, options); + } + + private static ParentPatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.ParseValue(ref reader).RootElement; + return Deserialize(element); + } + + ParentPatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(data, options); + } + + private static ParentPatchModel Deserialize(BinaryData data, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.Parse(data).RootElement; + return Deserialize(element); + } + + private void SerializeFull(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WritePropertyName("id"); + writer.WriteStringValue(Id); + + writer.WritePropertyName("child"); + Child.SerializeFull(writer); + + writer.WriteEndObject(); + } + + private void SerializePatch(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + if (_id.HasChanged) + { + writer.WritePropertyName("id"); + writer.WriteStringValue(Id); + } + + if (_child.HasChanged) + { + writer.WritePropertyName("child"); + if (Child == null) + { + writer.WriteNullValue(); + } + else + { + Child.SerializeFull(writer); + } + } + else if (_child.Value.HasChanges) + { + writer.WritePropertyName("child"); + Child.SerializePatch(writer); + } + + writer.WriteEndObject(); + } + + void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + switch (options.Format.ToString()) + { + case "J": + case "W": + SerializeFull(writer); + break; + case "P": + SerializePatch(writer); + break; + default: + // Exception was thrown by ValidateFormat. + break; + } + } + + BinaryData IModelSerializable.Serialize(ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return ModelSerializer.SerializeCore(this, options); + } + + public static implicit operator RequestContent(ParentPatchModel model) + => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions); + + public static explicit operator ParentPatchModel(Response response) + { + Argument.AssertNotNull(response, nameof(response)); + return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions); + } + + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.cs new file mode 100644 index 000000000000..a6fb27502cd3 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + /// + /// This model illustrates a patch model with properties that are nested models. + /// + public partial class ParentPatchModel + { + /// + /// Public constructor. + /// + public ParentPatchModel() + { + } + + /// + /// Serialization constructor. + /// + internal ParentPatchModel(string id, ChildPatchModel child) + { + _id = new MergePatchValue(id); + _child = new MergePatchValue(child); + } + + private MergePatchValue _id; + /// + /// Optional string property corresponding to JSON """{"id": "abc"}""". + /// + public string Id + { + get => _id; + set => _id.Value = value; + } + + private MergePatchValue _child; + /// + /// Optional ChildPatchModel property corresponding to JSON """{"child": {"a":"aa", "b": "bb"}}""". + /// + public ChildPatchModel Child + { + get + { + if (_child.Value == null && !_child.HasChanged) + { + _child = new MergePatchValue(new ChildPatchModel()); + } + + return _child; + } + set => _child.Value = value; + } + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModelTests.cs new file mode 100644 index 000000000000..a0469e6e640e --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModelTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Serialization; +using Azure.Core.Tests.PatchModels; +using NUnit.Framework; + +namespace Azure.Core.Tests.Public.ModelSerializationTests +{ + internal class ParentPatchModelTests : ModelJsonTests + { + protected override string JsonPayload => """ + { + "id": "abc", + "child": {"a": "aa", "b": "bb"} + } + """; + + protected override string WirePayload => JsonPayload; + + protected override Func ToRequestContent => m => m == null ? null: RequestContent.Create(m, ModelSerializerOptions.DefaultWireOptions); + + protected override Func FromResponse => r => (ParentPatchModel)r; + + protected override void CompareModels(ParentPatchModel model, ParentPatchModel model2, ModelSerializerFormat format) + { + Assert.AreEqual(model.Child.A, model2.Child.A); + Assert.AreEqual(model.Child.B, model2.Child.B); + Assert.AreEqual(model.Id, model2.Id); + } + + protected override string GetExpectedResult(ModelSerializerFormat format) + { + return RemoveWhitespace(JsonPayload); + } + + protected override void VerifyModel(ParentPatchModel model, ModelSerializerFormat format) + { + Assert.AreEqual("aa", model.Child.A); + Assert.AreEqual("bb", model.Child.B); + Assert.AreEqual("abc", model.Id); + } + + private static string RemoveWhitespace(string value) => value.Replace("\r", "").Replace("\n", "").Replace(" ", ""); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelHelper.cs b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelHelper.cs new file mode 100644 index 000000000000..5d538cfcf878 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelHelper.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + internal class PatchModelHelper + { + // TODO: Move this into the public interface as a separate PR + public static void ValidateFormat(IModelSerializable model, ModelSerializerFormat format) + { + bool isValidPatchFormat = model is IModelJsonSerializable && format == "P"; + if (!isValidPatchFormat) + { + ModelSerializerHelper.ValidateFormat(model, format); + } + } + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelTests.cs new file mode 100644 index 000000000000..9b86c706bc2a --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelTests.cs @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Text.Json; +using Azure.Core.Serialization; +using Azure.Core.Tests.PatchModels; +using NUnit.Framework; + +namespace Azure.Core.Tests.Public +{ + public class PatchModelTests + { + #region SimplePatchModel + [Test] + public void CanPatchIntProperty() + { + SimplePatchModel model = new(); + model.Count = 2; + + ValidatePatch("""{"count":2}""", model); + } + + [Test] + public void CanPatchStringProperty() + { + SimplePatchModel model = new(); + model.Name = "abc"; + + ValidatePatch("""{"name":"abc"}""", model); + } + + [Test] + public void CanPatchDateTimeProperty() + { + DateTimeOffset updateTime = DateTimeOffset.Parse("2023-10-19T10:19:10.0190001Z"); + + SimplePatchModel model = new(); + model.UpdatedOn = updateTime; + + // TODO: fix test + Assert.AreEqual($"{{\"updatedOn\":\"{updateTime.UtcDateTime:O}\"}}", GetPatchJson(model)); + //ValidatePatch($"{{\"updatedOn\":\"{updateTime:O}\"}}", model); + } + + [Test] + public void CanRoundTripSimpleModel() + { + BinaryData json = BinaryData.FromString(""" + { + "name": "abc", + "count": 1 + } + """); + + SimplePatchModel model = ModelSerializer.Deserialize(json); + + Assert.AreEqual(1, model.Count); + Assert.AreEqual("abc", model.Name); + + model.Name = "xyz"; + model.Count = 2; + + ValidatePatch("""{"name":"xyz", "count":2}""", model); + } + #endregion + + #region NestedPatchModel + [Test] + public void CanPatchNestedModel() + { + ParentPatchModel model = new(); + + model.Child.B = "bb"; + + ValidatePatch("""{"child": {"b": "bb"}}""", model); + + model.Child.A = "aa"; + + ValidatePatch("""{"child": {"a": "aa", "b": "bb"}}""", model); + } + + [Test] + public void CanPatchNestedModel_DeleteChild() + { + ParentPatchModel model = new(); + model.Child = null; + + ValidatePatch("""{"child": null}""", model); + } + + [Test] + public void CanPatchNestedModelOneProperty() + { + ParentPatchModel model = new(); + + model.Child.A = "aa"; + + ValidatePatch("""{"child": {"a": "aa"}}""", model); + } + + [Test] + public void CanPatchNestedModelOnePropertyAndChangeIt() + { + ParentPatchModel model = new(); + + model.Child.A = "a1"; + + ValidatePatch("""{"child": {"a": "a1"}}""", model); + + model.Child.A = "a2"; + + ValidatePatch("""{"child": {"a": "a2"}}""", model); + } + + [Test] + public void CanPatchNestedModelInterleaveChanges() + { + ParentPatchModel model = new(); + + model.Id = "id1"; + model.Child.B = "b1"; + model.Child.A = "a1"; + + ValidatePatch("""{"id": "id1", "child": {"a": "a1", "b": "b1"}}""", model); + + model.Child.A = "a2"; + model.Id = "id2"; + + ValidatePatch("""{"id": "id2", "child": {"a": "a2", "b": "b1"}}""", model); + + model.Child.A = null; + model.Child.B = null; + + ValidatePatch("""{"id": "id2", "child": {"a": null, "b": null}}""", model); + } + + [Test] + public void CanRoundTripNestedModel() + { + BinaryData json = BinaryData.FromString(""" + { + "id": "123", + "child": { + "a": "aa", + "b": "bb" + } + } + """); + + ParentPatchModel model = ModelSerializer.Deserialize(json); + + Assert.AreEqual("123", model.Id); + Assert.AreEqual("aa", model.Child.A); + Assert.AreEqual("bb", model.Child.B); + + ValidateSerialize("""{"id": "123", "child": {"a": "aa", "b": "bb"}}""", model); + + // TODO: "{}" or "" ? Either is doable. + ValidatePatch("{}", model); + + model.Child.A = "a2"; + model.Child.B = null; + + ValidateSerialize("""{"id": "123", "child": {"a": "a2", "b": null}}""", model); + ValidatePatch("""{"child": {"a": "a2", "b": null}}""", model); + } + #endregion + + #region RoundTripPatchModel + [Test] + public void CanPatchInputOutputPatchProperty() + { + RoundTripPatchModel model = new("abc"); + model.Value = 1; + + Assert.AreEqual("abc", model.Id); + Assert.AreEqual(1, model.Value); + + ValidatePatch("""{"value":1}""", model); + } + + [Test] + public void CanRoundTripInputOutputPatchModel() + { + BinaryData json = BinaryData.FromString(""" + { + "id": "abc", + "value": 1 + } + """); + + RoundTripPatchModel model = ModelSerializer.Deserialize(json); + + Assert.AreEqual("abc", model.Id); + Assert.AreEqual(1, model.Value); + + ValidateSerialize("""{"id": "abc", "value": 1}""", model); + ValidatePatch("{}", model); + + model.Value = 2; + + ValidateSerialize("""{"id": "abc", "value": 2}""", model); + ValidatePatch("""{"value": 2}""", model); + } + #endregion + + #region CollectionPatchModel + [Test] + public void CanPatchCollectionProperty() + { + CollectionPatchModel model = new(); + model.Variables["abc"] = "123"; + model.Variables["xyz"] = "456"; + + ValidatePatch("""{"variables": {"abc":"123", "xyz":"456"}}""", model); + } + + [Test] + public void CanPatchCollectionProperty_DeleteItem() + { + CollectionPatchModel model = new(); + model.Variables["abc"] = "123"; + model.Variables.Remove("abc"); + + ValidatePatch("""{"variables": {"abc": null}}""", model); + } + + [Test] + public void CanPatchCollectionProperty_ClearItems() + { + BinaryData json = BinaryData.FromString(""" + { + "id": "abc", + "variables": + { + "a": "aa", + "b": "bb" + } + } + """); + + CollectionPatchModel model = ModelSerializer.Deserialize(json); + model.Variables.Clear(); + + ValidatePatch("""{"variables": {"a": null, "b":null}}""", model); + } + + [Test] + public void CanRoundTripCollectionPatchModel() + { + BinaryData json = BinaryData.FromString(""" + { + "id": "abc", + "variables": + { + "a": "aa", + "b": "bb" + } + } + """); + + CollectionPatchModel model = ModelSerializer.Deserialize(json); + + Assert.AreEqual("abc", model.Id); + Assert.AreEqual("aa", model.Variables["a"]); + Assert.AreEqual("bb", model.Variables["b"]); + + ValidateSerialize("""{"id": "abc","variables":{"a": "aa","b": "bb"}}""", model); + ValidatePatch("{}", model); + + model.Variables["a"] = "a2"; + + ValidateSerialize("""{"id": "abc","variables":{"a": "a2","b": "bb"}}""", model); + ValidatePatch("""{"variables": {"a":"a2"}}""", model); + + model.Variables.Remove("b"); + + ValidateSerialize("""{"id": "abc","variables":{"a": "a2"}}""", model); + ValidatePatch("""{"variables": {"a":"a2", "b": null}}""", model); + } + + [Test] + public void CanRoundTripCollectionPatchModel_ChangeChildren() + { + BinaryData json = BinaryData.FromString(""" + { + "id": "abc", + "variables": + { + "a": "aa", + "b": "bb" + }, + "children" : + { + "first": + { + "a": "a1", + "b": "b1" + }, + "second": + { + "a": "a2", + "b": "b2" + } + } + } + """); + + CollectionPatchModel model = ModelSerializer.Deserialize(json); + + Assert.AreEqual("abc", model.Id); + Assert.AreEqual("aa", model.Variables["a"]); + Assert.AreEqual("bb", model.Variables["b"]); + + Assert.AreEqual("a1", model.Children["first"].A); + Assert.AreEqual("b1", model.Children["first"].B); + + Assert.AreEqual("a2", model.Children["second"].A); + Assert.AreEqual("b2", model.Children["second"].B); + + //ValidateSerialize("""{"id": "abc","variables":{"a": "aa","b": "bb"}}""", model); + ValidatePatch("{}", model); + + model.Children["first"].A = "a11"; + + //ValidateSerialize("""{"id": "abc","variables":{"a": "a2","b": "bb"}}""", model); + ValidatePatch("""{"children": {"first":{"a":"a11"}}}""", model); + + //model.Variables.Remove("b"); + + //ValidateSerialize("""{"id": "abc","variables":{"a": "a2"}}""", model); + //ValidatePatch("""{"variables": {"a":"a2", "b": null}}""", model); + } + #endregion + + #region Helpers + private static void ValidateSerialize(string expected, IModelJsonSerializable model) + { + using Stream stream = new MemoryStream(); + using Utf8JsonWriter writer = new(stream); + model.Serialize(writer, new ModelSerializerOptions("J")); + writer.Flush(); + stream.Position = 0; + + string actual = BinaryData.FromStream(stream).ToString(); + + AreEqualJson(expected, actual); + } + + private string GetPatchJson(IModelJsonSerializable model) + { + using Stream stream = new MemoryStream(); + using Utf8JsonWriter writer = new(stream); + model.Serialize(writer, new ModelSerializerOptions("P")); + writer.Flush(); + stream.Position = 0; + + return BinaryData.FromStream(stream).ToString(); + } + + private static void ValidatePatch(string expected, IModelJsonSerializable model) + { + using Stream stream = new MemoryStream(); + using Utf8JsonWriter writer = new(stream); + model.Serialize(writer, new ModelSerializerOptions("P")); + writer.Flush(); + stream.Position = 0; + + string actual = BinaryData.FromStream(stream).ToString(); + + if (expected.Length == 0) + { + Assert.AreEqual(expected, actual); + return; + } + + AreEqualJson(expected, actual); + } + + private static void AreEqualJson(string expected, string actual) + { + JsonDocument doc = JsonDocument.Parse(expected); + + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + doc.WriteTo(writer); + writer.Flush(); + stream.Position = 0; + BinaryData buffer = BinaryData.FromStream(stream); + + Assert.AreEqual(buffer.ToString(), actual); + } + #endregion + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModel.Serialization.cs new file mode 100644 index 000000000000..3b63234d6dcc --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModel.Serialization.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + public partial class RoundTripPatchModel : IModelJsonSerializable, IUtf8JsonSerializable + { + private static RoundTripPatchModel Deserialize(JsonElement element) + { + string id = default; + int value = default; + + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.NameEquals("id")) + { + id = property.Value.GetString(); + continue; + } + + if (property.NameEquals("value")) + { + value = property.Value.GetInt32(); + continue; + } + } + + return new RoundTripPatchModel(id, value); + } + + RoundTripPatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + return Deserialize(ref reader, options); + } + + private static RoundTripPatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.ParseValue(ref reader).RootElement; + return Deserialize(element); + } + + RoundTripPatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + return Deserialize(data, options); + } + + private static RoundTripPatchModel Deserialize(BinaryData data, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.Parse(data).RootElement; + return Deserialize(element); + } + + private void SerializeFull(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WritePropertyName("id"); + writer.WriteStringValue(Id); + + writer.WritePropertyName("value"); + if (Value is null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteNumberValue(Value.Value); + } + + writer.WriteEndObject(); + } + + private void SerializePatch(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + // Id isn't modifiable. + + if (_valuePatchFlag) + { + writer.WritePropertyName("value"); + if (Value is null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteNumberValue(Value.Value); + } + } + + writer.WriteEndObject(); + } + + void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + switch (options.Format.ToString()) + { + case "J": + case "W": + SerializeFull(writer); + break; + case "P": + SerializePatch(writer); + break; + default: + // Exception was thrown by ValidateFormat. + break; + } + } + + BinaryData IModelSerializable.Serialize(ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + return ModelSerializer.SerializeCore(this, options); + } + + public static implicit operator RequestContent(RoundTripPatchModel model) + => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions); + + public static explicit operator RoundTripPatchModel(Response response) + { + Argument.AssertNotNull(response, nameof(response)); + + return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions); + } + + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModel.cs new file mode 100644 index 000000000000..382f2673d40f --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModel.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core.Tests.PatchModels +{ + /// + /// This model illustrates a model that can be used for both GET and PATCH operations. + /// + /// For GET operations, a model is an output model, as described by https://github.com/Azure/autorest.csharp/issues/2341 + /// For PATCH operations, a model is an input model, as described by https://github.com/Azure/autorest.csharp/issues/2339 + /// For both, a model is a round-trip model, as described by https://github.com/Azure/autorest.csharp/issues/2463 + /// + public partial class RoundTripPatchModel + { + /// + /// Public constructor. + /// + public RoundTripPatchModel(string id) + { + _id = id; + } + + /// + /// Deserialization constructor. + /// + internal RoundTripPatchModel() { } + + /// + /// Serialization constructor. + /// + internal RoundTripPatchModel(string id, int value) + { + _id = id; + _value = value; + } + + private string _id; + /// + /// Required and read-only string property corresponding to JSON """{"id": "abc"}""". + /// + public string Id => _id; + + private int? _value; + private bool _valuePatchFlag; + /// + /// Optional read/write int property corresponding to JSON """{"value": 1}""". + /// + public int? Value + { + get => _value; + set + { + _value = value; + _valuePatchFlag = true; + } + } +#pragma warning restore AZC0020 // Avoid using banned types in libraries + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModelTests.cs new file mode 100644 index 000000000000..6a2925f47420 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/RoundTripPatchModelTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Serialization; +using Azure.Core.Tests.PatchModels; +using NUnit.Framework; + +namespace Azure.Core.Tests.Public.ModelSerializationTests +{ + internal class RoundTripPatchModelTests : ModelJsonTests + { + protected override string JsonPayload => """ + { + "id": "abc", + "value": 1 + } + """; + + protected override string WirePayload => JsonPayload; + + protected override Func ToRequestContent => m => m == null ? null: RequestContent.Create(m, ModelSerializerOptions.DefaultWireOptions); + + protected override Func FromResponse => r => (RoundTripPatchModel)r; + + protected override void CompareModels(RoundTripPatchModel model, RoundTripPatchModel model2, ModelSerializerFormat format) + { + Assert.AreEqual(model.Id, model2.Id); + Assert.AreEqual(model.Value, model2.Value); + } + + protected override string GetExpectedResult(ModelSerializerFormat format) + { + return RemoveWhitespace(JsonPayload); + } + + protected override void VerifyModel(RoundTripPatchModel model, ModelSerializerFormat format) + { + Assert.AreEqual("abc", model.Id); + Assert.AreEqual(1, model.Value); + } + + private static string RemoveWhitespace(string value) => value.Replace("\r", "").Replace("\n", "").Replace(" ", ""); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.Serialization.cs new file mode 100644 index 000000000000..87f5ed747857 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.Serialization.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + public partial class SimplePatchModel : IModelJsonSerializable, IUtf8JsonSerializable + { + private static SimplePatchModel Deserialize(JsonElement element) + { + string name = default; + int count = default; + DateTimeOffset updatedOn = default; + + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.NameEquals("name")) + { + name = property.Value.GetString(); + continue; + } + + if (property.NameEquals("count")) + { + count = property.Value.GetInt32(); + continue; + } + + if (property.NameEquals("updatedOn")) + { + updatedOn = property.Value.GetDateTimeOffset("O"); + continue; + } + } + + return new SimplePatchModel(name, count, updatedOn); + } + + SimplePatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(ref reader, options); + } + + private static SimplePatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.ParseValue(ref reader).RootElement; + return Deserialize(element); + } + + SimplePatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return Deserialize(data, options); + } + + private static SimplePatchModel Deserialize(BinaryData data, ModelSerializerOptions options) + { + JsonElement element = JsonDocument.Parse(data).RootElement; + return Deserialize(element); + } + + private void SerializeFull(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WritePropertyName("name"); + writer.WriteStringValue(Name); + + writer.WritePropertyName("count"); + writer.WriteNumberValue(Count); + + writer.WritePropertyName("updatedOn"); + writer.WriteStringValue(UpdatedOn, "O"); + + writer.WriteEndObject(); + } + + private void SerializePatch(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + if (_name.HasChanged) + { + writer.WritePropertyName("name"); + writer.WriteStringValue(Name); + } + + if (_count.HasChanged) + { + writer.WritePropertyName("count"); + writer.WriteNumberValue(Count); + } + + if (_updatedOn.HasChanged) + { + writer.WritePropertyName("updatedOn"); + writer.WriteStringValue(UpdatedOn, "O"); + } + + writer.WriteEndObject(); + } + + void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + + switch (options.Format.ToString()) + { + case "J": + case "W": + SerializeFull(writer); + break; + case "P": + SerializePatch(writer); + break; + default: + // Exception was thrown by ValidateFormat. + break; + } + } + + BinaryData IModelSerializable.Serialize(ModelSerializerOptions options) + { + PatchModelHelper.ValidateFormat(this, options.Format); + return ModelSerializer.SerializeCore(this, options); + } + + public static implicit operator RequestContent(SimplePatchModel model) + => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions); + + public static explicit operator SimplePatchModel(Response response) + { + Argument.AssertNotNull(response, nameof(response)); + return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions); + } + + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions); + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.cs new file mode 100644 index 000000000000..86bee4c2b78e --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Serialization; + +namespace Azure.Core.Tests.PatchModels +{ + /// + /// This model illustrates optional read/write "primitive" properties. + /// + public partial class SimplePatchModel + { + /// + /// Public constructor. + /// + public SimplePatchModel() + { + } + + /// + /// Serialization constructor. + /// + /// + internal SimplePatchModel(string name, int count, DateTimeOffset updatedOn) + { + _name = new MergePatchValue(name); + _count = new MergePatchValue(count); + _updatedOn = new MergePatchValue(updatedOn); + } + + private MergePatchValue _name; + /// + /// Optional string property corresponding to JSON """{"name": "abc"}""". + /// + public string Name + { + get => _name; + set => _name.Value = value; + } + + private MergePatchValue _count; + /// + /// Optional int property corresponding to JSON """{"count": 1}""". + /// + public int Count + { + get => _count; + set => _count.Value = value; + } + + private MergePatchValue _updatedOn; + /// + /// Optional DateTimeOffset property corresponding to JSON """{"updatedOn": "2020-06-25T17:44:37.6830000Z"}""". + /// + public DateTimeOffset UpdatedOn + { + get => _updatedOn; + set => _updatedOn.Value = value; + } + } +} diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModelTests.cs new file mode 100644 index 000000000000..1e846b7b2925 --- /dev/null +++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModelTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Serialization; +using Azure.Core.Tests.PatchModels; +using NUnit.Framework; + +namespace Azure.Core.Tests.Public.ModelSerializationTests +{ + internal class SimplePatchModelTests : ModelJsonTests + { + protected override string JsonPayload => """ + { + "name": "abc", + "count": 1, + "updatedOn": "2023-10-19T10:19:10.0190001Z" + } + """; + + protected override string WirePayload => JsonPayload; + + protected override Func ToRequestContent => m => m == null ? null: RequestContent.Create(m, ModelSerializerOptions.DefaultWireOptions); + + protected override Func FromResponse => r => (SimplePatchModel)r; + + protected override void CompareModels(SimplePatchModel model, SimplePatchModel model2, ModelSerializerFormat format) + { + Assert.AreEqual(model.Name, model2.Name); + Assert.AreEqual(model.Count, model2.Count); + Assert.AreEqual(model.UpdatedOn, model2.UpdatedOn); + } + + protected override string GetExpectedResult(ModelSerializerFormat format) + { + return RemoveWhitespace(JsonPayload); + } + + protected override void VerifyModel(SimplePatchModel model, ModelSerializerFormat format) + { + Assert.AreEqual("abc", model.Name); + Assert.AreEqual(1, model.Count); + Assert.AreEqual(DateTimeOffset.Parse("2023-10-19T10:19:10.0190001Z"), model.UpdatedOn); + } + + private static string RemoveWhitespace(string value) => value.Replace("\r", "").Replace("\n", "").Replace(" ", ""); + } +}