diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 75b0aaacbd42..fc23ec5fd441 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -153,6 +153,7 @@ "referer", "renormalize", "retriable", + "scme", "serp", "servfail", "setsas", diff --git a/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs b/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs index 1555d7f8c2e7..63556858f200 100644 --- a/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs +++ b/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs @@ -12,11 +12,11 @@ namespace Azure.Core.Tests.ModelReaderWriterTests.Models [PersistableModelProxy(typeof(UnknownBaseModel))] public abstract class BaseModel : IUtf8JsonSerializable, IJsonModel { - private Dictionary _rawData; + private Dictionary _serializedAdditionalRawData; protected internal BaseModel(Dictionary rawData) { - _rawData = rawData ?? new Dictionary(); + _serializedAdditionalRawData = rawData ?? new Dictionary(); } public string Kind { get; internal set; } @@ -25,7 +25,7 @@ protected internal BaseModel(Dictionary rawData) protected internal void SerializeRawData(Utf8JsonWriter writer) { //write out the raw data - foreach (var property in _rawData) + foreach (var property in _serializedAdditionalRawData) { writer.WritePropertyName(property.Key); #if NET6_0_OR_GREATER diff --git a/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/ModelAsStruct.cs b/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/ModelAsStruct.cs index e262d0ea8cf5..49cc3065bbd8 100644 --- a/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/ModelAsStruct.cs +++ b/sdk/core/Azure.Core/tests/common/ModelReaderWriter/Models/ModelAsStruct.cs @@ -14,7 +14,7 @@ namespace Azure.Core.Tests.ModelReaderWriterTests.Models /// The InputAdditionalPropertiesModelStruct. public readonly partial struct ModelAsStruct : IUtf8JsonSerializable, IJsonModel, IJsonModel { - private readonly Dictionary _rawData; + private readonly Dictionary _serializedAdditionalRawData; /// Initializes a new instance of InputAdditionalPropertiesModelStruct. /// @@ -24,7 +24,7 @@ namespace Azure.Core.Tests.ModelReaderWriterTests.Models public ModelAsStruct(int id, Dictionary rawData) { Id = id; - _rawData = rawData; + _serializedAdditionalRawData = rawData; } /// Gets the id. @@ -41,9 +41,9 @@ private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) writer.WriteStartObject(); writer.WritePropertyName("id"u8); writer.WriteNumberValue(Id); - if (_rawData is not null && options.Format == "J") + if (_serializedAdditionalRawData is not null && options.Format == "J") { - foreach (var property in _rawData) + foreach (var property in _serializedAdditionalRawData) { writer.WritePropertyName(property.Key); #if NET6_0_OR_GREATER diff --git a/sdk/core/Azure.Core/tests/public/ModelReaderWriterTests/ModelTests.cs b/sdk/core/Azure.Core/tests/public/ModelReaderWriterTests/ModelTests.cs index 0cc8cdaa8371..4004df55c4c9 100644 --- a/sdk/core/Azure.Core/tests/public/ModelReaderWriterTests/ModelTests.cs +++ b/sdk/core/Azure.Core/tests/public/ModelReaderWriterTests/ModelTests.cs @@ -117,7 +117,7 @@ internal static Dictionary GetRawData(object model) { modelType = modelType.BaseType; } - var propertyInfo = modelType.GetField("_rawData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var propertyInfo = modelType.GetField("_serializedAdditionalRawData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); return propertyInfo.GetValue(model) as Dictionary; } diff --git a/sdk/core/System.ClientModel/api/System.ClientModel.net8.0.cs b/sdk/core/System.ClientModel/api/System.ClientModel.net8.0.cs index 43ee7709d2f9..39dc5e4c52f5 100644 --- a/sdk/core/System.ClientModel/api/System.ClientModel.net8.0.cs +++ b/sdk/core/System.ClientModel/api/System.ClientModel.net8.0.cs @@ -263,6 +263,121 @@ public JsonModelConverter(System.ClientModel.Primitives.ModelReaderWriterOptions public override System.ClientModel.Primitives.IJsonModel? Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; } public override void Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.IJsonModel value, System.Text.Json.JsonSerializerOptions options) { } } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0001")] + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct JsonPatch + { + private object _dummy; + private int _dummyPrimitive; + public JsonPatch(System.ReadOnlyMemory utf8Json) { throw null; } + public void Append(System.ReadOnlySpan arrayPath, System.BinaryData utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, bool value) { } + public void Append(System.ReadOnlySpan arrayPath, byte value) { } + public void Append(System.ReadOnlySpan arrayPath, byte[] utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, decimal value) { } + public void Append(System.ReadOnlySpan arrayPath, double value) { } + public void Append(System.ReadOnlySpan arrayPath, System.Guid value) { } + public void Append(System.ReadOnlySpan arrayPath, short value) { } + public void Append(System.ReadOnlySpan arrayPath, int value) { } + public void Append(System.ReadOnlySpan arrayPath, long value) { } + public void Append(System.ReadOnlySpan arrayPath, System.ReadOnlySpan utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, sbyte value) { } + public void Append(System.ReadOnlySpan arrayPath, float value) { } + public void Append(System.ReadOnlySpan arrayPath, string value) { } + public void Append(System.ReadOnlySpan arrayPath, System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, ushort value) { } + public void Append(System.ReadOnlySpan arrayPath, uint value) { } + public void Append(System.ReadOnlySpan arrayPath, ulong value) { } + public void AppendNull(System.ReadOnlySpan arrayPath) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public bool Contains(System.ReadOnlySpan jsonPath) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public bool Contains(System.ReadOnlySpan prefix, System.ReadOnlySpan property) { throw null; } + public bool GetBoolean(System.ReadOnlySpan jsonPath) { throw null; } + public byte GetByte(System.ReadOnlySpan jsonPath) { throw null; } + public System.DateTime GetDateTime(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public System.DateTimeOffset GetDateTimeOffset(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public decimal GetDecimal(System.ReadOnlySpan jsonPath) { throw null; } + public double GetDouble(System.ReadOnlySpan jsonPath) { throw null; } + public float GetFloat(System.ReadOnlySpan jsonPath) { throw null; } + public System.Guid GetGuid(System.ReadOnlySpan jsonPath) { throw null; } + public short GetInt16(System.ReadOnlySpan jsonPath) { throw null; } + public int GetInt32(System.ReadOnlySpan jsonPath) { throw null; } + public long GetInt64(System.ReadOnlySpan jsonPath) { throw null; } + public sbyte GetInt8(System.ReadOnlySpan jsonPath) { throw null; } + public System.BinaryData GetJson(System.ReadOnlySpan jsonPath) { throw null; } + public T? GetNullableValue(System.ReadOnlySpan jsonPath) where T : struct { throw null; } + public string? GetString(System.ReadOnlySpan jsonPath) { throw null; } + public System.TimeSpan GetTimeSpan(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public ushort GetUInt16(System.ReadOnlySpan jsonPath) { throw null; } + public uint GetUInt32(System.ReadOnlySpan jsonPath) { throw null; } + public ulong GetUInt64(System.ReadOnlySpan jsonPath) { throw null; } + public bool IsRemoved(System.ReadOnlySpan jsonPath) { throw null; } + public void Remove(System.ReadOnlySpan jsonPath) { } + public void Set(System.ReadOnlySpan jsonPath, System.BinaryData utf8Json) { } + public void Set(System.ReadOnlySpan jsonPath, bool value) { } + public void Set(System.ReadOnlySpan jsonPath, byte value) { } + public void Set(System.ReadOnlySpan jsonPath, byte[] utf8Json) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Set(System.ReadOnlySpan jsonPath, System.ClientModel.Primitives.JsonPatch.EncodedValue value) { } + public void Set(System.ReadOnlySpan jsonPath, System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, decimal value) { } + public void Set(System.ReadOnlySpan jsonPath, double value) { } + public void Set(System.ReadOnlySpan jsonPath, System.Guid value) { } + public void Set(System.ReadOnlySpan jsonPath, short value) { } + public void Set(System.ReadOnlySpan jsonPath, int value) { } + public void Set(System.ReadOnlySpan jsonPath, long value) { } + public void Set(System.ReadOnlySpan jsonPath, System.ReadOnlySpan utf8Json) { } + public void Set(System.ReadOnlySpan jsonPath, sbyte value) { } + public void Set(System.ReadOnlySpan jsonPath, float value) { } + public void Set(System.ReadOnlySpan jsonPath, string value) { } + public void Set(System.ReadOnlySpan jsonPath, System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, ushort value) { } + public void Set(System.ReadOnlySpan jsonPath, uint value) { } + public void Set(System.ReadOnlySpan jsonPath, ulong value) { } + public void SetNull(System.ReadOnlySpan jsonPath) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void SetPropagators(System.ClientModel.Primitives.JsonPatch.PropagatorSetter setter, System.ClientModel.Primitives.JsonPatch.PropagatorGetter getter) { } + public override string ToString() { throw null; } + public string ToString(string format) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public bool TryGetEncodedValue(System.ReadOnlySpan jsonPath, out System.ClientModel.Primitives.JsonPatch.EncodedValue value) { throw null; } + public bool TryGetJson(System.ReadOnlySpan jsonPath, out System.ReadOnlyMemory value) { throw null; } + public bool TryGetNullableValue(System.ReadOnlySpan jsonPath, out T? value) where T : struct { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out bool value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out byte value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out decimal value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out double value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.Guid value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out short value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out int value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out long value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out sbyte value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out float value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out string? value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out ushort value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out uint value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out ulong value) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.ReadOnlySpan jsonPath) { } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct EncodedValue + { + private object _dummy; + private int _dummyPrimitive; + } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public delegate bool PropagatorGetter(System.ReadOnlySpan jsonPath, out System.ClientModel.Primitives.JsonPatch.EncodedValue value); + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public delegate bool PropagatorSetter(System.ReadOnlySpan jsonPath, System.ClientModel.Primitives.JsonPatch.EncodedValue value); + } public partial class MessageLoggingPolicy : System.ClientModel.Primitives.PipelinePolicy { public MessageLoggingPolicy(System.ClientModel.Primitives.ClientLoggingOptions? options = null) { } diff --git a/sdk/core/System.ClientModel/api/System.ClientModel.net9.0.cs b/sdk/core/System.ClientModel/api/System.ClientModel.net9.0.cs new file mode 100644 index 000000000000..39dc5e4c52f5 --- /dev/null +++ b/sdk/core/System.ClientModel/api/System.ClientModel.net9.0.cs @@ -0,0 +1,574 @@ +namespace System.ClientModel +{ + public partial class ApiKeyCredential + { + public ApiKeyCredential(string key) { } + public void Deconstruct(out string key) { throw null; } + public void Update(string key) { } + } + public abstract partial class AsyncCollectionResult : System.ClientModel.Primitives.AsyncCollectionResult, System.Collections.Generic.IAsyncEnumerable + { + protected internal AsyncCollectionResult() { } + public System.Collections.Generic.IAsyncEnumerator GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + protected abstract System.Collections.Generic.IAsyncEnumerable GetValuesFromPageAsync(System.ClientModel.ClientResult page); + } + public abstract partial class AuthenticationTokenProvider + { + protected AuthenticationTokenProvider() { } + public abstract System.ClientModel.Primitives.GetTokenOptions? CreateTokenOptions(System.Collections.Generic.IReadOnlyDictionary properties); + public abstract System.ClientModel.Primitives.AuthenticationToken GetToken(System.ClientModel.Primitives.GetTokenOptions options, System.Threading.CancellationToken cancellationToken); + public abstract System.Threading.Tasks.ValueTask GetTokenAsync(System.ClientModel.Primitives.GetTokenOptions options, System.Threading.CancellationToken cancellationToken); + } + public abstract partial class BinaryContent : System.IDisposable + { + protected BinaryContent() { } + public string? MediaType { get { throw null; } protected set { } } + public static System.ClientModel.BinaryContent Create(System.BinaryData value) { throw null; } + public static System.ClientModel.BinaryContent Create(System.IO.Stream stream) { throw null; } + public static System.ClientModel.BinaryContent CreateJson(string jsonString, bool validate = false) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.")] + public static System.ClientModel.BinaryContent CreateJson(T jsonSerializable, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public static System.ClientModel.BinaryContent CreateJson(T jsonSerializable, System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) { throw null; } + public static System.ClientModel.BinaryContent Create(T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public abstract void Dispose(); + public abstract bool TryComputeLength(out long length); + public abstract void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public abstract System.Threading.Tasks.Task WriteToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial class ClientResult + { + protected ClientResult(System.ClientModel.Primitives.PipelineResponse response) { } + public static System.ClientModel.ClientResult FromOptionalValue(T? value, System.ClientModel.Primitives.PipelineResponse response) { throw null; } + public static System.ClientModel.ClientResult FromResponse(System.ClientModel.Primitives.PipelineResponse response) { throw null; } + public static System.ClientModel.ClientResult FromValue(T value, System.ClientModel.Primitives.PipelineResponse response) { throw null; } + public System.ClientModel.Primitives.PipelineResponse GetRawResponse() { throw null; } + } + public partial class ClientResultException : System.Exception + { + public ClientResultException(System.ClientModel.Primitives.PipelineResponse response, System.Exception? innerException = null) { } + public ClientResultException(string message, System.ClientModel.Primitives.PipelineResponse? response = null, System.Exception? innerException = null) { } + public int Status { get { throw null; } protected set { } } + public static System.Threading.Tasks.Task CreateAsync(System.ClientModel.Primitives.PipelineResponse response, System.Exception? innerException = null) { throw null; } + public System.ClientModel.Primitives.PipelineResponse? GetRawResponse() { throw null; } + } + public partial class ClientResult : System.ClientModel.ClientResult + { + protected internal ClientResult(T value, System.ClientModel.Primitives.PipelineResponse response) : base (default(System.ClientModel.Primitives.PipelineResponse)) { } + public virtual T Value { get { throw null; } } + public static implicit operator T (System.ClientModel.ClientResult result) { throw null; } + } + public abstract partial class CollectionResult : System.ClientModel.Primitives.CollectionResult, System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + { + protected internal CollectionResult() { } + public System.Collections.Generic.IEnumerator GetEnumerator() { throw null; } + protected abstract System.Collections.Generic.IEnumerable GetValuesFromPage(System.ClientModel.ClientResult page); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + } + public partial class ContinuationToken + { + protected ContinuationToken() { } + protected ContinuationToken(System.BinaryData bytes) { } + public static System.ClientModel.ContinuationToken FromBytes(System.BinaryData bytes) { throw null; } + public virtual System.BinaryData ToBytes() { throw null; } + } +} +namespace System.ClientModel.Primitives +{ + public static partial class ActivityExtensions + { + public static System.Diagnostics.Activity MarkClientActivityFailed(this System.Diagnostics.Activity activity, System.Exception? exception) { throw null; } + public static System.Diagnostics.Activity? StartClientActivity(this System.Diagnostics.ActivitySource activitySource, System.ClientModel.Primitives.ClientPipelineOptions options, string name, System.Diagnostics.ActivityKind kind = System.Diagnostics.ActivityKind.Internal, System.Diagnostics.ActivityContext parentContext = default(System.Diagnostics.ActivityContext), System.Collections.Generic.IEnumerable>? tags = null) { throw null; } + } + public partial class ApiKeyAuthenticationPolicy : System.ClientModel.Primitives.AuthenticationPolicy + { + internal ApiKeyAuthenticationPolicy() { } + public static System.ClientModel.Primitives.ApiKeyAuthenticationPolicy CreateBasicAuthorizationPolicy(System.ClientModel.ApiKeyCredential credential) { throw null; } + public static System.ClientModel.Primitives.ApiKeyAuthenticationPolicy CreateBearerAuthorizationPolicy(System.ClientModel.ApiKeyCredential credential) { throw null; } + public static System.ClientModel.Primitives.ApiKeyAuthenticationPolicy CreateHeaderApiKeyPolicy(System.ClientModel.ApiKeyCredential credential, string headerName, string? keyPrefix = null) { throw null; } + public sealed override void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + public sealed override System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + } + public abstract partial class AsyncCollectionResult + { + protected AsyncCollectionResult() { } + public abstract System.ClientModel.ContinuationToken? GetContinuationToken(System.ClientModel.ClientResult page); + public abstract System.Collections.Generic.IAsyncEnumerable GetRawPagesAsync(); + } + public abstract partial class AuthenticationPolicy : System.ClientModel.Primitives.PipelinePolicy + { + protected AuthenticationPolicy() { } + } + public partial class AuthenticationToken + { + public AuthenticationToken(string tokenValue, string tokenType, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn = default(System.DateTimeOffset?)) { } + public System.DateTimeOffset? ExpiresOn { get { throw null; } } + public System.DateTimeOffset? RefreshOn { get { throw null; } } + public string TokenType { get { throw null; } } + public string TokenValue { get { throw null; } } + } + public partial class BearerTokenPolicy : System.ClientModel.Primitives.AuthenticationPolicy + { + public BearerTokenPolicy(System.ClientModel.AuthenticationTokenProvider tokenProvider, System.Collections.Generic.IEnumerable> contexts) { } + public BearerTokenPolicy(System.ClientModel.AuthenticationTokenProvider tokenProvider, string scope) { } + public override void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + } + public partial class ClientCache + { + public ClientCache(int maxSize) { } + public T GetClient(object clientId, System.Func createClient) where T : class { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct ClientConnection + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public ClientConnection(string id, string locator) { throw null; } + public ClientConnection(string id, string locator, object credential, System.ClientModel.Primitives.CredentialKind credentialKind) { throw null; } + public ClientConnection(string id, string locator, object? credential, System.ClientModel.Primitives.CredentialKind credentialKind, System.Collections.Generic.IReadOnlyDictionary? metadata) { throw null; } + public object? Credential { get { throw null; } } + public System.ClientModel.Primitives.CredentialKind CredentialKind { get { throw null; } } + public string Id { get { throw null; } } + public string Locator { get { throw null; } } + public System.Collections.Generic.IReadOnlyDictionary Metadata { get { throw null; } } + public override string ToString() { throw null; } + public bool TryGetLocatorAsUri(out System.Uri? uri) { throw null; } + } + public partial class ClientConnectionCollection : System.Collections.ObjectModel.KeyedCollection + { + public ClientConnectionCollection() { } + public void AddRange(System.Collections.Generic.IEnumerable connections) { } + protected override string GetKeyForItem(System.ClientModel.Primitives.ClientConnection item) { throw null; } + } + public abstract partial class ClientConnectionProvider + { + protected ClientConnectionProvider(int maxCacheSize) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.ClientModel.Primitives.ClientCache Subclients { get { throw null; } } + public abstract System.Collections.Generic.IEnumerable GetAllConnections(); + public abstract System.ClientModel.Primitives.ClientConnection GetConnection(string connectionId); + } + [System.FlagsAttribute] + public enum ClientErrorBehaviors + { + Default = 0, + NoThrow = 1, + } + public partial class ClientLoggingOptions + { + public ClientLoggingOptions() { } + public System.Collections.Generic.IList AllowedHeaderNames { get { throw null; } } + public System.Collections.Generic.IList AllowedQueryParameters { get { throw null; } } + public bool? EnableLogging { get { throw null; } set { } } + public bool? EnableMessageContentLogging { get { throw null; } set { } } + public bool? EnableMessageLogging { get { throw null; } set { } } + public Microsoft.Extensions.Logging.ILoggerFactory? LoggerFactory { get { throw null; } set { } } + public int? MessageContentSizeLimit { get { throw null; } set { } } + protected void AssertNotFrozen() { } + public virtual void Freeze() { } + } + public sealed partial class ClientPipeline + { + internal ClientPipeline() { } + public static System.ClientModel.Primitives.ClientPipeline Create(System.ClientModel.Primitives.ClientPipelineOptions? options = null) { throw null; } + public static System.ClientModel.Primitives.ClientPipeline Create(System.ClientModel.Primitives.ClientPipelineOptions options, System.ReadOnlySpan perCallPolicies, System.ReadOnlySpan perTryPolicies, System.ReadOnlySpan beforeTransportPolicies) { throw null; } + public System.ClientModel.Primitives.PipelineMessage CreateMessage() { throw null; } + public System.ClientModel.Primitives.PipelineMessage CreateMessage(System.Uri uri, string method, System.ClientModel.Primitives.PipelineMessageClassifier? classifier = null) { throw null; } + public void Send(System.ClientModel.Primitives.PipelineMessage message) { } + public System.Threading.Tasks.ValueTask SendAsync(System.ClientModel.Primitives.PipelineMessage message) { throw null; } + } + public partial class ClientPipelineOptions + { + public ClientPipelineOptions() { } + public System.ClientModel.Primitives.ClientLoggingOptions? ClientLoggingOptions { get { throw null; } set { } } + public bool? EnableDistributedTracing { get { throw null; } set { } } + public System.ClientModel.Primitives.PipelinePolicy? MessageLoggingPolicy { get { throw null; } set { } } + public System.TimeSpan? NetworkTimeout { get { throw null; } set { } } + public System.ClientModel.Primitives.PipelinePolicy? RetryPolicy { get { throw null; } set { } } + public System.ClientModel.Primitives.PipelineTransport? Transport { get { throw null; } set { } } + public void AddPolicy(System.ClientModel.Primitives.PipelinePolicy policy, System.ClientModel.Primitives.PipelinePosition position) { } + protected void AssertNotFrozen() { } + public virtual void Freeze() { } + } + public partial class ClientRetryPolicy : System.ClientModel.Primitives.PipelinePolicy + { + public ClientRetryPolicy(int maxRetries = 3) { } + public ClientRetryPolicy(int maxRetries, bool enableLogging, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) { } + public static System.ClientModel.Primitives.ClientRetryPolicy Default { get { throw null; } } + protected virtual System.TimeSpan GetNextDelay(System.ClientModel.Primitives.PipelineMessage message, int tryCount) { throw null; } + protected virtual void OnRequestSent(System.ClientModel.Primitives.PipelineMessage message) { } + protected virtual System.Threading.Tasks.ValueTask OnRequestSentAsync(System.ClientModel.Primitives.PipelineMessage message) { throw null; } + protected virtual void OnSendingRequest(System.ClientModel.Primitives.PipelineMessage message) { } + protected virtual System.Threading.Tasks.ValueTask OnSendingRequestAsync(System.ClientModel.Primitives.PipelineMessage message) { throw null; } + protected virtual void OnTryComplete(System.ClientModel.Primitives.PipelineMessage message) { } + public sealed override void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + public sealed override System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + protected virtual bool ShouldRetry(System.ClientModel.Primitives.PipelineMessage message, System.Exception? exception) { throw null; } + protected virtual System.Threading.Tasks.ValueTask ShouldRetryAsync(System.ClientModel.Primitives.PipelineMessage message, System.Exception? exception) { throw null; } + protected virtual void Wait(System.TimeSpan time, System.Threading.CancellationToken cancellationToken) { } + protected virtual System.Threading.Tasks.Task WaitAsync(System.TimeSpan time, System.Threading.CancellationToken cancellationToken) { throw null; } + } + public abstract partial class CollectionResult + { + protected CollectionResult() { } + public abstract System.ClientModel.ContinuationToken? GetContinuationToken(System.ClientModel.ClientResult page); + public abstract System.Collections.Generic.IEnumerable GetRawPages(); + } + public enum CredentialKind + { + None = 0, + ApiKeyString = 1, + TokenCredential = 2, + } + public partial class GetTokenOptions + { + public const string AuthorizationUrlPropertyName = "authorizationUrl"; + public const string RefreshUrlPropertyName = "refreshUrl"; + public const string ScopesPropertyName = "scopes"; + public const string TokenUrlPropertyName = "tokenUrl"; + public GetTokenOptions(System.Collections.Generic.IReadOnlyDictionary properties) { } + public System.Collections.Generic.IReadOnlyDictionary Properties { get { throw null; } } + } + public partial class HttpClientPipelineTransport : System.ClientModel.Primitives.PipelineTransport, System.IDisposable + { + public HttpClientPipelineTransport() { } + public HttpClientPipelineTransport(System.Net.Http.HttpClient client) { } + public HttpClientPipelineTransport(System.Net.Http.HttpClient? client, bool enableLogging, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) { } + public static System.ClientModel.Primitives.HttpClientPipelineTransport Shared { get { throw null; } } + protected override System.ClientModel.Primitives.PipelineMessage CreateMessageCore() { throw null; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + protected virtual void OnReceivedResponse(System.ClientModel.Primitives.PipelineMessage message, System.Net.Http.HttpResponseMessage httpResponse) { } + protected virtual void OnSendingRequest(System.ClientModel.Primitives.PipelineMessage message, System.Net.Http.HttpRequestMessage httpRequest) { } + protected sealed override void ProcessCore(System.ClientModel.Primitives.PipelineMessage message) { } + protected sealed override System.Threading.Tasks.ValueTask ProcessCoreAsync(System.ClientModel.Primitives.PipelineMessage message) { throw null; } + } + public partial interface IJsonModel : System.ClientModel.Primitives.IPersistableModel + { + T? Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options); + void Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options); + } + public partial interface IPersistableModel + { + T? Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options); + string GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options); + System.BinaryData Write(System.ClientModel.Primitives.ModelReaderWriterOptions options); + } + public partial class JsonModelConverter : System.Text.Json.Serialization.JsonConverter> + { + public JsonModelConverter() { } + public JsonModelConverter(System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + public JsonModelConverter(System.ClientModel.Primitives.ModelReaderWriterOptions options, System.ClientModel.Primitives.ModelReaderWriterContext context) { } + public override bool CanConvert(System.Type typeToConvert) { throw null; } + public override System.ClientModel.Primitives.IJsonModel? Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; } + public override void Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.IJsonModel value, System.Text.Json.JsonSerializerOptions options) { } + } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0001")] + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct JsonPatch + { + private object _dummy; + private int _dummyPrimitive; + public JsonPatch(System.ReadOnlyMemory utf8Json) { throw null; } + public void Append(System.ReadOnlySpan arrayPath, System.BinaryData utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, bool value) { } + public void Append(System.ReadOnlySpan arrayPath, byte value) { } + public void Append(System.ReadOnlySpan arrayPath, byte[] utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, decimal value) { } + public void Append(System.ReadOnlySpan arrayPath, double value) { } + public void Append(System.ReadOnlySpan arrayPath, System.Guid value) { } + public void Append(System.ReadOnlySpan arrayPath, short value) { } + public void Append(System.ReadOnlySpan arrayPath, int value) { } + public void Append(System.ReadOnlySpan arrayPath, long value) { } + public void Append(System.ReadOnlySpan arrayPath, System.ReadOnlySpan utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, sbyte value) { } + public void Append(System.ReadOnlySpan arrayPath, float value) { } + public void Append(System.ReadOnlySpan arrayPath, string value) { } + public void Append(System.ReadOnlySpan arrayPath, System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, ushort value) { } + public void Append(System.ReadOnlySpan arrayPath, uint value) { } + public void Append(System.ReadOnlySpan arrayPath, ulong value) { } + public void AppendNull(System.ReadOnlySpan arrayPath) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public bool Contains(System.ReadOnlySpan jsonPath) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public bool Contains(System.ReadOnlySpan prefix, System.ReadOnlySpan property) { throw null; } + public bool GetBoolean(System.ReadOnlySpan jsonPath) { throw null; } + public byte GetByte(System.ReadOnlySpan jsonPath) { throw null; } + public System.DateTime GetDateTime(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public System.DateTimeOffset GetDateTimeOffset(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public decimal GetDecimal(System.ReadOnlySpan jsonPath) { throw null; } + public double GetDouble(System.ReadOnlySpan jsonPath) { throw null; } + public float GetFloat(System.ReadOnlySpan jsonPath) { throw null; } + public System.Guid GetGuid(System.ReadOnlySpan jsonPath) { throw null; } + public short GetInt16(System.ReadOnlySpan jsonPath) { throw null; } + public int GetInt32(System.ReadOnlySpan jsonPath) { throw null; } + public long GetInt64(System.ReadOnlySpan jsonPath) { throw null; } + public sbyte GetInt8(System.ReadOnlySpan jsonPath) { throw null; } + public System.BinaryData GetJson(System.ReadOnlySpan jsonPath) { throw null; } + public T? GetNullableValue(System.ReadOnlySpan jsonPath) where T : struct { throw null; } + public string? GetString(System.ReadOnlySpan jsonPath) { throw null; } + public System.TimeSpan GetTimeSpan(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public ushort GetUInt16(System.ReadOnlySpan jsonPath) { throw null; } + public uint GetUInt32(System.ReadOnlySpan jsonPath) { throw null; } + public ulong GetUInt64(System.ReadOnlySpan jsonPath) { throw null; } + public bool IsRemoved(System.ReadOnlySpan jsonPath) { throw null; } + public void Remove(System.ReadOnlySpan jsonPath) { } + public void Set(System.ReadOnlySpan jsonPath, System.BinaryData utf8Json) { } + public void Set(System.ReadOnlySpan jsonPath, bool value) { } + public void Set(System.ReadOnlySpan jsonPath, byte value) { } + public void Set(System.ReadOnlySpan jsonPath, byte[] utf8Json) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Set(System.ReadOnlySpan jsonPath, System.ClientModel.Primitives.JsonPatch.EncodedValue value) { } + public void Set(System.ReadOnlySpan jsonPath, System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, decimal value) { } + public void Set(System.ReadOnlySpan jsonPath, double value) { } + public void Set(System.ReadOnlySpan jsonPath, System.Guid value) { } + public void Set(System.ReadOnlySpan jsonPath, short value) { } + public void Set(System.ReadOnlySpan jsonPath, int value) { } + public void Set(System.ReadOnlySpan jsonPath, long value) { } + public void Set(System.ReadOnlySpan jsonPath, System.ReadOnlySpan utf8Json) { } + public void Set(System.ReadOnlySpan jsonPath, sbyte value) { } + public void Set(System.ReadOnlySpan jsonPath, float value) { } + public void Set(System.ReadOnlySpan jsonPath, string value) { } + public void Set(System.ReadOnlySpan jsonPath, System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, ushort value) { } + public void Set(System.ReadOnlySpan jsonPath, uint value) { } + public void Set(System.ReadOnlySpan jsonPath, ulong value) { } + public void SetNull(System.ReadOnlySpan jsonPath) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void SetPropagators(System.ClientModel.Primitives.JsonPatch.PropagatorSetter setter, System.ClientModel.Primitives.JsonPatch.PropagatorGetter getter) { } + public override string ToString() { throw null; } + public string ToString(string format) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public bool TryGetEncodedValue(System.ReadOnlySpan jsonPath, out System.ClientModel.Primitives.JsonPatch.EncodedValue value) { throw null; } + public bool TryGetJson(System.ReadOnlySpan jsonPath, out System.ReadOnlyMemory value) { throw null; } + public bool TryGetNullableValue(System.ReadOnlySpan jsonPath, out T? value) where T : struct { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out bool value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out byte value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out decimal value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out double value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.Guid value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out short value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out int value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out long value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out sbyte value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out float value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out string? value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out ushort value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out uint value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out ulong value) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.ReadOnlySpan jsonPath) { } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct EncodedValue + { + private object _dummy; + private int _dummyPrimitive; + } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public delegate bool PropagatorGetter(System.ReadOnlySpan jsonPath, out System.ClientModel.Primitives.JsonPatch.EncodedValue value); + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public delegate bool PropagatorSetter(System.ReadOnlySpan jsonPath, System.ClientModel.Primitives.JsonPatch.EncodedValue value); + } + public partial class MessageLoggingPolicy : System.ClientModel.Primitives.PipelinePolicy + { + public MessageLoggingPolicy(System.ClientModel.Primitives.ClientLoggingOptions? options = null) { } + public static System.ClientModel.Primitives.MessageLoggingPolicy Default { get { throw null; } } + public sealed override void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + public sealed override System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + } + public static partial class ModelReaderWriter + { + public static object? Read(System.BinaryData data, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] System.Type returnType, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) { throw null; } + public static object? Read(System.BinaryData data, System.Type returnType, System.ClientModel.Primitives.ModelReaderWriterOptions options, System.ClientModel.Primitives.ModelReaderWriterContext context) { throw null; } + public static T? Read(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static T? Read(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options, System.ClientModel.Primitives.ModelReaderWriterContext context) { throw null; } + public static System.BinaryData Write(object model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) { throw null; } + public static System.BinaryData Write(object model, System.ClientModel.Primitives.ModelReaderWriterOptions options, System.ClientModel.Primitives.ModelReaderWriterContext context) { throw null; } + public static System.BinaryData Write(T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static System.BinaryData Write(T model, System.ClientModel.Primitives.ModelReaderWriterOptions options, System.ClientModel.Primitives.ModelReaderWriterContext context) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true)] + public partial class ModelReaderWriterBuildableAttribute : System.Attribute + { + public ModelReaderWriterBuildableAttribute(System.Type type) { } + } + public abstract partial class ModelReaderWriterContext + { + protected ModelReaderWriterContext() { } + public System.ClientModel.Primitives.ModelReaderWriterTypeBuilder GetTypeBuilder(System.Type type) { throw null; } + public bool TryGetTypeBuilder(System.Type type, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.ClientModel.Primitives.ModelReaderWriterTypeBuilder? builder) { throw null; } + protected virtual bool TryGetTypeBuilderCore(System.Type type, out System.ClientModel.Primitives.ModelReaderWriterTypeBuilder? builder) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly)] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public sealed partial class ModelReaderWriterContextTypeAttribute : System.Attribute + { + public ModelReaderWriterContextTypeAttribute(System.Type contextType) { } + } + public partial class ModelReaderWriterOptions + { + public ModelReaderWriterOptions(string format) { } + public string Format { get { throw null; } } + public static System.ClientModel.Primitives.ModelReaderWriterOptions Json { get { throw null; } } + public static System.ClientModel.Primitives.ModelReaderWriterOptions Xml { get { throw null; } } + } + public abstract partial class ModelReaderWriterTypeBuilder + { + protected ModelReaderWriterTypeBuilder() { } + protected abstract System.Type BuilderType { get; } + protected virtual System.Type? ItemType { get { throw null; } } + protected virtual void AddItem(object collectionBuilder, object? item) { } + protected virtual void AddItemWithKey(object collectionBuilder, string key, object? item) { } + protected virtual object ConvertCollectionBuilder(object collectionBuilder) { throw null; } + protected abstract object CreateInstance(); + protected virtual System.Collections.IEnumerable? GetItems(object collection) { throw null; } + } + public abstract partial class OperationResult + { + protected OperationResult(System.ClientModel.Primitives.PipelineResponse response) { } + public bool HasCompleted { get { throw null; } protected set { } } + public abstract System.ClientModel.ContinuationToken? RehydrationToken { get; protected set; } + public System.ClientModel.Primitives.PipelineResponse GetRawResponse() { throw null; } + protected void SetRawResponse(System.ClientModel.Primitives.PipelineResponse response) { } + public abstract System.ClientModel.ClientResult UpdateStatus(System.ClientModel.Primitives.RequestOptions? options = null); + public abstract System.Threading.Tasks.ValueTask UpdateStatusAsync(System.ClientModel.Primitives.RequestOptions? options = null); + public virtual void WaitForCompletion(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public virtual System.Threading.Tasks.ValueTask WaitForCompletionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Class)] + public sealed partial class PersistableModelProxyAttribute : System.Attribute + { + public PersistableModelProxyAttribute([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] System.Type proxyType) { } + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + public System.Type ProxyType { get { throw null; } } + } + public partial class PipelineMessage : System.IDisposable + { + protected internal PipelineMessage(System.ClientModel.Primitives.PipelineRequest request) { } + public bool BufferResponse { get { throw null; } set { } } + public System.Threading.CancellationToken CancellationToken { get { throw null; } protected internal set { } } + public System.TimeSpan? NetworkTimeout { get { throw null; } set { } } + public System.ClientModel.Primitives.PipelineRequest Request { get { throw null; } } + public System.ClientModel.Primitives.PipelineResponse? Response { get { throw null; } protected internal set { } } + public System.ClientModel.Primitives.PipelineMessageClassifier ResponseClassifier { get { throw null; } set { } } + public void Apply(System.ClientModel.Primitives.RequestOptions? options) { } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.ClientModel.Primitives.PipelineResponse? ExtractResponse() { throw null; } + public void SetProperty(System.Type key, object? value) { } + public bool TryGetProperty(System.Type key, out object? value) { throw null; } + } + public abstract partial class PipelineMessageClassifier + { + protected PipelineMessageClassifier() { } + public static System.ClientModel.Primitives.PipelineMessageClassifier Default { get { throw null; } } + public static System.ClientModel.Primitives.PipelineMessageClassifier Create(System.ReadOnlySpan successStatusCodes) { throw null; } + public abstract bool TryClassify(System.ClientModel.Primitives.PipelineMessage message, out bool isError); + public abstract bool TryClassify(System.ClientModel.Primitives.PipelineMessage message, System.Exception? exception, out bool isRetriable); + } + public abstract partial class PipelinePolicy + { + protected PipelinePolicy() { } + public abstract void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex); + public abstract System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex); + protected static void ProcessNext(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + protected static System.Threading.Tasks.ValueTask ProcessNextAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + } + public enum PipelinePosition + { + PerCall = 0, + PerTry = 1, + BeforeTransport = 2, + } + public abstract partial class PipelineRequest : System.IDisposable + { + protected PipelineRequest() { } + public System.ClientModel.BinaryContent? Content { get { throw null; } set { } } + protected abstract System.ClientModel.BinaryContent? ContentCore { get; set; } + public System.ClientModel.Primitives.PipelineRequestHeaders Headers { get { throw null; } } + protected abstract System.ClientModel.Primitives.PipelineRequestHeaders HeadersCore { get; } + public string Method { get { throw null; } set { } } + protected abstract string MethodCore { get; set; } + public System.Uri? Uri { get { throw null; } set { } } + protected abstract System.Uri? UriCore { get; set; } + public abstract void Dispose(); + } + public abstract partial class PipelineRequestHeaders : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + protected PipelineRequestHeaders() { } + public abstract void Add(string name, string value); + public abstract System.Collections.Generic.IEnumerator> GetEnumerator(); + public abstract bool Remove(string name); + public abstract void Set(string name, string value); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public abstract bool TryGetValue(string name, out string? value); + public abstract bool TryGetValues(string name, out System.Collections.Generic.IEnumerable? values); + } + public abstract partial class PipelineResponse : System.IDisposable + { + protected PipelineResponse() { } + public abstract System.BinaryData Content { get; } + public abstract System.IO.Stream? ContentStream { get; set; } + public System.ClientModel.Primitives.PipelineResponseHeaders Headers { get { throw null; } } + protected abstract System.ClientModel.Primitives.PipelineResponseHeaders HeadersCore { get; } + public virtual bool IsError { get { throw null; } } + protected internal virtual bool IsErrorCore { get { throw null; } set { } } + public abstract string ReasonPhrase { get; } + public abstract int Status { get; } + public abstract System.BinaryData BufferContent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public abstract System.Threading.Tasks.ValueTask BufferContentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public abstract void Dispose(); + } + public abstract partial class PipelineResponseHeaders : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + protected PipelineResponseHeaders() { } + public abstract System.Collections.Generic.IEnumerator> GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public abstract bool TryGetValue(string name, out string? value); + public abstract bool TryGetValues(string name, out System.Collections.Generic.IEnumerable? values); + } + public abstract partial class PipelineTransport : System.ClientModel.Primitives.PipelinePolicy + { + protected PipelineTransport() { } + protected PipelineTransport(bool enableLogging, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) { } + public System.ClientModel.Primitives.PipelineMessage CreateMessage() { throw null; } + protected abstract System.ClientModel.Primitives.PipelineMessage CreateMessageCore(); + public void Process(System.ClientModel.Primitives.PipelineMessage message) { } + public sealed override void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + public System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message) { throw null; } + public sealed override System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + protected abstract void ProcessCore(System.ClientModel.Primitives.PipelineMessage message); + protected abstract System.Threading.Tasks.ValueTask ProcessCoreAsync(System.ClientModel.Primitives.PipelineMessage message); + } + public partial class RequestOptions + { + public RequestOptions() { } + public bool BufferResponse { get { throw null; } set { } } + public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } } + public System.ClientModel.Primitives.ClientErrorBehaviors ErrorOptions { get { throw null; } set { } } + public void AddHeader(string name, string value) { } + public void AddPolicy(System.ClientModel.Primitives.PipelinePolicy policy, System.ClientModel.Primitives.PipelinePosition position) { } + protected internal virtual void Apply(System.ClientModel.Primitives.PipelineMessage message) { } + protected void AssertNotFrozen() { } + public virtual void Freeze() { } + public void SetHeader(string name, string value) { } + } + public partial class UserAgentPolicy : System.ClientModel.Primitives.PipelinePolicy + { + public UserAgentPolicy(System.Reflection.Assembly callerAssembly, string? applicationId = null) { } + public string? ApplicationId { get { throw null; } } + public System.Reflection.Assembly Assembly { get { throw null; } } + public string UserAgentValue { get { throw null; } } + public override void Process(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(System.ClientModel.Primitives.PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { throw null; } + } +} diff --git a/sdk/core/System.ClientModel/api/System.ClientModel.netstandard2.0.cs b/sdk/core/System.ClientModel/api/System.ClientModel.netstandard2.0.cs index f6eac76d6a4a..b6a143a641c8 100644 --- a/sdk/core/System.ClientModel/api/System.ClientModel.netstandard2.0.cs +++ b/sdk/core/System.ClientModel/api/System.ClientModel.netstandard2.0.cs @@ -262,6 +262,120 @@ public JsonModelConverter(System.ClientModel.Primitives.ModelReaderWriterOptions public override System.ClientModel.Primitives.IJsonModel? Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; } public override void Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.IJsonModel value, System.Text.Json.JsonSerializerOptions options) { } } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct JsonPatch + { + private object _dummy; + private int _dummyPrimitive; + public JsonPatch(System.ReadOnlyMemory utf8Json) { throw null; } + public void Append(System.ReadOnlySpan arrayPath, System.BinaryData utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, bool value) { } + public void Append(System.ReadOnlySpan arrayPath, byte value) { } + public void Append(System.ReadOnlySpan arrayPath, byte[] utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, decimal value) { } + public void Append(System.ReadOnlySpan arrayPath, double value) { } + public void Append(System.ReadOnlySpan arrayPath, System.Guid value) { } + public void Append(System.ReadOnlySpan arrayPath, short value) { } + public void Append(System.ReadOnlySpan arrayPath, int value) { } + public void Append(System.ReadOnlySpan arrayPath, long value) { } + public void Append(System.ReadOnlySpan arrayPath, System.ReadOnlySpan utf8Json) { } + public void Append(System.ReadOnlySpan arrayPath, sbyte value) { } + public void Append(System.ReadOnlySpan arrayPath, float value) { } + public void Append(System.ReadOnlySpan arrayPath, string value) { } + public void Append(System.ReadOnlySpan arrayPath, System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Append(System.ReadOnlySpan arrayPath, ushort value) { } + public void Append(System.ReadOnlySpan arrayPath, uint value) { } + public void Append(System.ReadOnlySpan arrayPath, ulong value) { } + public void AppendNull(System.ReadOnlySpan arrayPath) { } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public bool Contains(System.ReadOnlySpan jsonPath) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public bool Contains(System.ReadOnlySpan prefix, System.ReadOnlySpan property) { throw null; } + public bool GetBoolean(System.ReadOnlySpan jsonPath) { throw null; } + public byte GetByte(System.ReadOnlySpan jsonPath) { throw null; } + public System.DateTime GetDateTime(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public System.DateTimeOffset GetDateTimeOffset(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public decimal GetDecimal(System.ReadOnlySpan jsonPath) { throw null; } + public double GetDouble(System.ReadOnlySpan jsonPath) { throw null; } + public float GetFloat(System.ReadOnlySpan jsonPath) { throw null; } + public System.Guid GetGuid(System.ReadOnlySpan jsonPath) { throw null; } + public short GetInt16(System.ReadOnlySpan jsonPath) { throw null; } + public int GetInt32(System.ReadOnlySpan jsonPath) { throw null; } + public long GetInt64(System.ReadOnlySpan jsonPath) { throw null; } + public sbyte GetInt8(System.ReadOnlySpan jsonPath) { throw null; } + public System.BinaryData GetJson(System.ReadOnlySpan jsonPath) { throw null; } + public T? GetNullableValue(System.ReadOnlySpan jsonPath) where T : struct { throw null; } + public string? GetString(System.ReadOnlySpan jsonPath) { throw null; } + public System.TimeSpan GetTimeSpan(System.ReadOnlySpan jsonPath, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public ushort GetUInt16(System.ReadOnlySpan jsonPath) { throw null; } + public uint GetUInt32(System.ReadOnlySpan jsonPath) { throw null; } + public ulong GetUInt64(System.ReadOnlySpan jsonPath) { throw null; } + public bool IsRemoved(System.ReadOnlySpan jsonPath) { throw null; } + public void Remove(System.ReadOnlySpan jsonPath) { } + public void Set(System.ReadOnlySpan jsonPath, System.BinaryData utf8Json) { } + public void Set(System.ReadOnlySpan jsonPath, bool value) { } + public void Set(System.ReadOnlySpan jsonPath, byte value) { } + public void Set(System.ReadOnlySpan jsonPath, byte[] utf8Json) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Set(System.ReadOnlySpan jsonPath, System.ClientModel.Primitives.JsonPatch.EncodedValue value) { } + public void Set(System.ReadOnlySpan jsonPath, System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, decimal value) { } + public void Set(System.ReadOnlySpan jsonPath, double value) { } + public void Set(System.ReadOnlySpan jsonPath, System.Guid value) { } + public void Set(System.ReadOnlySpan jsonPath, short value) { } + public void Set(System.ReadOnlySpan jsonPath, int value) { } + public void Set(System.ReadOnlySpan jsonPath, long value) { } + public void Set(System.ReadOnlySpan jsonPath, System.ReadOnlySpan utf8Json) { } + public void Set(System.ReadOnlySpan jsonPath, sbyte value) { } + public void Set(System.ReadOnlySpan jsonPath, float value) { } + public void Set(System.ReadOnlySpan jsonPath, string value) { } + public void Set(System.ReadOnlySpan jsonPath, System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { } + public void Set(System.ReadOnlySpan jsonPath, ushort value) { } + public void Set(System.ReadOnlySpan jsonPath, uint value) { } + public void Set(System.ReadOnlySpan jsonPath, ulong value) { } + public void SetNull(System.ReadOnlySpan jsonPath) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void SetPropagators(System.ClientModel.Primitives.JsonPatch.PropagatorSetter setter, System.ClientModel.Primitives.JsonPatch.PropagatorGetter getter) { } + public override string ToString() { throw null; } + public string ToString(string format) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public bool TryGetEncodedValue(System.ReadOnlySpan jsonPath, out System.ClientModel.Primitives.JsonPatch.EncodedValue value) { throw null; } + public bool TryGetJson(System.ReadOnlySpan jsonPath, out System.ReadOnlyMemory value) { throw null; } + public bool TryGetNullableValue(System.ReadOnlySpan jsonPath, out T? value) where T : struct { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out bool value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out byte value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.DateTime value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.DateTimeOffset value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out decimal value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out double value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.Guid value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out short value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out int value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out long value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out sbyte value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out float value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out string? value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out System.TimeSpan value, System.Buffers.StandardFormat format = default(System.Buffers.StandardFormat)) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out ushort value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out uint value) { throw null; } + public bool TryGetValue(System.ReadOnlySpan jsonPath, out ulong value) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.ReadOnlySpan jsonPath) { } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct EncodedValue + { + private object _dummy; + private int _dummyPrimitive; + } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public delegate bool PropagatorGetter(System.ReadOnlySpan jsonPath, out System.ClientModel.Primitives.JsonPatch.EncodedValue value); + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public delegate bool PropagatorSetter(System.ReadOnlySpan jsonPath, System.ClientModel.Primitives.JsonPatch.EncodedValue value); + } public partial class MessageLoggingPolicy : System.ClientModel.Primitives.PipelinePolicy { public MessageLoggingPolicy(System.ClientModel.Primitives.ClientLoggingOptions? options = null) { } diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.EncodedValue.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.EncodedValue.cs new file mode 100644 index 000000000000..20b95698b3e1 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.EncodedValue.cs @@ -0,0 +1,646 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Buffers.Text; +using System.Buffers; +using System.Text; +using System.Diagnostics; + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + /// + /// A patch value that has been encoded in UTF-8 bytes along with its value kind. + /// + public struct EncodedValue + { + private const byte MaxInt32Utf8Bytes = 11; + private const byte MaxInt64Utf8Bytes = 20; + private const byte MaxDecimalUtf8Bytes = 64; + private const byte MaxGuidUtf8Bytes = 48; + private const byte MaxDateTimeUtf8Bytes = 64; + private const byte MaxTimeSpanUtf8Bytes = 64; + + private static readonly ReadOnlyMemory s_null = "null"u8.ToArray(); + private static readonly ReadOnlyMemory s_true = "true"u8.ToArray(); + private static readonly ReadOnlyMemory s_false = "false"u8.ToArray(); + + internal static readonly EncodedValue Empty = new(ValueKind.None, Array.Empty()); + internal static readonly EncodedValue Removed = new(ValueKind.Removed); + internal static readonly EncodedValue Null = new(ValueKind.Null, s_null); + + private EncodedValue(ValueKind kind) + { + Kind = kind; + } + + internal EncodedValue(ValueKind kind, ReadOnlyMemory value) + { + Kind = kind; + Value = value; + } + + internal EncodedValue(bool value) + { + if (value) + { + Kind = ValueKind.BooleanTrue; + Value = s_true; + } + else + { + Kind = ValueKind.BooleanFalse; + Value = s_false; + } + } + + internal EncodedValue(byte value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt32Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(DateTime value, StandardFormat format = default) + { + Kind = ValueKind.DateTime; + + Span buffer = stackalloc byte[MaxDateTimeUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten, format)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(DateTimeOffset value, StandardFormat format = default) + { + Kind = ValueKind.DateTime; + + Span buffer = stackalloc byte[MaxDateTimeUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten, format)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(decimal value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxDecimalUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(double value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxDecimalUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(float value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxDecimalUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(Guid value) + { + Kind = ValueKind.Guid; + + Span buffer = stackalloc byte[MaxGuidUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(int value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt32Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(long value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt64Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(sbyte value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt32Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(short value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt32Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(TimeSpan value, StandardFormat format = default) + { + Kind = ValueKind.TimeSpan; + + Span buffer = stackalloc byte[MaxTimeSpanUtf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten, format)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + internal EncodedValue(uint value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt32Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(ulong value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt64Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(ushort value) + { + Kind = ValueKind.Number; + + Span buffer = stackalloc byte[MaxInt32Utf8Bytes]; + if (Utf8Formatter.TryFormat(value, buffer, out var bytesWritten)) + { + Value = buffer.Slice(0, bytesWritten).ToArray(); + } + + Debug.Assert(true, "Failed to encode value"); + } + + internal EncodedValue(string value) + { + Kind = ValueKind.Utf8String; + Value = Encoding.UTF8.GetBytes(value); + } + + internal EncodedValue(BinaryData value) : this(value.ToMemory()) { } + + internal EncodedValue(ReadOnlySpan value) + { + if (value.SequenceEqual(s_null.Span)) + { + Kind = ValueKind.Null; + Value = s_null; + } + else if (value.SequenceEqual(s_false.Span)) + { + Kind = ValueKind.BooleanFalse; + Value = s_false; + } + else if (value.SequenceEqual(s_true.Span)) + { + Kind = ValueKind.BooleanTrue; + Value = s_true; + } + else + { + Kind = ValueKind.Json; + Value = value.ToArray(); + } + } + + internal EncodedValue(byte[] value) : this((ReadOnlyMemory)value) { } + + internal EncodedValue(ReadOnlyMemory value) + { + ReadOnlySpan span = value.Span; + if (span.SequenceEqual(s_null.Span)) + { + Kind = ValueKind.Null; + Value = s_null; + } + else if (span.SequenceEqual(s_false.Span)) + { + Kind = ValueKind.BooleanFalse; + Value = s_false; + } + else if (span.SequenceEqual(s_true.Span)) + { + Kind = ValueKind.BooleanTrue; + Value = s_true; + } + else + { + Kind = ValueKind.Json; + Value = value; + } + } + + internal ValueKind Kind { get; set; } + + internal ReadOnlyMemory Value { get; } + + internal bool TryDecodeValue(out bool value) + { + if (Utf8Parser.TryParse(Value.Span, out bool result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out byte value) + { + if (Utf8Parser.TryParse(Value.Span, out byte result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out DateTime value, StandardFormat format = default) + { + if (Utf8Parser.TryParse(Value.Span, out DateTime result, out _, format.Symbol)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out DateTimeOffset value, StandardFormat format = default) + { + if (Utf8Parser.TryParse(Value.Span, out DateTimeOffset result, out _, format.Symbol)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out decimal value) + { + if (Utf8Parser.TryParse(Value.Span, out decimal result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out double value) + { + if (Utf8Parser.TryParse(Value.Span, out double result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out float value) + { + if (Utf8Parser.TryParse(Value.Span, out float result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out Guid value) + { + if (Utf8Parser.TryParse(Value.Span, out Guid result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out int value) + { + if (Utf8Parser.TryParse(Value.Span, out int result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out long value) + { + if (Utf8Parser.TryParse(Value.Span, out long result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out sbyte value) + { + if (Utf8Parser.TryParse(Value.Span, out sbyte result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out short value) + { + if (Utf8Parser.TryParse(Value.Span, out short result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out TimeSpan value, StandardFormat format = default) + { + if (Utf8Parser.TryParse(Value.Span, out TimeSpan result, out _, format.Symbol)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out uint value) + { + if (Utf8Parser.TryParse(Value.Span, out uint result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out ulong value) + { + if (Utf8Parser.TryParse(Value.Span, out ulong result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out ushort value) + { + if (Utf8Parser.TryParse(Value.Span, out ushort result, out _)) + { + value = result; + return true; + } + + value = default; + return false; + } + + internal bool TryDecodeValue(out string? value) + { + var span = Value.Span; + + if (s_null.Span.SequenceEqual(span)) + { + value = null; + return true; + } + + if (span.Length >= 2 && span[0] == (byte)'"' && span[span.Length - 1] == (byte)'"') + { + // Trim the quotes + span = span.Slice(1, span.Length - 2); + } + +#if NET6_0_OR_GREATER + value = Encoding.UTF8.GetString(span); +#else + value = Encoding.UTF8.GetString(span.ToArray()); +#endif + return true; + } + + internal bool TryDecodeNullableValue(out T? value, out bool supportedType) + where T : struct + { + value = default; + supportedType = true; + + if (Value.Span.SequenceEqual(s_null.Span)) + { + return true; + } + + Type target = typeof(T); + bool parsed; + + if (target == typeof(bool)) + { + parsed = Utf8Parser.TryParse(Value.Span, out bool boolValue, out _); + value = parsed ? (T?)(object)boolValue : null; + return parsed; + } + if (target == typeof(byte)) + { + parsed = Utf8Parser.TryParse(Value.Span, out byte byteValue, out _); + value = parsed ? (T?)(object)byteValue : null; + return parsed; + } + if (target == typeof(sbyte)) + { + parsed = Utf8Parser.TryParse(Value.Span, out sbyte sbyteValue, out _); + value = parsed ? (T?)(object)sbyteValue : null; + return parsed; + } + if (target == typeof(short)) + { + parsed = Utf8Parser.TryParse(Value.Span, out short shortValue, out _); + value = parsed ? (T?)(object)shortValue : null; + return parsed; + } + if (target == typeof(ushort)) + { + parsed = Utf8Parser.TryParse(Value.Span, out ushort ushortValue, out _); + value = parsed ? (T?)(object)ushortValue : null; + return parsed; + } + if (target == typeof(int)) + { + parsed = Utf8Parser.TryParse(Value.Span, out int intValue, out _); + value = parsed ? (T?)(object)intValue : null; + return parsed; + } + if (target == typeof(uint)) + { + parsed = Utf8Parser.TryParse(Value.Span, out uint uintValue, out _); + value = parsed ? (T?)(object)uintValue : null; + return parsed; + } + if (target == typeof(long)) + { + parsed = Utf8Parser.TryParse(Value.Span, out long longValue, out _); + value = parsed ? (T?)(object)longValue : null; + return parsed; + } + if (target == typeof(ulong)) + { + parsed = Utf8Parser.TryParse(Value.Span, out ulong ulongValue, out _); + value = parsed ? (T?)(object)ulongValue : null; + return parsed; + } + if (target == typeof(float)) + { + parsed = Utf8Parser.TryParse(Value.Span, out float floatValue, out _); + value = parsed ? (T?)(object)floatValue : null; + return parsed; + } + if (target == typeof(double)) + { + parsed = Utf8Parser.TryParse(Value.Span, out double doubleValue, out _); + value = parsed ? (T?)(object)doubleValue : null; + return parsed; + } + if (target == typeof(decimal)) + { + parsed = Utf8Parser.TryParse(Value.Span, out decimal decimalValue, out _); + value = parsed ? (T?)(object)decimalValue : null; + return parsed; + } + if (target == typeof(DateTime)) + { + parsed = Utf8Parser.TryParse(Value.Span, out DateTime dateTimeValue, out _); + value = parsed ? (T?)(object)dateTimeValue : null; + return parsed; + } + if (target == typeof(DateTimeOffset)) + { + parsed = Utf8Parser.TryParse(Value.Span, out DateTimeOffset dateTimeOffsetValue, out _); + value = parsed ? (T?)(object)dateTimeOffsetValue : null; + return parsed; + } + if (target == typeof(Guid)) + { + parsed = Utf8Parser.TryParse(Value.Span, out Guid guidValue, out _); + value = parsed ? (T?)(object)guidValue : null; + return parsed; + } + if (target == typeof(TimeSpan)) + { + parsed = Utf8Parser.TryParse(Value.Span, out TimeSpan timeSpanValue, out _); + value = parsed ? (T?)(object)timeSpanValue : null; + return parsed; + } + + supportedType = false; + return false; + } + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.Implementation.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.Implementation.cs new file mode 100644 index 000000000000..955e6e42ef6a --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.Implementation.cs @@ -0,0 +1,699 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Buffers.Text; +using System.ClientModel.Internal; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + // Dictionary-based storage using UTF8 byte arrays as keys and encoded byte arrays as values + private SpanDictionary? _properties; + private readonly EncodedValue _rawJson; + + private PropagatorSetter? _propagatorSetter; + private PropagatorGetter? _propagatorGetter; + + private bool TryGetEncodedValueInternal(ReadOnlySpan jsonPath, out EncodedValue value) + { + value = EncodedValue.Empty; + EncodedValue encodedValue; + + // if root is overridden we should not propagate. + if (jsonPath.IsRoot()) + { + return TryGetExactMatch(jsonPath, out value); + } + + if (_propagatorGetter is not null && _propagatorGetter(jsonPath, out value)) + { + return true; + } + + if (_properties == null) + { + if (TryGetParentMatch(jsonPath, true, out _, out encodedValue)) + { + value = new(encodedValue.Kind, encodedValue.Value.GetJson(jsonPath)); + return true; + } + return false; + } + + if (TryGetExactMatch(jsonPath, out value)) + { + return true; + } + + Span childPath = stackalloc byte[jsonPath.Length]; + + // if jsonPath is an array index and its direct parent is not an insert skip root merger + ReadOnlySpan directParent = jsonPath.GetParent(); + if (jsonPath.IsArrayIndex() && _properties.TryGetValue(directParent, out var parentValue) && + !parentValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + GetSubPath(directParent, jsonPath, ref childPath); + value = new(parentValue.Kind, parentValue.Value.GetJson(childPath)); + return true; + } + + if (TryGetParentMatch(jsonPath, true, out var parentPath, out encodedValue)) + { + // normalize jsonPath once to avoid multiple Normalize calls in GetMaxSibling + Span normalizedJsonPathBuffer = stackalloc byte[jsonPath.Length]; + JsonPathComparer.Default.Normalize(jsonPath, normalizedJsonPathBuffer, out var bytesWritten); + ReadOnlySpan normalizedJsonPath = normalizedJsonPathBuffer.Slice(0, bytesWritten); + + // if the jsonPath has arrays I need to check if they exist in the root first then patch + JsonPathReader reader = new(normalizedJsonPath); + reader.Advance(parentPath); + ReadOnlySpan normalizedArrayPath = reader.GetNextArray(); + if (normalizedArrayPath.IsEmpty) + { + // no array in sub path + GetSubPath(parentPath, normalizedJsonPath, ref childPath); + value = new(encodedValue.Kind, encodedValue.Value.GetJson(childPath)); + return true; + } + + // see if the requested index exist in root first + // collect and adjust indexes in a new path as I go + // indexes will only get smaller so jsonPath.Length is safe + Span adjustedJsonPath = stackalloc byte[normalizedJsonPath.Length]; + normalizedJsonPath.CopyTo(adjustedJsonPath); + int adjustedLength = normalizedJsonPath.Length; + + while (!normalizedArrayPath.IsEmpty) + { + if (TryGetArrayItemFromRoot(normalizedArrayPath, reader, out var indexRequested, out var length, out var arrayItem)) + { + GetSubPath(normalizedArrayPath, jsonPath, ref childPath); + value = new(ValueKind.Json, GetCombinedArray(jsonPath, arrayItem.GetJson(childPath), EncodedValue.Empty)); + return true; + } + + AdjustJsonPath( + indexRequested - Math.Max(length, GetMaxSibling(normalizedArrayPath) + 1), + reader.Current.ValueSpan.Length, + reader.Current.TokenStartIndex, + adjustedJsonPath, + ref adjustedLength); + + normalizedArrayPath = reader.GetNextArray(); + } + + GetSubPath(parentPath, adjustedJsonPath.Slice(0, adjustedLength), ref childPath); + value = new(encodedValue.Kind, encodedValue.Value.GetJson(childPath)); + return true; + } + + return false; + } + + private readonly int GetMaxSibling(ReadOnlySpan normalizedArrayPath) + { + Debug.Assert(_properties is not null, "GetMaxSibling should only be called when _properties is not null."); + + int maxSibling = -1; + var normalizedPrefix = normalizedArrayPath.GetParent(); + + foreach (var kvp in _properties!) + { + if (kvp.Value.Kind == ValueKind.Removed || kvp.Value.Kind.HasFlag(ValueKind.ModelOwned)) + { + continue; + } + + ReadOnlySpan keySpan = kvp.Key; + if (!keySpan.StartsWith(normalizedPrefix)) + { + continue; + } + + keySpan = keySpan.Slice(normalizedPrefix.Length); + + if (!keySpan.IsArrayWrapped()) + { + continue; + } + + if (Utf8Parser.TryParse(keySpan.Slice(1, keySpan.Length - 2), out int index, out _)) + { + maxSibling = Math.Max(maxSibling, index); + } + } + + return maxSibling; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetExactMatch(ReadOnlySpan jsonPath, out EncodedValue value) + { + if (_properties is not null && _properties.TryGetValue(jsonPath, out var currentValue)) + { + if (currentValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + value = new(currentValue.Kind, GetCombinedArray(jsonPath, currentValue)); + } + else + { + value = currentValue; + } + return true; + } + + value = EncodedValue.Empty; + return false; + } + + private static void AdjustJsonPath(int newIndex, int indexLength, int indexStart, Span buffer, ref int length) + { + Utf8Formatter.TryFormat(newIndex, buffer.Slice(indexStart), out var bytesWritten); + int shift = indexLength - bytesWritten; + if (shift > 0) + { + buffer.Slice(indexStart + indexLength).CopyTo(buffer.Slice(indexStart + bytesWritten)); + length -= shift; + } + } + + private bool TryGetArrayItemFromRoot(ReadOnlySpan jsonPath, JsonPathReader reader, out int indexRequested, out int length, out ReadOnlyMemory arrayItem) + { + length = 0; + indexRequested = 0; + arrayItem = ReadOnlyMemory.Empty; + + if (!Utf8Parser.TryParse(jsonPath.GetIndexSpan(), out indexRequested, out _)) + { + return false; + } + + if (!TryGetRootJson(out var rootJson)) + { + return false; + } + + Utf8JsonReader jsonReader = new(rootJson.Span); + if (!jsonReader.Advance(jsonPath.GetParent())) + { + return false; + } + + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(); // Move to the value after the property name + } + + if (jsonReader.TokenType != JsonTokenType.StartArray) + { + return false; + } + + if (!jsonReader.SkipToIndex(indexRequested, out length)) + { + return false; + } + + long start = jsonReader.TokenStartIndex; + jsonReader.Skip(); + long end = jsonReader.BytesConsumed; + arrayItem = rootJson.Slice((int)start, (int)(end - start)); + return true; + } + + private ReadOnlyMemory GetCombinedArray(ReadOnlySpan jsonPath, EncodedValue encodedValue) + { + TryGetRootJson(out var rootJson); + + rootJson.TryGetJson(jsonPath, out var existingArray); + + return GetCombinedArray(jsonPath, existingArray, encodedValue); + } + + private ReadOnlyMemory GetCombinedArray(ReadOnlySpan jsonPath, ReadOnlyMemory existingArray, EncodedValue encodedValue) + { + Debug.Assert(_properties is not null, "GetCombinedArray should only be called when _properties is not null."); + + Span normalizedPrefix = stackalloc byte[jsonPath.Length]; + byte[] childPath = new byte[_properties!.MaxKeyLength]; + JsonPathComparer.Default.Normalize(jsonPath, normalizedPrefix, out int bytesWritten); + normalizedPrefix = normalizedPrefix.Slice(0, bytesWritten); + + // find all items in _properties that start with normalizedPrefix and combine them into a single array + foreach (var kvp in _properties) + { + if (kvp.Value.Kind == ValueKind.Removed || kvp.Value.Kind.HasFlag(ValueKind.ModelOwned)) + { + continue; + } + + ReadOnlySpan keySpan = kvp.Key; + + if (!keySpan.StartsWith(normalizedPrefix)) + { + continue; + } + + if (existingArray.IsEmpty) + { + // if the existing array is empty, then we can just use the encoded value directly if it matches the prefix, otherwise we need to wrap it in an array + existingArray = keySpan.SequenceEqual(normalizedPrefix) ? encodedValue.Value : new([(byte)'[', .. kvp.Value.Value.Span, (byte)']']); + } + else + { + GetSubPath(normalizedPrefix, kvp.Key, childPath, out int childPathLength); + ReadOnlySpan childJsonPath = childPath.AsSpan(0, childPathLength); + if (childJsonPath.IsArrayIndex()) + { + // if the childJsonPath is an array index, we need to insert the value at that index in the existing array + existingArray = existingArray.InsertAt(childJsonPath, kvp.Value.Value); + } + else + { + // otherwise we can just append the value to the existing array + existingArray = existingArray.Append(childJsonPath, kvp.Value.Value.Slice(1, kvp.Value.Value.Length - 2)); + } + } + } + // if existing array is still empty return the original encoded value, otherwise return the combined array + return existingArray.IsEmpty ? encodedValue.Value : existingArray; + } + + private bool TryGetRootJson(out ReadOnlyMemory value) + { + value = ReadOnlyMemory.Empty; + + Debug.Assert(_properties is not null, "TryGetRootJson should only be called when _properties is not null."); + + if (_rawJson.Value.IsEmpty) + { + return false; + } + + value = _rawJson.Value; + return !value.IsEmpty; + } + + private bool TryGetParentMatch(ReadOnlySpan jsonPath, bool includeRoot, [NotNullWhen(true)] out ReadOnlySpan parentPath, out EncodedValue encodedValue) + { + parentPath = default; + encodedValue = EncodedValue.Empty; + if (_properties == null) + { + if (includeRoot && !_rawJson.Value.IsEmpty) + { + encodedValue = _rawJson; + parentPath = "$"u8; + return true; + } + return false; + } + + parentPath = jsonPath.GetParent(); + while (parentPath.Length > 0) + { + if (_properties.TryGetValue(parentPath, out encodedValue)) + { + return true; + } + + parentPath = parentPath.GetParent(); + + if (parentPath.IsRoot()) + { + break; + } + } + + if (parentPath.IsRoot() && includeRoot) + { + if (_properties.TryGetValue(parentPath, out encodedValue)) + { + return true; + } + + if (!_rawJson.Value.IsEmpty) + { + encodedValue = _rawJson; + return true; + } + } + + return false; + } + + private void SetInternal(ReadOnlySpan jsonPath, EncodedValue encodedValue) + { + if (_propagatorSetter is not null && _propagatorSetter(jsonPath, encodedValue)) + { + return; + } + + EncodedValue currentValue = EncodedValue.Empty; + + if (_properties is not null && _properties.TryGetValue(jsonPath, out currentValue)) + { + _properties.Set(jsonPath, new(encodedValue.Kind, ModifyJson(currentValue, "$"u8, encodedValue))); + return; + } + + ReadOnlySpan localPath = jsonPath; + byte[] adjustedPath = new byte[jsonPath.Length]; + Span childPath = stackalloc byte[localPath.Length]; + + if (!_rawJson.Value.IsEmpty && encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + Utf8JsonReader jsonReader = new(_rawJson.Value.Span); + JsonPathReader pathReader = new(jsonPath); + if (jsonReader.Advance(ref pathReader)) + { + _properties ??= new(); + _properties.Set(jsonPath, new(encodedValue.Kind, GetNewJson("$"u8, encodedValue))); + return; + } + + if (pathReader.Current.TokenType != JsonPathTokenType.End) + { + var parsedPath = pathReader.GetParsedPath(); + if (parsedPath.IsArrayIndex()) + { + jsonReader = new(_rawJson.Value.Span); + int length = jsonReader.GetArrayLength(parsedPath.GetParent()); + Utf8Parser.TryParse(pathReader.Current.ValueSpan, out int index, out _); + int newLength = parsedPath.Length; + parsedPath.CopyTo(adjustedPath); + AdjustJsonPath(index - length, pathReader.Current.ValueSpan.Length, pathReader.Current.TokenStartIndex, adjustedPath, ref newLength); + var remainingPath = jsonPath.Slice(pathReader.Current.TokenStartIndex + 2); + remainingPath.CopyTo(adjustedPath.AsSpan(newLength)); + localPath = adjustedPath.AsSpan(0, newLength + remainingPath.Length); + } + } + } + + var parentPath = localPath.SequenceEqual(jsonPath) ? localPath : localPath.GetParent(); + var nextPath = localPath; + + if (_properties is not null) + { + while (true) + { + if (_properties.TryGetValue(parentPath, out currentValue)) + { + GetSubPath(parentPath, localPath, ref childPath); + ValueKind newKind = childPath.IsRoot() ? encodedValue.Kind : currentValue.Kind; + _properties.Set(parentPath, new(newKind, ModifyJson(currentValue, childPath, encodedValue))); + return; + } + + if (parentPath.IsRoot()) + { + break; + } + + nextPath = parentPath; + parentPath = parentPath.GetParent(); + } + } + + if (encodedValue.Kind == ValueKind.Removed && jsonPath.IsArrayIndex()) + { + Utf8JsonReader jsonReader = new(_rawJson.Value.Span); + JsonPathReader pathReader = new(jsonPath); + if (jsonReader.Advance(ref pathReader)) + { + _properties ??= new(); + _properties.Set(jsonPath, new(encodedValue.Kind, GetNewJson("$"u8, encodedValue))); + return; + } + + // we cannot remove an array index that doesn't exist + ThrowIndexOutOfRangeException(jsonPath); + } + + ValueKind kind = ValueKind.Json; + + var jsonParentPath = localPath.GetParent(); + if (jsonParentPath.IsRoot()) + { + // fast path if we are simply adding to root + nextPath = localPath; + parentPath = jsonParentPath; + } + else + { + if (_rawJson.Value.IsEmpty) + { + if (_properties is null) + { + nextPath = localPath.GetFirstProperty(); + parentPath = "$"u8; + } + } + else + { + // since parentPath is not root we need to find how much of localPath exists in _rawJson + // we need to set the key in _properties to that subPath of localPath + + JsonPathReader pathReader = new(jsonPath); + Utf8JsonReader jsonReader = new(_rawJson.Value.Span); + if (jsonReader.Advance(ref pathReader)) + { + nextPath = jsonPath; + parentPath = jsonParentPath; + } + else + { + nextPath = pathReader.GetParsedPath(); + parentPath = nextPath.GetParent(); + } + } + } + + if (nextPath.IsArrayIndex() && !encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + nextPath = parentPath; + kind |= ValueKind.ArrayItemAppend; + } + + if (nextPath.SequenceEqual(localPath)) + { + kind = encodedValue.Kind; + } + + GetSubPath(nextPath, localPath, ref childPath); + + _properties ??= new(); + _properties.Set(nextPath, new(kind, GetNewJson(childPath, encodedValue))); + } + + private static void GetSubPath(ReadOnlySpan parentPath, ReadOnlySpan fullPath, Span subPath, out int bytesWritten) + { + if (parentPath.IsRoot()) + { + fullPath.CopyTo(subPath); + bytesWritten = fullPath.Length; + return; + } + + var childSlice = fullPath.Slice(parentPath.Length); + subPath[0] = (byte)'$'; + childSlice.CopyTo(subPath.Slice(1)); + bytesWritten = childSlice.Length + 1; + } + + private static void GetSubPath(ReadOnlySpan parentPath, ReadOnlySpan fullPath, ref Span subPath) + { + GetSubPath(parentPath, fullPath, subPath, out var bytesWritten); + subPath = subPath.Slice(0, bytesWritten); + } + + private static ReadOnlyMemory ModifyJson(EncodedValue currentValue, ReadOnlySpan jsonPath, EncodedValue encodedValue) + { + if (!currentValue.Kind.HasFlag(ValueKind.Json) && !currentValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + //we are a primitive so just return the new value + return encodedValue.Value; + } + + ReadOnlyMemory json = currentValue.Value; + if (encodedValue.Kind == ValueKind.Removed) + { + return json.Remove(jsonPath); + } + + JsonPathReader pathReader = new(jsonPath); + Utf8JsonReader jsonReader = new(json.Span); + + bool jsonFound = jsonReader.Advance(ref pathReader); + + ReadOnlyMemory data = NeedsQuotes(encodedValue.Kind, jsonPath.IsArrayIndex()) ? new([(byte)'"', .. encodedValue.Value.Span, (byte)'"']) : encodedValue.Value; + ReadOnlySpan propertyName = pathReader.Current.ValueSpan; + + int index = 0; + if (pathReader.Current.TokenType == JsonPathTokenType.ArrayIndex) + { + Utf8Parser.TryParse(pathReader.Current.ValueSpan, out index, out _); + } + if (pathReader.Current.TokenType != JsonPathTokenType.End || jsonReader.TokenType == JsonTokenType.Null) + { + data = GetNonRootNewJson(ref pathReader, jsonPath.GetParent().IsRoot(), jsonPath.IsArrayIndex(), encodedValue); + } + + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(); + } + + if (jsonFound && (jsonReader.TokenType != JsonTokenType.StartArray || !encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend))) + { + return jsonReader.SetCurrentValue(jsonFound, propertyName, json, data); + } + else + { + return jsonReader.Insert(json, propertyName, data, index > 0); + } + } + + private static ReadOnlyMemory GetNewJson(ReadOnlySpan jsonPath, EncodedValue encodedValue) + { + if (encodedValue.Kind == ValueKind.Removed) + { + return encodedValue.Value; + } + + if (jsonPath.IsRoot()) + { + // fast path for root + if (encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + return NeedsQuotes(encodedValue.Kind, true) + ? new([(byte)'[', (byte)'"', .. encodedValue.Value.Span, (byte)'"', (byte)']']) + : new([(byte)'[', .. encodedValue.Value.Span, (byte)']']); + } + return encodedValue.Value; + } + + JsonPathReader pathReader = new(jsonPath); + + return GetNonRootNewJson(ref pathReader, jsonPath.GetParent().IsRoot(), jsonPath.IsArrayIndex(), encodedValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedsQuotes(ValueKind kind, bool isArrayIndex) + { + var stringKinds = ValueKind.Utf8String | ValueKind.TimeSpan | ValueKind.Guid | ValueKind.DateTime; + return (kind & stringKinds) > 0 && (isArrayIndex || kind.HasFlag(ValueKind.ArrayItemAppend)); + } + + private static ReadOnlyMemory GetNonRootNewJson(ref JsonPathReader reader, bool isParentRoot, bool isArrayIndex, EncodedValue encodedValue) + { + using var buffer = new UnsafeBufferSequence(); + using var writer = new Utf8JsonWriter(buffer); + + if (reader.Peek().TokenType == JsonPathTokenType.End) + { + if (encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + writer.WriteStartArray(); + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + writer.WriteEndArray(); + } + else + { + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + } + } + else + { + ProjectJson(writer, ref reader, encodedValue, false); + } + + writer.Flush(); + using var bufferReader = buffer.ExtractReader(); + return bufferReader.ToBinaryData().ToMemory(); + } + + private static void ProjectJson(Utf8JsonWriter writer, ref JsonPathReader pathReader, EncodedValue encodedValue, bool inArray) + { + while (pathReader.Read()) + { + switch (pathReader.Current.TokenType) + { + case JsonPathTokenType.Root: + break; + case JsonPathTokenType.ArrayIndex: + writer.WriteStartArray(); + Utf8Parser.TryParse(pathReader.Current.ValueSpan, out int index, out _); + for (int i = 0; i < index; i++) + { + writer.WriteNullValue(); // Placeholder for array items + } + if (pathReader.Peek().TokenType == JsonPathTokenType.End) + { + if (encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + writer.WriteStartArray(); + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + writer.WriteEndArray(); + } + else + { + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + } + } + else + { + ProjectJson(writer, ref pathReader, encodedValue, true); + } + writer.WriteEndArray(); + break; + case JsonPathTokenType.Property: + if (!inArray) + { + writer.WritePropertyName(pathReader.Current.ValueSpan); + } + if (pathReader.Peek().TokenType == JsonPathTokenType.End) + { + if (encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + writer.WriteStartArray(); + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + writer.WriteEndArray(); + } + else + { + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + } + } + else + { + ProjectJson(writer, ref pathReader, encodedValue, false); + } + break; + case JsonPathTokenType.PropertySeparator: + writer.WriteStartObject(); + if (pathReader.Peek().TokenType == JsonPathTokenType.End) + { + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, encodedValue); + } + else + { + ProjectJson(writer, ref pathReader, encodedValue, false); + } + writer.WriteEndObject(); + break; + case JsonPathTokenType.End: + break; + default: + // we will never reach this since all token types are handled above + ThrowInvalidToken(pathReader.Current.TokenType); + break; + } + } + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.Serialization.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.Serialization.cs new file mode 100644 index 000000000000..19663b080f6f --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.Serialization.cs @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Internal; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; +using System.Text.Json; + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + /// + /// Writes the JSON representation of the JsonPatch to the specified Utf8JsonWriter for the specified JSON path. + /// Writes in standard JSON format. + /// + /// The Utf8JsonWriter to write to. + /// The JSON path to write. + [EditorBrowsable(EditorBrowsableState.Never)] + public void WriteTo(Utf8JsonWriter writer, ReadOnlySpan jsonPath) + { + if (_properties == null) + { + return; + } + + if (_properties.TryGetValue(jsonPath, out var encodedValue)) + { + if (encodedValue.Kind == ValueKind.Removed) + { + return; + } + + if (encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + _properties.TryUpdateValueKind(jsonPath, encodedValue.Kind | ValueKind.ModelOwned); + writer.WriteRawValue(encodedValue.Value.Span.Slice(1, encodedValue.Value.Length - 2)); + } + } + + Span normalizedPrefix = stackalloc byte[jsonPath.Length]; + JsonPathComparer.Default.Normalize(jsonPath, normalizedPrefix, out int bytesWritten); + normalizedPrefix = normalizedPrefix.Slice(0, bytesWritten); + +#if !NET8_0_OR_GREATER + Dictionary keysToUpdate = new(); +#endif + foreach (var kvp in _properties) + { + if (kvp.Value.Kind == ValueKind.Removed || kvp.Value.Kind.HasFlag(ValueKind.ModelOwned)) + { + continue; + } + + ReadOnlySpan keySpan = kvp.Key; + if (!keySpan.StartsWith(normalizedPrefix)) + { + continue; + } + + keySpan = keySpan.Slice(normalizedPrefix.Length); + + WriteEncodedValueAsJson(writer, keySpan.GetPropertyNameFromSlice(), kvp.Value); + +#if NET8_0_OR_GREATER + _properties.TryUpdateValueKind(kvp.Key, kvp.Value.Kind | ValueKind.ModelOwned); +#else + keysToUpdate.Add(kvp.Key, kvp.Value.Kind); +#endif + } + +#if !NET8_0_OR_GREATER + foreach (var kvp in keysToUpdate) + { + _properties.TryUpdateValueKind(kvp.Key, kvp.Value | ValueKind.ModelOwned); + } +#endif + } + + /// + /// Writes the JSON representation of the JsonPatch to the specified Utf8JsonWriter. + /// + /// The Utf8JsonWriter to write to. + [EditorBrowsable(EditorBrowsableState.Never)] + public void WriteTo(Utf8JsonWriter writer) + { + bool isWriterEmpty = writer.CurrentDepth == 0 && writer.BytesCommitted == 0 && writer.BytesPending == 0; + bool isSeeded = !_rawJson.Value.IsEmpty; + + SpanHashSet? arrays = null; + + // write an empty object if we are not seeded and there are no properties to write and the writer is empty + if (!isSeeded && _properties is null && isWriterEmpty) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + return; + } + + // if there are no properties to write and the writer is not empty then we don't need to write anything + // we assume the wrapping {} happened outside of us since writer is not empty. + if (_properties is null && !isWriterEmpty) + { + return; + } + + // if we have properties and we are either not seeded or the writer is empty then we need to write out the properties + // as standalone items not applying them to the original seeded raw json. + if (_properties is not null && (isSeeded ? !isWriterEmpty : true)) + { + bool writingRoot = !isSeeded && isWriterEmpty && !_properties.TryGetValue("$"u8, out var encodedValue) && !encodedValue.Kind.HasFlag(ValueKind.ArrayItemAppend); + if (writingRoot) + { + writer.WriteStartObject(); + } + + foreach (var kvp in _properties) + { + if (kvp.Value.Kind == ValueKind.Removed || kvp.Value.Kind.HasFlag(ValueKind.ModelOwned)) + { + continue; + } + + if (kvp.Value.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + var firstNonArray = kvp.Key.GetFirstNonIndexParent(); + if (arrays?.Contains(firstNonArray) == true) + { + continue; + } + + if (!kvp.Key.IsRoot() && !kvp.Key.IsArrayIndex()) + { + writer.WritePropertyName(kvp.Key.GetPropertyName()); + } + _properties.TryGetValue(firstNonArray, out var existingArrayValue); + var rawArray = GetCombinedArray(firstNonArray, existingArrayValue); + writer.WriteRawValue(rawArray.Span); + arrays ??= new(); + arrays.Add(firstNonArray); + continue; + } + + WriteEncodedValueAsJson(writer, kvp.Key.GetPropertyName(), kvp.Value); + } + + if (writingRoot) + { + writer.WriteEndObject(); + } + return; + } + + Debug.Assert(isSeeded, "Raw JSON should not be empty at this point"); + Debug.Assert(isWriterEmpty, "Writer should be empty at this point"); + + if (_properties is null) + { + // if there are no properties then we simply echo the existing raw json back into the writer. + if (!_rawJson.Value.IsEmpty && writer.CurrentDepth == 0 && writer.BytesCommitted == 0 && writer.BytesPending == 0) + { + writer.WriteRawValue(_rawJson.Value.Span); + } + } + else + { + // we need to apply the properties to the existing raw json and write out the new combined json to the writer. + ReadOnlyMemory newJson = _rawJson.Value; + foreach (var kvp in _properties) + { + if (kvp.Value.Kind.HasFlag(ValueKind.Removed)) + { + newJson = newJson.Remove(kvp.Key); + } + else if (kvp.Value.Kind.HasFlag(ValueKind.ArrayItemAppend)) + { + newJson = newJson.Append(kvp.Key, kvp.Value.Value.Slice(1, kvp.Value.Value.Length - 2)); + } + else + { + if (kvp.Key.IsArrayIndex()) + { + newJson = newJson.InsertAt(kvp.Key, kvp.Value.Value); + } + else + { + newJson = newJson.Set(kvp.Key, kvp.Value.Value); + } + } + } + writer.WriteRawValue(newJson.Span); + } + } + + private static void WriteEncodedValueAsJson(Utf8JsonWriter writer, ReadOnlySpan propertyName, EncodedValue encodedValue) + { + if (encodedValue.Value.Length == 0) + { + throw new ArgumentException("Empty encoded value"); + } + + ValueKind kind = encodedValue.Kind & ~ValueKind.ArrayItemAppend; + ReadOnlySpan valueBytes = encodedValue.Value.Span; + + if (!propertyName.IsEmpty) + { + writer.WritePropertyName(propertyName); + } + + switch (kind) + { + case ValueKind.Number: + // valueBytes contains JSON number representation + writer.WriteRawValue(valueBytes); + break; + + case ValueKind.BooleanTrue: + case ValueKind.BooleanFalse: + // valueBytes contains JSON boolean representation + writer.WriteBooleanValue(kind == ValueKind.BooleanTrue); + break; + + case ValueKind.Json: + // Write raw JSON value + writer.WriteRawValue(valueBytes, skipInputValidation: true); + break; + + case ValueKind.Null: + writer.WriteNullValue(); + break; + + case ValueKind.Removed: + // Skip removed properties during serialization + break; + + case ValueKind.Utf8String: + case ValueKind.TimeSpan: + case ValueKind.Guid: + case ValueKind.DateTime: + writer.WriteStringValue(valueBytes); + break; + + default: + throw new NotSupportedException($"Unsupported value kind: {kind}"); + } + } + + private void WriteAsJsonPatchTo(Utf8JsonWriter writer) + { + if (_properties is null) + { + return; + } + + bool isSeeded = !_rawJson.Value.IsEmpty; + ReadOnlyMemory rawJson = _rawJson.Value; + + // need to add extra space for append characters and escape characters. + Span jsonPointerBuffer = stackalloc byte[_properties.MaxKeyLength << 1]; + + foreach (var kvp in _properties) + { + ReadOnlySpan opType; + bool isArrayItemAppend = kvp.Value.Kind.HasFlag(ValueKind.ArrayItemAppend); + if (kvp.Value.Kind == ValueKind.Removed) + { + opType = "remove"u8; + } + else + { + if (isArrayItemAppend || !isSeeded || !_rawJson.Value.TryGetJson(kvp.Key, out var currentValue)) + { + opType = "add"u8; + } + else + { + var valueSpan = kvp.Value.Value.Span; + if (valueSpan.SequenceEqual(currentValue.Span) || + (valueSpan.Length > 2 && valueSpan[0] == (byte)'"' && valueSpan[valueSpan.Length - 1] == '"' && valueSpan.Slice(1, valueSpan.Length - 2).SequenceEqual(currentValue.Span))) + { + // same value we can skip + continue; + } + opType = "replace"u8; + } + } + writer.WriteStartObject(); + writer.WritePropertyName("op"u8); + writer.WriteStringValue(opType); + writer.WritePropertyName("path"u8); + int bytesWritten = kvp.Key.ConvertToJsonPointer(jsonPointerBuffer, isArrayItemAppend); + writer.WriteStringValue(jsonPointerBuffer.Slice(0, bytesWritten)); + if (kvp.Value.Kind != ValueKind.Removed) + { + writer.WritePropertyName("value"u8); + ReadOnlyMemory valueRom = kvp.Value.Value; + ValueKind kind = kvp.Value.Kind; + if (isArrayItemAppend && valueRom.IsArrayWrapped()) + { + kind = ValueKind.Json; + valueRom = valueRom.Slice(1, valueRom.Length - 2); + } + WriteEncodedValueAsJson(writer, ReadOnlySpan.Empty, new(kind, valueRom)); + } + writer.WriteEndObject(); + } + } + + private string SerializeToJson() + { + using UnsafeBufferSequence buffer = new(); + using Utf8JsonWriter writer = new(buffer); + WriteTo(writer); + writer.Flush(); +#if NET6_0_OR_GREATER + return Encoding.UTF8.GetString(buffer.ExtractReader().ToBinaryData().ToMemory().Span); +#else + return Encoding.UTF8.GetString(buffer.ExtractReader().ToBinaryData().ToArray()); +#endif + } + + private string SerializeToJsonPatch() + { + using UnsafeBufferSequence buffer = new(); + using Utf8JsonWriter writer = new(buffer); + writer.WriteStartArray(); + WriteAsJsonPatchTo(writer); + writer.WriteEndArray(); + writer.Flush(); +#if NET6_0_OR_GREATER + return Encoding.UTF8.GetString(buffer.ExtractReader().ToBinaryData().ToMemory().Span); +#else + return Encoding.UTF8.GetString(buffer.ExtractReader().ToBinaryData().ToArray()); +#endif + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.SpanDictionary.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.SpanDictionary.cs new file mode 100644 index 000000000000..5d385af27d81 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.SpanDictionary.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + /// + /// This class ensures that no one can access the underlying dictionary without normalizing the keys. + /// If the private field was a Dictionary<byte[], EncodedValue> directly + /// then someone could mistakenly use the dictionary methods that take byte[] for keys and would skip normalization. + /// Its a private nested class because it is not intended for general purpose use its specialized for the needs of JsonPatch only. + /// + private class SpanDictionary + { + private readonly Dictionary _inner; + + public SpanDictionary() + { + _inner = new(JsonPathComparer.Default); + } + + public int MaxKeyLength { get; private set; } + + public bool TryGetValue(ReadOnlySpan key, out EncodedValue value) + { + Span buffer = stackalloc byte[key.Length]; + ReadOnlySpan normalizedKey = GetNormalizedKey(key, buffer); +#if NET9_0_OR_GREATER + return _inner.GetAlternateLookup>().TryGetValue(normalizedKey, out value); +#else + return _inner.TryGetValue(normalizedKey.ToArray(), out value); +#endif + } + + public void Set(ReadOnlySpan key, EncodedValue value) + { + Span buffer = stackalloc byte[key.Length]; + ReadOnlySpan normalizedKey = GetNormalizedKey(key, buffer); + MaxKeyLength = Math.Max(MaxKeyLength, normalizedKey.Length); +#if NET9_0_OR_GREATER + _inner.GetAlternateLookup>()[normalizedKey] = value; +#else + _inner[normalizedKey.ToArray()] = value; +#endif + } + + public Dictionary.Enumerator GetEnumerator() + => _inner.GetEnumerator(); + + public void TryUpdateValueKind(ReadOnlySpan key, ValueKind kind) + { + Span buffer = stackalloc byte[key.Length]; + ReadOnlySpan normalizedKey = GetNormalizedKey(key, buffer); +#if NET9_0_OR_GREATER + var lookup = _inner.GetAlternateLookup>(); + ref EncodedValue encodedValue = ref CollectionsMarshal.GetValueRefOrNullRef(lookup, normalizedKey); + if (!Unsafe.IsNullRef(ref encodedValue)) + { + encodedValue.Kind = kind; + } +#elif NET8_0 + ref EncodedValue encodedValue = ref CollectionsMarshal.GetValueRefOrNullRef(_inner, normalizedKey.ToArray()); + if (!Unsafe.IsNullRef(ref encodedValue)) + { + encodedValue.Kind = kind; + } +#else + byte[] normalizedBytes = normalizedKey.ToArray(); + if (_inner.TryGetValue(normalizedBytes, out EncodedValue encodedValue)) + { + encodedValue.Kind = kind; + _inner[normalizedBytes] = encodedValue; + } +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ReadOnlySpan GetNormalizedKey(ReadOnlySpan key, Span buffer) + { + JsonPathComparer.Default.Normalize(key, buffer, out int bytesWritten); + return buffer.Slice(0, bytesWritten); + } + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.SpanHashSet.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.SpanHashSet.cs new file mode 100644 index 000000000000..a00efd21a700 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.SpanHashSet.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + private class SpanHashSet + { + private HashSet _inner; + + public SpanHashSet() + { + _inner = new(JsonPathComparer.Default); + } + + public bool Contains(ReadOnlySpan item) + { + Span normalizedItem = stackalloc byte[item.Length]; + JsonPathComparer.Default.Normalize(item, normalizedItem, out int bytesWritten); + normalizedItem = normalizedItem.Slice(0, bytesWritten); +#if NET9_0_OR_GREATER + return _inner.GetAlternateLookup>().Contains(normalizedItem); +#else + return _inner.Contains(normalizedItem.ToArray()); +#endif + } + + public bool Add(ReadOnlySpan item) + { + Span normalizedItem = stackalloc byte[item.Length]; + JsonPathComparer.Default.Normalize(item, normalizedItem, out int bytesWritten); + normalizedItem = normalizedItem.Slice(0, bytesWritten); +#if NET9_0_OR_GREATER + return _inner.GetAlternateLookup>().Add(normalizedItem); +#else + return _inner.Add(normalizedItem.ToArray()); +#endif + } + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.ThrowHelper.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.ThrowHelper.cs new file mode 100644 index 000000000000..31325181762b --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.ThrowHelper.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static ReadOnlyMemory ThrowKeyNotFoundException(ReadOnlySpan jsonPath) + => throw new KeyNotFoundException($"No value found at JSON path '{Encoding.UTF8.GetString(jsonPath.ToArray())}'."); + + private static void ThrowIfNull(object? obj, string parameterName) + { + if (obj is null) + { + ThrowArgumentNullException(parameterName); + } + } + + [DoesNotReturn] + private static void ThrowArgumentNullException(string parameterName) + => throw new ArgumentNullException(parameterName); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static string ThrowFormatNotSupportedException(string format) + => throw new NotSupportedException($"The format '{format}' is not supported."); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static T ThrowFormatException(ReadOnlySpan jsonPath) + => throw new FormatException($"Value at '{Encoding.UTF8.GetString(jsonPath.ToArray())}' is not a {typeof(T)}."); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static T? NullableTypeNotSupported() + => throw new NotSupportedException($"Type '{typeof(T)}' is not supported by GetNullableValue."); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static void ThrowIndexOutOfRangeException(ReadOnlySpan jsonPath) + => throw new IndexOutOfRangeException($"Cannot remove non-existing array item at path '{Encoding.UTF8.GetString(jsonPath.ToArray())}'."); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static void ThrowInvalidToken(JsonPathTokenType tokenType) + => throw new InvalidOperationException($"Unexpected token type: {tokenType}"); +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.ValueKind.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.ValueKind.cs new file mode 100644 index 000000000000..3c2bd8810eee --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.ValueKind.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace System.ClientModel.Primitives; + +public partial struct JsonPatch +{ + // Value kinds for encoding type information in byte arrays + [Flags] + internal enum ValueKind : short + { + None = 0, + Json = 1 << 0, + Number = 1 << 1, + Utf8String = 1 << 2, + Removed = 1 << 3, + Null = 1 << 4, + BooleanTrue = 1 << 5, + BooleanFalse = 1 << 6, + ArrayItemAppend = 1 << 7, + ModelOwned = 1 << 8, + DateTime = 1 << 9, + Guid = 1 << 10, + TimeSpan = 1 << 11, + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.cs new file mode 100644 index 000000000000..f243af1faf8c --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPatch.cs @@ -0,0 +1,1403 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Buffers; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.ClientModel.Primitives; + +/// +/// A struct representing a JSON patch for partial updates to a JSON structure. +/// +[Experimental("SCME0001")] +public partial struct JsonPatch +{ + /// + /// Delegate for propagating set operations to child properties of the containing model. + /// + /// The JSON path of the value to be set. + /// The value to set. + [EditorBrowsable(EditorBrowsableState.Never)] + public delegate bool PropagatorSetter(ReadOnlySpan jsonPath, EncodedValue value); + + /// + /// Delegate for propagating get operations from child properties of the containing model. + /// + /// The JSON path of the value to be set. + /// The value to set. + [EditorBrowsable(EditorBrowsableState.Never)] + public delegate bool PropagatorGetter(ReadOnlySpan jsonPath, out EncodedValue value); + + /// + /// Initializes a new instance of with utf8 JSON representing the entire object. + /// + /// The utf8 JSON. + public JsonPatch(ReadOnlyMemory utf8Json) + { + _rawJson = new(ValueKind.Json, utf8Json); + } + + /// + /// Sets the callbacks for propagating values to/from another JsonPatch on child properties. + /// + /// The callback to use. + /// The callback to use. + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetPropagators(PropagatorSetter setter, PropagatorGetter getter) + { + _propagatorSetter = setter; + _propagatorGetter = getter; + } + + /// + /// Determines whether the specified JSON path exists in the patch. + /// + /// The JSON path to check. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(ReadOnlySpan jsonPath) + { + if (_properties == null) + { + return false; + } + + // if someone called Append on an array, we don't want to consider that as "contains" for the array path + // since the entire array wasn't set it was just one item appended to it. + return _properties.TryGetValue(jsonPath, out var value) && !value.Kind.HasFlag(ValueKind.ArrayItemAppend); + } + + #region Set Methods + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, bool value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, byte value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + /// The format to encode the value into. + public void Set(ReadOnlySpan jsonPath, DateTime value, StandardFormat format = default) + { + SetInternal(jsonPath, new(value, format)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + /// The format to encode the value into. + public void Set(ReadOnlySpan jsonPath, DateTimeOffset value, StandardFormat format = default) + { + SetInternal(jsonPath, new(value, format)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, decimal value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, double value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, float value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, Guid value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, int value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, long value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, sbyte value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, short value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + /// The format to encode the value into. + public void Set(ReadOnlySpan jsonPath, TimeSpan value, StandardFormat format = default) + { + SetInternal(jsonPath, new(value, format)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, uint value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, ulong value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, ushort value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + public void Set(ReadOnlySpan jsonPath, string value) + { + SetInternal(jsonPath, new(value)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The utf8 json to set. + public void Set(ReadOnlySpan jsonPath, byte[] utf8Json) + { + SetInternal(jsonPath, new(utf8Json)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The utf8 json to set. + public void Set(ReadOnlySpan jsonPath, BinaryData utf8Json) + { + SetInternal(jsonPath, new(utf8Json)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The utf8 json to set. + public void Set(ReadOnlySpan jsonPath, ReadOnlySpan utf8Json) + { + SetInternal(jsonPath, new(utf8Json)); + } + + /// + /// Sets a value at the specified JSON path. + /// + /// The JSON path of the value to be set. + /// The value to set. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Set(ReadOnlySpan jsonPath, EncodedValue value) + { + SetInternal(jsonPath, value); + } + + /// + /// Sets NULL at the specified JSON path. + /// + /// The JSON path of the value to be set. + public void SetNull(ReadOnlySpan jsonPath) + { + SetInternal(jsonPath, EncodedValue.Null); + } + #endregion + + #region Get Methods + /// + /// Gets a boolean value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public bool GetBoolean(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out bool boolValue)) + { + return boolValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a byte value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public byte GetByte(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out byte byteValue)) + { + return byteValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a DateTime value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The format the DateTime is in. + /// If the was not found. + public DateTime GetDateTime(ReadOnlySpan jsonPath, StandardFormat format = default) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out DateTime dateTimeValue, format)) + { + return dateTimeValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a DateTimeOffset value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The format the DateTimeOffset is in. + /// If the was not found. + public DateTimeOffset GetDateTimeOffset(ReadOnlySpan jsonPath, StandardFormat format = default) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out DateTimeOffset dateTimeOffsetValue, format)) + { + return dateTimeOffsetValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a decimal value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public decimal GetDecimal(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out decimal decimalValue)) + { + return decimalValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a double value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public double GetDouble(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out double doubleValue)) + { + return doubleValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a float value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public float GetFloat(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out float floatValue)) + { + return floatValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a Guid value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public Guid GetGuid(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out Guid guidValue)) + { + return guidValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a Int32 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public int GetInt32(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out int intValue)) + { + return intValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a Int64 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public long GetInt64(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out long int64Value)) + { + return int64Value; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a Int8 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public sbyte GetInt8(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out sbyte int8Value)) + { + return int8Value; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a Int16 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public short GetInt16(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out short int16Value)) + { + return int16Value; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a TimeSpan value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The format the TimeSpan is in. + /// If the was not found. + public TimeSpan GetTimeSpan(ReadOnlySpan jsonPath, StandardFormat format = default) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out TimeSpan timeSpanValue, format)) + { + return timeSpanValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a UInt32 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public uint GetUInt32(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out uint uintValue)) + { + return uintValue; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a UInt64 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public ulong GetUInt64(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out ulong uint64Value)) + { + return uint64Value; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a UInt16 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public ushort GetUInt16(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeValue(out ushort uint16Value)) + { + return uint16Value; + } + + return ThrowFormatException(jsonPath); + } + + /// + /// Gets a string value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public string? GetString(ReadOnlySpan jsonPath) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) && + encodedValue.Kind != ValueKind.Removed && + encodedValue.TryDecodeValue(out string? stringValue)) + { + return stringValue; + } + + ThrowKeyNotFoundException(jsonPath); + return null; + } + + /// + /// Gets the Utf8 JSON value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// If the was not found. + public BinaryData GetJson(ReadOnlySpan jsonPath) + { + if (!TryGetEncodedValueInternal(jsonPath, out var encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + return new(encodedValue.Value); + } + + /// + /// Gets a nullable primitive value at the specified JSON path. + /// + /// The struct type if its not null. + /// The JSON path of the value to get. + /// If the was not found. + /// If the is not supported. + public T? GetNullableValue(ReadOnlySpan jsonPath) + where T : struct + { + if (!TryGetEncodedValueInternal(jsonPath, out EncodedValue encodedValue) || encodedValue.Kind == ValueKind.Removed) + { + ThrowKeyNotFoundException(jsonPath); + } + + if (encodedValue.TryDecodeNullableValue(out T? value, out bool supportedType)) + { + return value; + } + + if (supportedType) + { + return ThrowFormatException(jsonPath); + } + else + { + return NullableTypeNotSupported(); + } + } + #endregion + + #region TryGet Methods + /// + /// Tries to get a boolean value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out bool value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out bool result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a byte value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out byte value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out byte result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a DateTime value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// The format the DateTime is in. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out DateTime value, StandardFormat format = default) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out DateTime result, format.Symbol)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a DateTimeOffset value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// The format the DateTimeOffset is in. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out DateTimeOffset value, StandardFormat format = default) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out DateTimeOffset result, format.Symbol)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a decimal value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out decimal value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out decimal result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a double value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out double value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out double result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a float value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out float value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out float result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a Guid value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out Guid value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out Guid result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a Int32 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out int value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out int result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a Int64 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out long value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out long result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a Int8 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out sbyte value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out sbyte result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a Int16 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out short value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out short result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a TimeSpan value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// The format the TimeSpan is in. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out TimeSpan value, StandardFormat format = default) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out TimeSpan result, format.Symbol)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a UInt32 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out uint value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out uint result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a UInt64 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out ulong value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out ulong result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a UInt16 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out ushort value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue) + && encodedValue.TryDecodeValue(out ushort result)) + { + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a string value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetValue(ReadOnlySpan jsonPath, out string? value) + { + if (TryGetEncodedValueInternal(jsonPath, out var encodedValue)) + { + encodedValue.TryDecodeValue(out string? result); + value = result; + return true; + } + + value = default; + return false; + } + + /// + /// Tries to get a UInt16 value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetNullableValue(ReadOnlySpan jsonPath, out T? value) + where T : struct + { + value = default; + + if (!TryGetEncodedValueInternal(jsonPath, out EncodedValue encodedValue)) + { + return false; + } + + if (encodedValue.Kind.HasFlag(ValueKind.Null)) + { + return true; + } + + if (encodedValue.TryDecodeNullableValue(out T? result, out _)) + { + value = result; + return true; + } + + return false; + } + + /// + /// Tries to get a string value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool TryGetEncodedValue(ReadOnlySpan jsonPath, out EncodedValue value) + { + return TryGetEncodedValueInternal(jsonPath, out value); + } + + /// + /// Tries to get the Utf8 JSON value at the specified JSON path. + /// + /// The JSON path of the value to get. + /// The value if found. + /// True if the value was found and parsed; otherwise, false. + public bool TryGetJson(ReadOnlySpan jsonPath, out ReadOnlyMemory value) + { + var found = TryGetEncodedValueInternal(jsonPath, out var encodedValue); + value = encodedValue.Value; + return found; + } + #endregion + + #region Append Methods + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, bool value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, byte value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + /// The format to encode the value into. + public void Append(ReadOnlySpan arrayPath, DateTime value, StandardFormat format = default) + { + EncodedValue encodedValue = new(value, format); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + /// The format to encode the value into. + public void Append(ReadOnlySpan arrayPath, DateTimeOffset value, StandardFormat format = default) + { + EncodedValue encodedValue = new(value, format); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, decimal value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, double value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, float value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, Guid value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, int value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, long value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, sbyte value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, short value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + /// The format to encode the value into. + public void Append(ReadOnlySpan arrayPath, TimeSpan value, StandardFormat format = default) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, uint value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, ulong value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, ushort value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The value to append. + public void Append(ReadOnlySpan arrayPath, string value) + { + EncodedValue encodedValue = new(value); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The utf8 json to insert. + public void Append(ReadOnlySpan arrayPath, byte[] utf8Json) + { + EncodedValue encodedValue = new(utf8Json); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The utf8 json to insert. + public void Append(ReadOnlySpan arrayPath, BinaryData utf8Json) + { + EncodedValue encodedValue = new(utf8Json); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends a value to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + /// The utf8 json to insert. + public void Append(ReadOnlySpan arrayPath, ReadOnlySpan utf8Json) + { + EncodedValue encodedValue = new(utf8Json); + encodedValue.Kind |= ValueKind.ArrayItemAppend; + SetInternal(arrayPath, encodedValue); + } + + /// + /// Appends NULL to an array at the specified JSON path. + /// + /// The JSON path pointing at the array. + public void AppendNull(ReadOnlySpan arrayPath) + { + SetInternal(arrayPath, new(ValueKind.ArrayItemAppend, EncodedValue.Null.Value)); + } + #endregion + + /// + /// Removes the value at the specified JSON path. + /// + /// The JSON path of the value to remove. + public void Remove(ReadOnlySpan jsonPath) + { + SetInternal(jsonPath, EncodedValue.Removed); + } + + /// + /// Checks if the value at the specified JSON path has been removed. + /// + /// The JSON path of the value to check. + public bool IsRemoved(ReadOnlySpan jsonPath) + { + if (_properties is null) + { + return false; + } + + return _properties.TryGetValue(jsonPath, out var value) && value.Kind == ValueKind.Removed; + } + + /// + /// Checks if there is a child property of the given prefix that matches the given property name. + /// Allows the caller to avoid concatenating the prefix and property name to check for existence. + /// + /// The prefix JSON path. + /// The property name. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool Contains(ReadOnlySpan prefix, ReadOnlySpan property) + { + if (_properties == null) + { + return false; + } + + Span normalizedPrefix = stackalloc byte[prefix.Length]; + JsonPathComparer.Default.Normalize(prefix, normalizedPrefix, out int bytesWritten); + normalizedPrefix = normalizedPrefix.Slice(0, bytesWritten); + + foreach (var kvp in _properties) + { + ReadOnlySpan keySpan = kvp.Key; + + if (!keySpan.StartsWith(normalizedPrefix)) + { + continue; + } + + if (property.SequenceEqual(keySpan.Slice(normalizedPrefix.Length).GetPropertyNameFromSlice())) + { + return true; + } + } + + return false; + } + + /// + public override string ToString() + => ToString("JP"); + + /// + /// Returns a string representation of the object using the specified format. + /// + /// The format to return 'J' for application/json or 'JP' for application/json-patch+json. + /// A string that represents the object, formatted according to the specified format. + public string ToString(string format) + { + ThrowIfNull(format, nameof(format)); + + switch (format) + { + case "J": + return SerializeToJson(); + case "JP": + return SerializeToJsonPatch(); + default: + return ThrowFormatNotSupportedException(format); + } + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathComparer.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathComparer.cs new file mode 100644 index 000000000000..a6d7389bf6c3 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathComparer.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace System.ClientModel.Primitives; + +internal class JsonPathComparer : IEqualityComparer +#if NET9_0_OR_GREATER + , IAlternateEqualityComparer, byte[]> +#endif +{ + internal static readonly JsonPathComparer Default = new(); + + public bool Equals(byte[]? x, byte[]? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + if (x is null || y is null) + { + return false; + } + return x.AsSpan().SequenceEqual(y.AsSpan()); + } + + public int GetHashCode(byte[] obj) + => GetHashCode(obj.AsSpan()); + + public bool Equals(ReadOnlySpan alternate, byte[] other) + { + if (other is null) + { + return false; + } + + return alternate.SequenceEqual(other.AsSpan()); + } + + public int GetHashCode(ReadOnlySpan alternate) + { +#if NET8_0_OR_GREATER + var hash = new HashCode(); + hash.AddBytes(alternate); + return hash.ToHashCode(); +#else + unchecked + { + int hash = 17; + for (int i = 0; i < alternate.Length; i++) + { + hash = hash * 31 + alternate[i]; + } + return hash; + } +#endif + } + + public byte[] Create(ReadOnlySpan alternate) + => alternate.ToArray(); + + /// + /// There are multiple ways to represent a JSON path. This method normalizes the JSON path to a canonical form that can be used for comparison. + /// As an example $.x is the same as $['x'] and also the same as $["x"]. This method will convert all of these to $.x. + /// In the event that the property name contains a dot that single property will use ['x.y'] format to represent the property name with a dot in it. + /// + /// The json path to convert. + /// Buffer to write the normalized path into. + /// The number of bytes written into . + public void Normalize(ReadOnlySpan jsonPath, Span buffer, out int bytesWritten) + { + ReadOnlySpan localPath = jsonPath; + bytesWritten = 0; + int length = jsonPath.Length; + int block = 0; + bool inBracket = false; + bool needEscapedDot = false; + for (int i = 0; i < length; i++) + { + byte current = jsonPath[i]; + if (current == (byte)'[' && i + 1 < length && (jsonPath[i + 1] == (byte)'\'' || jsonPath[i + 1] == (byte)'"')) + { + localPath.Slice(0, block).CopyTo(buffer.Slice(bytesWritten)); + localPath = localPath.Slice(block + 2); + bytesWritten += block; + block = 0; + buffer[bytesWritten++] = (byte)'.'; + i++; + inBracket = true; + } + else if (current == (byte)']' && i - 1 >= 0 && (jsonPath[i - 1] == (byte)'\'' || jsonPath[i - 1] == (byte)'"')) + { + block--; + localPath.Slice(0, block).CopyTo(buffer.Slice(bytesWritten)); + localPath = localPath.Slice(block + 2); + bytesWritten += block; + block = 0; + if (needEscapedDot) + { + buffer[bytesWritten++] = (byte)'\''; + buffer[bytesWritten++] = (byte)']'; + needEscapedDot = false; + } + inBracket = false; + } + else if (current == (byte)'.' && inBracket && !needEscapedDot) + { + // need to keep the ['(.+)'] format + buffer[bytesWritten - 1] = (byte)'['; + buffer[bytesWritten++] = (byte)'\''; + needEscapedDot = true; + block++; + } + else + { + block++; + } + } + + if (block > 0) + { + localPath.Slice(0, block).CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += block; + } + } + + public int GetNormalizedHashCode(ReadOnlySpan jsonPath) + { + Span buffer = stackalloc byte[jsonPath.Length]; + Normalize(jsonPath, buffer, out int bytesWritten); + buffer = buffer.Slice(0, bytesWritten); + return GetHashCode(buffer); + } + + public bool NormalizedEquals(ReadOnlySpan x, ReadOnlySpan y) + { + // fast path if they are already nomrlaized + if (x.SequenceEqual(y)) + { + return true; + } + + Span bufferX = stackalloc byte[x.Length]; + Span bufferY = stackalloc byte[y.Length]; + Normalize(x, bufferX, out int bytesWrittenX); + Normalize(y, bufferY, out int bytesWrittenY); + if (bytesWrittenX != bytesWrittenY) + { + return false; + } + return bufferX.Slice(0, bytesWrittenX).SequenceEqual(bufferY.Slice(0, bytesWrittenY)); + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathExtensions.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathExtensions.cs new file mode 100644 index 000000000000..cc5644d02c49 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathExtensions.cs @@ -0,0 +1,1105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Buffers; +using System.Buffers.Text; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +namespace System.ClientModel.Primitives; + +internal static class JsonPathExtensions +{ + /// + /// Converts JSON path RFC 9535 to JSON Pointer RFC 6901 format. + /// + /// The JSON path to convert. + /// The buffer to write the JSON pointer into. + /// True if the jsonPath represents an array append operation. + public static int ConvertToJsonPointer(this byte[] jsonPath, Span buffer, bool isArrayAppend = false) + { + ReadOnlySpan jsonPathSpan = jsonPath.AsSpan(); + if (jsonPathSpan.SequenceEqual("$"u8)) + { + if (isArrayAppend) + { + buffer[0] = (byte)'/'; + buffer[1] = (byte)'-'; + return 2; + } + else + { + buffer[0] = (byte)'/'; + return 1; + } + } + + JsonPathReader reader = new JsonPathReader(jsonPathSpan); + int bytesWritten = 0; + + while (reader.Read()) + { + switch (reader.Current.TokenType) + { + case JsonPathTokenType.Root: + break; + case JsonPathTokenType.Property: + buffer[bytesWritten++] = (byte)'/'; + if (reader.Current.ValueSpan.Length > 0) + { + EscapePropertyName(reader.Current.ValueSpan, buffer.Slice(bytesWritten), out int escapedLength); + bytesWritten += escapedLength; + } + break; + case JsonPathTokenType.ArrayIndex: + buffer[bytesWritten++] = (byte)'/'; + int indexLength = reader.Current.ValueSpan.Length; + if (indexLength > 0) + { + reader.Current.ValueSpan.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += indexLength; + } + break; + case JsonPathTokenType.End: + break; + } + } + + if (isArrayAppend) + { + buffer[bytesWritten++] = (byte)'/'; + buffer[bytesWritten++] = (byte)'-'; + } + + return bytesWritten; + } + + private static void EscapePropertyName(ReadOnlySpan propertyName, Span buffer, out int bytesWritten) + { + ReadOnlySpan local = propertyName; + int count = 0; + bytesWritten = 0; + for (int i = 0; i < propertyName.Length; i++) + { + byte c = propertyName[i]; + if (c == (byte)'~') + { + local.Slice(0, count).CopyTo(buffer.Slice(bytesWritten)); + local = local.Slice(count + 1); + bytesWritten += count; + count = 0; + "~0"u8.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += 2; + } + else if (c == (byte)'/') + { + local.Slice(0, count).CopyTo(buffer.Slice(bytesWritten)); + local = local.Slice(count + 1); + bytesWritten += count; + count = 0; + "~1"u8.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += 2; + } + else + { + count++; + } + } + local.Slice(0, count).CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan GetPropertyName(this byte[] jsonPath) + => GetPropertyName(jsonPath.AsSpan()); + + /// + /// Gets the first parent path that is not an array index from the specified JSON path. + /// For example for the JSON path "$.store.book[0].title", this method will return "$.store.book[0].title". + /// If the JSON path is "$.store.book[0][1]", this method will return "$.store.book". + /// + /// The JSON path to process. + public static ReadOnlySpan GetFirstNonIndexParent(this byte[] jsonPath) + { + int index = 0; + ReadOnlySpan newPath = jsonPath.AsSpan(); + while (!newPath.IsRoot()) + { + if (!newPath.IsArrayIndex(out index)) + { + break; + } + newPath = newPath.Slice(0, index + 1); + } + + return newPath; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsArrayIndex(this byte[] jsonPath) + => IsArrayIndex(jsonPath, out _); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsArrayIndex(this ReadOnlySpan jsonPath) + => IsArrayIndex(jsonPath, out _); + + /// + /// Determines whether the specified JSON path segment represents a valid array index and extracts the index + /// position if it does. + /// + /// The JSON path to test. + /// The position of the first digit in the index. + /// True if the is an array index otherwise false. + private static bool IsArrayIndex(this ReadOnlySpan jsonPath, out int indexPosition) + { + indexPosition = 0; + if (jsonPath.Length < 4) + { + return false; + } + + indexPosition = jsonPath.Length - 1; + if (jsonPath[indexPosition] != (byte)']') + { + return false; + } + + while (--indexPosition >= 0 && jsonPath[indexPosition] != (byte)'[') + { + if (!JsonPathReader.IsDigit(jsonPath[indexPosition])) + { + return false; + } + } + + if (indexPosition < 0 || jsonPath[indexPosition] != (byte)'[') + { + return false; + } + + indexPosition--; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan GetFirstProperty(this ReadOnlySpan jsonPath) + => new JsonPathReader(jsonPath).GetFirstProperty(); + + /// + /// Extracts the first property name from a JSON path slice, ignoring any leading root or array index syntax. + /// Does not assume we have a full JSON path, just a slice that may start with a property name. + /// For example, for the JSON path slice ".store.book[0].title", this method will return "store". + /// + /// The slice of a JSON path to process. + public static ReadOnlySpan GetPropertyNameFromSlice(this ReadOnlySpan slice) + { + if (slice.IsEmpty) + { + return slice; + } + + // in case someone does pass in a full path + if (slice[0] == (byte)'$') + { + slice = slice.Slice(1); + } + + if (slice[0] == (byte)'.') + { + slice = slice.Slice(1); + } + + bool indexSyntax = slice[0] == (byte)'['; + int start = indexSyntax ? 2 : 0; + // we assume the slice starts at the property name + for (int i = start; i < slice.Length; i++) + { + byte c = slice[i]; + if (c == (byte)'.' && !indexSyntax) + { + return slice.Slice(start, i - start); + } + if (c == (byte)']' && (indexSyntax ? slice[1] == slice[i - 1] : (slice[i - 1] == (byte)'\'' || slice[i - 1] == (byte)'"'))) + { + return slice.Slice(start, i - 1 - start); + } + if (c == (byte)'[' && !indexSyntax) + { + return slice.Slice(start, i); + } + } + + return slice.Slice(start); + } + + /// + /// Gets the property name of the current JSON path. + /// For example for the JSON path "$.store.book[0].title", this method will return "title". + /// + /// The JSON path to get the property name from. + /// If the does not start with '$'. + public static ReadOnlySpan GetPropertyName(this ReadOnlySpan jsonPath) + { + if (jsonPath.IsEmpty || jsonPath[0] != (byte)'$') + { + throw new ArgumentException("JsonPath must start with '$'", nameof(jsonPath)); + } + + JsonPathReader reader = new JsonPathReader(jsonPath); + JsonPathToken lastToken = default; + while (reader.Read() && reader.Current.TokenType != JsonPathTokenType.End) + { + if (reader.Current.TokenType == JsonPathTokenType.Property) + { + lastToken = reader.Current; + } + } + + return lastToken.TokenType == JsonPathTokenType.Property + ? lastToken.ValueSpan + : ReadOnlySpan.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan GetParent(this byte[] jsonPath) + => GetParent(jsonPath.AsSpan()); + + /// + /// Gets the parent JSON path of the specified JSON path. + /// For example for the JSON path "$.store.book[0].title", this method will return "$.store.book[0]". + /// + /// The JSON path to get the parent of. + /// If the does not start with '$'. + public static ReadOnlySpan GetParent(this ReadOnlySpan jsonPath) + { + if (jsonPath.IsEmpty || jsonPath[0] != (byte)'$') + { + throw new ArgumentException("JsonPath must start with '$'", nameof(jsonPath)); + } + + if (jsonPath.Length == 1) + { + return jsonPath; + } + + if (jsonPath.IsArrayIndex()) + { + Span arrayInsert = stackalloc byte[jsonPath.Length]; + int openBracketIndex = jsonPath.LastIndexOf((byte)'['); + jsonPath.Slice(0, openBracketIndex).CopyTo(arrayInsert); + return arrayInsert.Slice(0, openBracketIndex).ToArray(); + } + + bool inBracket = false; + for (int i = jsonPath.Length - 1; i >= 1; i--) + { + byte c = jsonPath[i]; + + if (c == (byte)'[') + { + return jsonPath.Slice(0, i); + } + else if (c == (byte)']') + { + inBracket = true; + } + else if (c == (byte)'.' && !inBracket) + { + return jsonPath.Slice(0, i); + } + } + + return ReadOnlySpan.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan GetIndexSpan(this byte[] jsonPath) + => GetIndexSpan(jsonPath.AsSpan()); + + /// + /// Gets the index span of the current JSON path if it is an array index. + /// For example for the JSON path "$.store.book[0]", this method will return "0". + /// + /// The JSON path to process. + /// If the does not start with '$'. + public static ReadOnlySpan GetIndexSpan(this ReadOnlySpan jsonPath) + { + if (jsonPath.IsEmpty || jsonPath[0] != (byte)'$') + { + throw new ArgumentException("JsonPath must start with '$'", nameof(jsonPath)); + } + + int index = jsonPath.Length - 1; + if (jsonPath[index] != (byte)']') + { + return Array.Empty(); + } + + while (--index >= 0 && jsonPath[index] != (byte)'[') + { + //skip to array start + } + + if (index < 0 || jsonPath[index] != (byte)'[') + { + return Array.Empty(); + } + + var indexSpan = jsonPath.Slice(index + 1); + return indexSpan[0] == (byte)'\'' || indexSpan[0] == (byte)'"' + ? Array.Empty() + : indexSpan.Slice(0, indexSpan.Length - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRoot(this byte[] jsonPath) + => IsRoot(jsonPath.AsSpan()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRoot(this Span jsonPath) + => IsRoot((ReadOnlySpan)jsonPath); + + /// + /// Gets whether or not the specified JSON path represents the root of the JSON document. + /// + /// The JSON path to test. + /// True if is the root '$' otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRoot(this ReadOnlySpan jsonPath) + => "$"u8.SequenceEqual(jsonPath); + + /// + /// Removes the JSON value at the specified JSON path from the JSON byte array. + /// + /// The UTF8 json to modify. + /// The JSON path to remove. + /// The modified JSON excluding the value found at . + public static byte[] Remove(this ReadOnlyMemory json, ReadOnlySpan jsonPath) + { + if (jsonPath.IsRoot()) + { + return Array.Empty(); + } + + Find(json.Span, jsonPath, out Utf8JsonReader jsonReader, out Utf8JsonReader last); + + long endLeft = jsonReader.TokenStartIndex; + jsonReader.Skip(); + jsonReader.Read(); + long startRight = jsonReader.TokenStartIndex; + + if (jsonReader.TokenType == JsonTokenType.EndObject && !IsFirstProperty(json, jsonPath)) + { + endLeft = last.BytesConsumed; //remove trailing comma / whitespace + } + + if (jsonReader.TokenType == JsonTokenType.EndArray && !IsAtFirstIndex(json, jsonReader.TokenStartIndex)) + { + endLeft = last.BytesConsumed; //remove trailing comma / whitespace + } + + return [.. json.Slice(0, (int)endLeft).Span, .. json.Slice((int)startRight).Span]; + } + + private static bool IsFirstProperty(ReadOnlyMemory json, ReadOnlySpan jsonPath) + { + Find(json.Span, jsonPath.GetParent(), out var jsonReader); + + if (jsonReader.TokenType != JsonTokenType.StartObject) + { + return false; + } + + jsonReader.Read(); + + return jsonReader.TokenType == JsonTokenType.PropertyName && + jsonReader.ValueSpan.SequenceEqual(jsonPath.GetPropertyName()); + } + + /// + /// Sets the value of the JSON at the specified JSON path to the provided replacement JSON value. + /// If the does not exist in the original JSON, it will be added with the value. + /// + /// The original UTF8 JSON. + /// The JSON path to replace. + /// The new UTF8 JSON to replace. + /// The modified JSON which has the at the . + public static byte[] Set(this ReadOnlyMemory json, ReadOnlySpan jsonPath, ReadOnlyMemory jsonReplacement) + { + bool found = TryFind(json.Span, jsonPath, out Utf8JsonReader jsonReader); + return jsonReader.SetCurrentValue(found, jsonPath.GetPropertyName(), json, jsonReplacement); + } + + /// + /// Sets the value of the JSON at the current position of the to the provided replacement JSON value. + /// + /// The pointing at the position to insert. + /// Indicates if the value previously existed and should be replaced. + /// The name of the property insert. + /// The original UTF8 JSON. + /// The new value to insert at the current position. + /// The modified JSON which has inserted at the current position of . + public static byte[] SetCurrentValue(this Utf8JsonReader jsonReader, bool wasFound, ReadOnlySpan propertyName, ReadOnlyMemory json, ReadOnlyMemory jsonReplacement) + { + if (wasFound) + { + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + //move to the value + jsonReader.Read(); + } + + long endLeft = jsonReader.TokenStartIndex; + jsonReader.Skip(); + var atEnd = !jsonReader.Read(); + long startRight = atEnd ? jsonReader.BytesConsumed : jsonReader.TokenStartIndex; + + if (jsonReader.TokenType == JsonTokenType.EndObject || jsonReader.TokenType == JsonTokenType.EndArray || endLeft == jsonReader.TokenStartIndex) + { + return [.. json.Slice(0, (int)endLeft).Span, .. jsonReplacement.Span, .. json.Slice((int)startRight).Span]; + } + else + { + return [.. json.Slice(0, (int)endLeft).Span, .. jsonReplacement.Span, (byte)',', .. json.Slice((int)startRight).Span]; + } + } + else + { + return jsonReader.Insert(json, propertyName, jsonReplacement); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsArrayWrapped(this ReadOnlyMemory json) + => IsArrayWrapped(json.Span); + + /// + /// Determines whether or not the specified JSON byte array is wrapped in an array (i.e. starts with '[' and ends with ']'). + /// + /// The UTF8 JSON to test. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsArrayWrapped(this ReadOnlySpan json) + { + return json.Length >= 2 && json[0] == (byte)'[' && json[json.Length - 1] == (byte)']'; + } + + private static ReadOnlyMemory FillAt(this ReadOnlyMemory json, ReadOnlySpan arrayPath) + { + int parentIndex = 0; + Utf8Parser.TryParse(arrayPath.GetIndexSpan(), out int index, out _); + ReadOnlySpan arrayParent = arrayPath.GetParent(); + bool needsWrap = false; + if (arrayParent.IsArrayIndex()) + { + needsWrap = true; + Utf8Parser.TryParse(arrayParent.GetIndexSpan(), out parentIndex, out _); + json = FillAt(json, arrayParent); + } + ReadOnlySpan jsonSpan = json.Span; + Utf8JsonReader jsonReader = new(jsonSpan); + JsonPathReader pathReader = new(arrayParent); + + if (!jsonReader.Advance(ref pathReader)) + { + return json; + } + + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(); // Move to the value after the property name + } + + int nextIndex = 0; + + Utf8JsonReader jsonReaderCopy = jsonReader; + if (jsonReaderCopy.SkipToIndex(index, out nextIndex)) + { + return json; + } + + int gap = index - nextIndex; + byte[] rented = ArrayPool.Shared.Rent(gap * 5 + 6); + var rentedSpan = rented.AsSpan(); + int length = 0; + + if (needsWrap) + { + rentedSpan[0] = (byte)'['; + rentedSpan = rentedSpan.Slice(1); + length++; + } + + for (int i = 0; i < gap; i++) + { + "null,"u8.CopyTo(rentedSpan); + rentedSpan = rentedSpan.Slice(5); + length += 5; + } + "null"u8.CopyTo(rentedSpan); + length += 4; + + if (needsWrap) + { + rentedSpan = rentedSpan.Slice(4); + rentedSpan[0] = (byte)']'; + length++; + } + + ReadOnlyMemory result; + if (needsWrap) + { + result = jsonReader.SetCurrentValue(true, ReadOnlySpan.Empty, json, new(rented, 0, length)); + } + else + { + result = jsonReaderCopy.Insert(json, ReadOnlySpan.Empty, new(rented, 0, length), nextIndex > 0); + } + + ArrayPool.Shared.Return(rented); + return result; + } + + /// + /// Inserts the specified JSON value at the specified array path in the original JSON byte array. + /// If the array at the specified path does not exist, it will be created and filled with null values up to the specified index. + /// + /// The original UTF8 JSON. + /// The JSON path pointing to the array index to be inserted. + /// The UTF8 JSON to insert. + /// The modified JSON with the inserted at the . + /// If the parent of is not an array. + public static byte[] InsertAt(this ReadOnlyMemory json, ReadOnlySpan arrayPath, ReadOnlyMemory jsonToInsert) + { + int parentIndex = 0; + Utf8Parser.TryParse(arrayPath.GetIndexSpan(), out int index, out _); + ReadOnlySpan arrayParent = arrayPath.GetParent(); + if (arrayParent.IsArrayIndex()) + { + Utf8Parser.TryParse(arrayParent.GetIndexSpan(), out parentIndex, out _); + json = FillAt(json, arrayParent); + } + ReadOnlySpan jsonSpan = json.Span; + Utf8JsonReader jsonReader = new(jsonSpan); + JsonPathReader pathReader = new(arrayParent); + + ReadOnlyMemory insert = jsonReader.Advance(ref pathReader) && jsonToInsert.IsArrayWrapped() + ? jsonToInsert.Slice(1, jsonToInsert.Length - 2) + : jsonToInsert; + + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(); // Move to the value after the property name + } + + if (jsonReader.TokenType != JsonTokenType.StartArray && jsonReader.TokenType != JsonTokenType.Null) + { + throw new FormatException($"{Encoding.UTF8.GetString(arrayParent.ToArray())} is not an array."); + } + + bool needsWrap = jsonReader.TokenType == JsonTokenType.Null; + + int nextIndex = 0; + + if (!needsWrap) + { + insert = jsonReader.SkipToIndex(index, out nextIndex) && jsonReader.TokenType != JsonTokenType.Null && jsonToInsert.IsArrayWrapped() + ? jsonToInsert.Slice(1, jsonToInsert.Length - 2) + : jsonToInsert; + } + + if (index > nextIndex) + { + int gap = index - nextIndex; + byte[] rented = ArrayPool.Shared.Rent(gap * 5); + var rentedSpan = rented.AsSpan(); + for (int i = 0; i < gap; i++) + { + "null,"u8.CopyTo(rentedSpan); + rentedSpan = rentedSpan.Slice(5); + } + insert = needsWrap + ? new([(byte)'[', .. rented.AsSpan(0, 5 * gap), .. insert.Span, (byte)']']) + : new([.. rented.AsSpan(0, 5 * gap), .. insert.Span]); + ArrayPool.Shared.Return(rented); + } + else if (needsWrap) + { + insert = new([(byte)'[', .. insert.Span, (byte)']']); + } + + if (jsonReader.TokenType == JsonTokenType.Null) + { + return jsonReader.SetCurrentValue(true, ReadOnlySpan.Empty, json, insert); + } + else + { + return jsonReader.Insert(json, ReadOnlySpan.Empty, insert, needsWrap ? parentIndex > 0 : nextIndex > 0); + } + } + + /// + /// Appends the specified JSON value to the end of the array at the specified array path in the original JSON byte array. + /// + /// The original UTF8 JSON. + /// The JSON path to the array. + /// The UTF8 JSON to insert. + /// The modified JSON with the appended to the end of . + public static byte[] Append(this ReadOnlyMemory json, ReadOnlySpan arrayPath, ReadOnlyMemory jsonToInsert) + { + Find(json.Span, arrayPath, out Utf8JsonReader jsonReader); + return jsonReader.Insert(json, ReadOnlySpan.Empty, jsonToInsert); + } + + /// + /// Inserts the specified JSON value at the current position of the in the original JSON byte array. + /// + /// The pointing at the place to insert. + /// The original UTF8 JSON. + /// The property name to insert. + /// The UTF8 JSON to insert. + /// Indicates if the array was originally empty if is pointing inside an array. + /// The modified JSON with the inserted at the current position of . + internal static byte[] Insert( + ref this Utf8JsonReader jsonReader, + ReadOnlyMemory json, + ReadOnlySpan propertyName, + ReadOnlyMemory jsonToInsert, + bool hasPreviousItems = false) + { + long endLeft; + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + //move to the value + jsonReader.Read(); + } + + if (jsonReader.TokenType == JsonTokenType.StartArray) + { + //skip to the end of the array + while (jsonReader.Read() && jsonReader.TokenType != JsonTokenType.EndArray) + { + hasPreviousItems = true; + jsonReader.Skip(); + } + } + + endLeft = jsonReader.TokenStartIndex; + if (jsonReader.TokenType == JsonTokenType.EndArray) + { + if (!hasPreviousItems) + { + return + [ + .. json.Slice(0, (int)endLeft).Span, + .. jsonToInsert.Span, + .. json.Slice((int)endLeft).Span + ]; + } + else + { + return + [ + .. json.Slice(0, (int)endLeft).Span, + (byte)',', + .. jsonToInsert.Span, + .. json.Slice((int)endLeft).Span + ]; + } + } + + if (jsonReader.TokenType == JsonTokenType.StartObject) + { + //skip to the end of the object + while (jsonReader.Read() && jsonReader.TokenType != JsonTokenType.EndObject) + { + hasPreviousItems = true; + jsonReader.Skip(); + } + } + else if (jsonReader.TokenType == JsonTokenType.EndObject) + { + hasPreviousItems = !IsEmptyObject(json, jsonReader.TokenStartIndex); + } + + endLeft = jsonReader.TokenStartIndex; + var buffer = ArrayPool.Shared.Rent(json.Length + propertyName.Length + jsonToInsert.Length + 5); + var shared = buffer.AsSpan(); + var left = json.Slice(0, (int)endLeft); + left.Span.CopyTo(shared); + shared = shared.Slice(left.Length); + int length = left.Length; + + if (hasPreviousItems && !propertyName.IsEmpty) + { + shared[0] = (byte)','; + shared = shared.Slice(1); + length++; + } + if (!propertyName.IsEmpty) + { + shared[0] = (byte)'"'; + propertyName.CopyTo(shared.Slice(1)); + shared = shared.Slice(1 + propertyName.Length); + "\":"u8.CopyTo(shared); + shared = shared.Slice(2); + length += propertyName.Length + 3; + } + + jsonToInsert.Span.CopyTo(shared); + shared = shared.Slice(jsonToInsert.Length); + length += jsonToInsert.Length; + + // if I inserted into an array I need a trailing comma if there are more items to come after this one + if (propertyName.IsEmpty && + jsonReader.TokenType != JsonTokenType.EndArray) + { + shared[0] = (byte)','; + shared = shared.Slice(1); + length++; + } + + var right = json.Slice((int)endLeft); + right.Span.CopyTo(shared); + shared = shared.Slice(right.Length); + length += right.Length; + + var result = buffer.AsSpan(0, length).ToArray(); + ArrayPool.Shared.Return(buffer); + return result; + } + + private static bool IsAtFirstIndex(ReadOnlyMemory json, long tokenStartIndex) + { + for (int i = (int)tokenStartIndex - 1; i >= 0; i--) + { + byte c = json.Span[i]; + if (c == (byte)'[') + { + return true; + } + if (c != (byte)' ' && c != (byte)'\r' && c != (byte)'\n' && c != (byte)'\t') + { + return false; + } + } + return false; + } + + private static bool IsEmptyObject(ReadOnlyMemory json, long tokenStartIndex) + { + for (int i = (int)tokenStartIndex - 1; i >= 0; i--) + { + byte c = json.Span[i]; + if (c == (byte)'{') + { + return true; + } + + if (c != (byte)' ' && c != (byte)'\r' && c != (byte)'\n' && c != (byte)'\t') + { + return false; + } + } + return false; + } + + /// + /// Tries to get the JSON value at the specified JSON path from the original JSON byte array. + /// + /// The UTF8 JSON to search in. + /// The JSON path to find. + /// The UTF8 JSON value at if it exists. + /// True if the was found in otherwise false. + public static bool TryGetJson(this ReadOnlyMemory json, ReadOnlySpan jsonPath, out ReadOnlyMemory target) + { + target = ReadOnlyMemory.Empty; + + if (!TryFind(json.Span, jsonPath, out Utf8JsonReader jsonReader)) + { + return false; + } + + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(); // Move to the value after the property name + } + + long start = jsonReader.TokenStartIndex; + jsonReader.Skip(); + long end = jsonReader.BytesConsumed; + // drop wrapping quotes for strings + if (json.Span[(int)start] == (byte)'"' && json.Span[(int)(end - 1)] == (byte)'"') + { + start++; + end--; + } + target = json.Slice((int)start, (int)(end - start)); + return true; + } + + /// + /// Gets the JSON value at the specified JSON path from the original JSON byte array. + /// + /// The UTF8 JSON to search in. + /// The JSON path to find. + /// If was not found in . + public static ReadOnlyMemory GetJson(this ReadOnlyMemory json, ReadOnlySpan jsonPath) + { + if (!TryGetJson(json, jsonPath, out var target)) + { + throw new InvalidOperationException($"{Encoding.UTF8.GetString(jsonPath.ToArray())} was not found in the JSON structure."); + } + return target; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Find(ReadOnlySpan json, ReadOnlySpan jsonPath, out Utf8JsonReader jsonReader) + => Find(json, jsonPath, out jsonReader, out _); + + private static void Find(ReadOnlySpan json, ReadOnlySpan jsonPath, out Utf8JsonReader jsonReader, out Utf8JsonReader last) + { + if (!TryFind(json, jsonPath, out jsonReader, out last)) + { + throw new InvalidOperationException($"{Encoding.UTF8.GetString(jsonPath.ToArray())} was not found in the JSON structure."); + } + } + + private static bool TryFind(ReadOnlySpan json, ReadOnlySpan jsonPath, out Utf8JsonReader jsonReader) + { + Utf8JsonReader last = default; + return TryFind(json, jsonPath, out jsonReader, out last); + } + + private static bool TryFind(ReadOnlySpan json, ReadOnlySpan jsonPath, out Utf8JsonReader jsonReader, out Utf8JsonReader last) + { + jsonReader = default; + last = default; + + if (json.IsEmpty) + { + return jsonPath.IsEmpty; + } + + jsonReader = new Utf8JsonReader(json); + + return jsonReader.Advance(jsonPath, ref last); + } + + /// + /// Gets the length of the array at the specified JSON path from the original JSON byte array. + /// + /// holding the JSON to read. + /// The JSON path to search for. + /// If is not an array or if was not found in . + public static int GetArrayLength(ref this Utf8JsonReader jsonReader, ReadOnlySpan arrayPath) + { + if (!jsonReader.Advance(arrayPath)) + { + throw new InvalidOperationException($"{Encoding.UTF8.GetString(arrayPath.ToArray())} was not found in the JSON structure."); + } + + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(); // Move to the value after the property name + } + + if (jsonReader.TokenType != JsonTokenType.StartArray) + { + throw new InvalidOperationException($"{Encoding.UTF8.GetString(arrayPath.ToArray())} is not an array."); + } + + int length = 0; + while (jsonReader.Read() && jsonReader.TokenType != JsonTokenType.EndArray) + { + length++; + jsonReader.Skip(); + } + + return length; + } + + /// + /// Advances the to the position specified by the provided JSON path. + /// + /// The to advance. + /// The JSON path to advance to. + /// The last state of . + /// If does not start with '$'. + public static bool Advance(ref this Utf8JsonReader jsonReader, ReadOnlySpan jsonPath, ref Utf8JsonReader last) + { + if (jsonPath.IsEmpty || jsonPath[0] != (byte)'$') + { + throw new ArgumentException("JsonPath must start with '$'", nameof(jsonPath)); + } + + JsonPathReader pathReader = new(jsonPath); + return jsonReader.Advance(ref pathReader, ref last); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Advance(ref this Utf8JsonReader jsonReader, ReadOnlySpan jsonPath) + { + Utf8JsonReader last = default; + return jsonReader.Advance(jsonPath, ref last); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Advance(ref this Utf8JsonReader jsonReader, ref JsonPathReader pathReader) + { + Utf8JsonReader last = default; + return jsonReader.Advance(ref pathReader, ref last); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Read(ref this Utf8JsonReader jsonReader, ref Utf8JsonReader last) + { + last = jsonReader; + return jsonReader.Read(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Skip(ref this Utf8JsonReader jsonReader, ref Utf8JsonReader last) + { + last = jsonReader; + jsonReader.Skip(); + } + + /// + /// Advances the to the position specified by the provided . + /// + /// The to advance. + /// The that holds the JSON path to find. + /// The previous state of . + public static bool Advance(ref this Utf8JsonReader jsonReader, ref JsonPathReader pathReader, ref Utf8JsonReader last) + { + while (pathReader.Read()) + { + switch (pathReader.Current.TokenType) + { + case JsonPathTokenType.Root: + if (!jsonReader.Read(ref last)) + { + return false; + } + break; + + case JsonPathTokenType.Property: + if (!SkipToProperty(ref jsonReader, pathReader, ref last)) + { + return false; + } + break; + + case JsonPathTokenType.ArrayIndex: + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(ref last); // Move to the value after the property name + } + + if (jsonReader.TokenType != JsonTokenType.StartArray) + { + return false; + } + + var indexSpan = pathReader.Current.ValueSpan; + if (!Utf8Parser.TryParse(indexSpan, out int indexToFind, out _)) + { + return false; + } + + if (!jsonReader.SkipToIndex(indexToFind, out _, ref last)) + { + return false; + } + + break; + + case JsonPathTokenType.PropertySeparator: + if (jsonReader.TokenType == JsonTokenType.PropertyName) + { + jsonReader.Read(ref last); + } + break; + + case JsonPathTokenType.End: + return true; + + default: + return false; + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool SkipToIndex(ref this Utf8JsonReader jsonReader, int indexToFind, out int nextIndex) + { + Utf8JsonReader last = default; + return jsonReader.SkipToIndex(indexToFind, out nextIndex, ref last); + } + + /// + /// Given a positioned at the start of an array, skips to the specified index in the array. + /// + /// The pointed at an array. + /// The index to skip to. + /// The next index an item will be added to. + /// The last state of . + /// Try if the index was found otherwise false. + internal static bool SkipToIndex(ref this Utf8JsonReader jsonReader, int indexToFind, out int nextIndex, ref Utf8JsonReader last) + { + nextIndex = 0; + + while (jsonReader.Read(ref last)) + { + if (jsonReader.TokenType == JsonTokenType.EndArray) + { + return false; + } + + if (nextIndex == indexToFind) + { + break; + } + else + { + // Skip the value + jsonReader.Skip(ref last); + } + nextIndex++; + } + + return true; + } + + private static bool SkipToProperty(ref Utf8JsonReader jsonReader, JsonPathReader pathReader, ref Utf8JsonReader last) + { + if (jsonReader.TokenType != JsonTokenType.StartObject) + { + return false; + } + + while (jsonReader.Read(ref last)) + { + if (jsonReader.TokenType == JsonTokenType.EndObject) + { + return false; + } + + if (jsonReader.TokenType == JsonTokenType.PropertyName && + jsonReader.ValueSpan.SequenceEqual(pathReader.Current.ValueSpan)) + { + return true; + } + else + { + // Skip the value + jsonReader.Skip(ref last); + } + } + + return false; + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathReader.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathReader.cs new file mode 100644 index 000000000000..f65d83fd34e7 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonPathReader.cs @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.ClientModel.Primitives; + +internal enum JsonPathTokenType +{ + Root, // $ + PropertySeparator, // . or [' or [" + Property, // propertyName + ArrayIndex, // [(\d+)] + End +} + +internal readonly ref struct JsonPathToken +{ + public int TokenStartIndex { get; } + public JsonPathTokenType TokenType { get; } + public ReadOnlySpan ValueSpan { get; } + + public JsonPathToken(JsonPathTokenType tokenType, int tokenStartIndex, ReadOnlySpan valueSpan = default) + { + TokenType = tokenType; + ValueSpan = valueSpan; + TokenStartIndex = tokenStartIndex; + } +} + +/// +/// Reads a JsonPath expression and breaks it into tokens for processing. +/// Follows RFC 9535 minus the filter expressions and wildcards, which are not supported in this implementation. +/// +internal ref struct JsonPathReader +{ + private ref struct PeekedJsonPathToken + { + public JsonPathToken Token; + public bool HasValue; + public int Consumed; + } + + private const byte DollarSign = 36; // '$' + private const byte Dot = 46; // '.' + private const byte OpenBracket = 91; // '[' + private const byte CloseBracket = 93; // ']' + private const byte SingleQuote = 39; // '\'' + private const byte DoubleQuote = 34; // '"' + private const byte Zero = 48; // '0' + private const byte Nine = 57; // '9' + + private readonly ReadOnlySpan _jsonPath; + private int _consumed; + private int _length; + private PeekedJsonPathToken _peeked; + + public JsonPathToken Current { get; private set; } + + public JsonPathReader(string jsonPath) + : this(Encoding.UTF8.GetBytes(jsonPath).AsSpan()) { } + + public JsonPathReader(ReadOnlySpan jsonPath) + { + _jsonPath = jsonPath; + _consumed = 0; + _length = jsonPath.Length; + } + + public bool Read() + { + if (_peeked.HasValue) + { + Current = _peeked.Token; + _consumed = _peeked.Consumed; + _peeked.HasValue = false; + return true; + } + + if (Current.TokenType == JsonPathTokenType.End) + { + return false; + } + + if (_consumed >= _length) + { + Current = new JsonPathToken(JsonPathTokenType.End, _length - 1); + return true; + } + + byte current = _jsonPath[_consumed]; + + switch (current) + { + case DollarSign: + var tokenSlice = _jsonPath.Slice(_consumed, 1); + Current = new JsonPathToken(JsonPathTokenType.Root, _consumed++, tokenSlice); + return true; + + case Dot: + Current = new JsonPathToken(JsonPathTokenType.PropertySeparator, _consumed++); + return true; + + case OpenBracket: + // if the next byte is '\'', or '"', it's a quoted string, otherwise it's an array start + if (_consumed + 1 >= _length) + { + throw new FormatException($"Invalid JsonPath syntax at position {_consumed + 1}: expected a property or positive array index after '['"); + } + + var next = _jsonPath[_consumed + 1]; + if (next == SingleQuote || next == DoubleQuote) + { + Current = new JsonPathToken(JsonPathTokenType.PropertySeparator, _consumed++); + return true; + } + + if (!IsDigit(next)) + { + throw new FormatException($"Invalid JsonPath syntax at position {_consumed + 1}: expected a property or positive array index after '['"); + } + + _consumed++; // Skip '[' + Current = ReadNumber(); + if (_consumed >= _length || _jsonPath[_consumed] != CloseBracket) + { + throw new FormatException($"Invalid JsonPath syntax at position {_consumed}: expected ']' after number"); + } + + _consumed++; // Skip closing bracket + return true; + + case SingleQuote or DoubleQuote: + if (Current.TokenType == JsonPathTokenType.PropertySeparator && _jsonPath[_consumed - 1] == Dot) + { + Current = ReadProperty(); + } + else + { + Current = ReadQuotedString(); + if (_consumed >= _length || _jsonPath[_consumed] != CloseBracket) + { + throw new FormatException($"Invalid JsonPath syntax at position {_consumed}: expected ']' after quoted string"); + } + + _consumed++; // Skip closing bracket + } + return true; + + default: + // Read a property name + Current = ReadProperty(); + return true; + } + } + + public JsonPathToken Peek() + { + if (Current.TokenType == JsonPathTokenType.End) + { + return Current; + } + + if (_peeked.HasValue) + { + return _peeked.Token; + } + + JsonPathReader local = this; + local.Read(); + _peeked.HasValue = true; + _peeked.Consumed = local._consumed; + _peeked.Token = local.Current; + + return _peeked.Token; + } + + public ReadOnlySpan GetFirstProperty() + { + if (_jsonPath.IsRoot()) + { + return _jsonPath; + } + + if (!Read()) + { + return ReadOnlySpan.Empty; + } + + if (Current.TokenType != JsonPathTokenType.Root) + { + return ReadOnlySpan.Empty; + } + + if (!Read()) + { + return ReadOnlySpan.Empty; + } + + if (Current.TokenType == JsonPathTokenType.PropertySeparator && !Read()) + { + return ReadOnlySpan.Empty; + } + + switch (Current.TokenType) + { + case JsonPathTokenType.Property: + return _jsonPath.Slice(0, _consumed); + + case JsonPathTokenType.ArrayIndex: + return _jsonPath.Slice(0, _consumed); + + default: + return ReadOnlySpan.Empty; + } + } + + public ReadOnlySpan GetNextArray() + { + while (Read()) + { + if (Current.TokenType == JsonPathTokenType.ArrayIndex) + { + return _jsonPath.Slice(0, _consumed); + } + } + + return ReadOnlySpan.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private JsonPathToken ReadQuotedString() + { + var localJsonPath = _jsonPath; + byte quote = localJsonPath[_consumed]; + Debug.Assert(quote == SingleQuote || quote == DoubleQuote, "ReadQuotedString should only be called if _consumed is pointing at a single or double quote character"); + int initial = ++_consumed; // Skip opening quote + + while (_consumed < _length) + { + if (localJsonPath[_consumed] == quote && _consumed + 1 < _length && localJsonPath[_consumed + 1] == ']') + { + break; + } + + _consumed++; + } + + if (_consumed >= _length) + { + throw new FormatException("Unterminated quoted string in JsonPath"); + } + + var value = localJsonPath.Slice(initial, _consumed - initial); + _consumed++; // Skip closing quote + return new JsonPathToken(JsonPathTokenType.Property, initial, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private JsonPathToken ReadNumber() + { + var localJsonPath = _jsonPath; + int initial = _consumed; + while (_consumed < _length && IsDigit(localJsonPath[_consumed])) + { + _consumed++; + } + + return new JsonPathToken(JsonPathTokenType.ArrayIndex, initial, localJsonPath.Slice(initial, _consumed - initial)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private JsonPathToken ReadProperty() + { + var localJsonPath = _jsonPath; + int initial = _consumed; + while (_consumed < _length && localJsonPath[_consumed] is not Dot and not OpenBracket and not CloseBracket) + { + _consumed++; + } + + return new JsonPathToken(JsonPathTokenType.Property, initial, localJsonPath.Slice(initial, _consumed - initial)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDigit(byte b) => b >= Zero && b <= Nine; + + public bool Advance(ReadOnlySpan prefix) + { + if (prefix.IsEmpty || _jsonPath.IsEmpty) + { + return false; + } + + if (prefix.Length >= _length) + { + return false; + } + + if (_jsonPath.StartsWith(prefix)) + { + _consumed = prefix.Length; + return true; + } + + if (prefix[0] != DollarSign || _jsonPath[0] != DollarSign) + { + return false; + } + + JsonPathReader prefixReader = new(prefix); + + while (Read() && prefixReader.Read()) + { + if (!Current.ValueSpan.SequenceEqual(prefixReader.Current.ValueSpan)) + { + return false; + } + } + + return prefixReader.Current.TokenType == JsonPathTokenType.End && Current.TokenType != JsonPathTokenType.End; + } + + internal ReadOnlySpan GetParsedPath() + { + if (_consumed == 0) + { + return ReadOnlySpan.Empty; + } + + return _jsonPath.Slice(0, _consumed); + } + + public bool Equals(JsonPathReader other) + { + return JsonPathComparer.Default.NormalizedEquals(_jsonPath, other._jsonPath); + } + + public override int GetHashCode() + { + return JsonPathComparer.Default.GetNormalizedHashCode(_jsonPath); + } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/UnsafeBufferSequence.Reader.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/UnsafeBufferSequence.Reader.cs index d2b38284aa2a..10d1c32fa499 100644 --- a/sdk/core/System.ClientModel/src/ModelReaderWriter/UnsafeBufferSequence.Reader.cs +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/UnsafeBufferSequence.Reader.cs @@ -100,6 +100,12 @@ public BinaryData ToBinaryData() { throw new InvalidOperationException($"Length of serialized model is too long. Value was {length} max is {int.MaxValue}"); } + if (_count == 1) + { + var arr = new byte[_buffers[0].Written]; + Buffer.BlockCopy(_buffers[0].Array, 0, arr, 0, _buffers[0].Written); + return new BinaryData(arr); + } using var stream = new MemoryStream((int)length); CopyTo(stream, default); return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); diff --git a/sdk/core/System.ClientModel/src/NetStandardPolyfill/ExperimentalAttribute.cs b/sdk/core/System.ClientModel/src/NetStandardPolyfill/ExperimentalAttribute.cs new file mode 100644 index 000000000000..5d7d2786f08f --- /dev/null +++ b/sdk/core/System.ClientModel/src/NetStandardPolyfill/ExperimentalAttribute.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if !NET8_0_OR_GREATER +#nullable enable + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that an API is experimental and it may change in the future. +/// +/// +/// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental +/// feature is used. Authors can use this attribute to ship preview features in their assemblies. +/// +[AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, Inherited = false)] +internal sealed class ExperimentalAttribute : Attribute +{ + /// + /// Initializes a new instance of the class, specifying the ID that the compiler will use + /// when reporting a use of the API the attribute applies to. + /// + /// The ID that the compiler will use when reporting a use of the API the attribute applies to. + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + /// + /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. + /// + /// The unique diagnostic ID. + /// + /// The diagnostic ID is shown in build output for warnings and errors. + /// This property represents the unique ID that can be used to suppress the warnings or errors, if needed. + /// + public string DiagnosticId { get; } + + /// + /// Gets or sets the URL for corresponding documentation. + /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. + /// + /// The format string that represents a URL to corresponding documentation. + /// An example format string is https://contoso.com/obsoletion-warnings/{0}. + public string? UrlFormat { get; set; } +} + +#endif // !NET8_0_OR_LATER diff --git a/sdk/core/System.ClientModel/src/System.ClientModel.csproj b/sdk/core/System.ClientModel/src/System.ClientModel.csproj index 1f5f8f4cc437..3bbd0d9bda8b 100644 --- a/sdk/core/System.ClientModel/src/System.ClientModel.csproj +++ b/sdk/core/System.ClientModel/src/System.ClientModel.csproj @@ -6,10 +6,11 @@ 1.6.1 enable - netstandard2.0;net8.0 + netstandard2.0;net8.0;net9.0 DotNetPackageIcon.png $(RepoEngPath)/images/$(PackageIcon) enable + net8.0 diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchAppendPrimitiveTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchAppendPrimitiveTests.cs new file mode 100644 index 000000000000..481d510ea36b --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchAppendPrimitiveTests.cs @@ -0,0 +1,575 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal class JsonPatchAppendPrimitiveTests + { + [Test] + public void Append_Boolean() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, true); + jp.Append("$.array"u8, false); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(true, jp.GetBoolean("$.array[0]"u8)); + Assert.AreEqual(false, jp.GetBoolean("$.array[1]"u8)); + + Assert.AreEqual(true, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(false, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out bool boolValue0)); + Assert.AreEqual(true, boolValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out bool boolValue1)); + Assert.AreEqual(false, boolValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out bool? nullableBoolValue0)); + Assert.AreEqual(true, nullableBoolValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out bool? nullableBoolValue1)); + Assert.AreEqual(false, nullableBoolValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out bool? nullableBoolValue2)); + Assert.AreEqual(null, nullableBoolValue2); + + Assert.AreEqual("{\"array\":[true,false,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Byte() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, (byte)42); + jp.Append("$.array"u8, (byte)255); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(42, jp.GetByte("$.array[0]"u8)); + Assert.AreEqual(255, jp.GetByte("$.array[1]"u8)); + + Assert.AreEqual(42, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(255, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out byte byteValue0)); + Assert.AreEqual(42, byteValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out byte byteValue1)); + Assert.AreEqual(255, byteValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out byte? nullableByteValue0)); + Assert.AreEqual(42, nullableByteValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out byte? nullableByteValue1)); + Assert.AreEqual(255, nullableByteValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out byte? nullableByteValue2)); + Assert.AreEqual(null, nullableByteValue2); + + Assert.AreEqual("{\"array\":[42,255,null]}", jp.ToString("J")); + } + + [Test] + public void Append_DateTime() + { + JsonPatch jp = new(); + + DateTime dt1 = new(2024, 1, 1, 12, 0, 0); + DateTime dt2 = new(2024, 12, 31, 23, 59, 59); + + jp.Append("$.array"u8, dt1); + jp.Append("$.array"u8, dt2); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(dt1, jp.GetDateTime("$.array[0]"u8)); + Assert.AreEqual(dt2, jp.GetDateTime("$.array[1]"u8)); + + Assert.AreEqual(dt1, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(dt2, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out DateTime dateTimeValue0)); + Assert.AreEqual(dt1, dateTimeValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out DateTime dateTimeValue1)); + Assert.AreEqual(dt2, dateTimeValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out DateTime? nullableDateTimeValue0)); + Assert.AreEqual(dt1, nullableDateTimeValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out DateTime? nullableDateTimeValue1)); + Assert.AreEqual(dt2, nullableDateTimeValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out DateTime? nullableDateTimeValue2)); + Assert.AreEqual(null, nullableDateTimeValue2); + + Assert.AreEqual("{\"array\":[\"01/01/2024 12:00:00\",\"12/31/2024 23:59:59\",null]}", jp.ToString("J")); + } + + [Test] + public void Append_DateTimeOffset() + { + JsonPatch jp = new(); + + DateTimeOffset dto1 = new(2024, 1, 1, 12, 0, 0, TimeSpan.FromHours(1)); + DateTimeOffset dto2 = new(2024, 12, 31, 23, 59, 59, TimeSpan.FromHours(2)); + + jp.Append("$.array"u8, dto1); + jp.Append("$.array"u8, dto2); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(dto1, jp.GetDateTimeOffset("$.array[0]"u8)); + Assert.AreEqual(dto2, jp.GetDateTimeOffset("$.array[1]"u8)); + + Assert.AreEqual(dto1, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(dto2, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out DateTimeOffset dateTimeOffsetValue0)); + Assert.AreEqual(dto1, dateTimeOffsetValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out DateTimeOffset dateTimeOffsetValue1)); + Assert.AreEqual(dto2, dateTimeOffsetValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out DateTimeOffset? nullableDateTimeOffsetValue0)); + Assert.AreEqual(dto1, nullableDateTimeOffsetValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out DateTimeOffset? nullableDateTimeOffsetValue1)); + Assert.AreEqual(dto2, nullableDateTimeOffsetValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out DateTimeOffset? nullableDateTimeOffsetValue2)); + Assert.AreEqual(null, nullableDateTimeOffsetValue2); + + Assert.AreEqual("{\"array\":[\"01/01/2024 12:00:00 +01:00\",\"12/31/2024 23:59:59 +02:00\",null]}", jp.ToString("J")); + } + + [Test] + public void Append_Decimal() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 123.45m); + jp.Append("$.array"u8, 67890.12m); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(123.45m, jp.GetDecimal("$.array[0]"u8)); + Assert.AreEqual(67890.12m, jp.GetDecimal("$.array[1]"u8)); + + Assert.AreEqual(123.45m, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(67890.12m, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out decimal decimalValue0)); + Assert.AreEqual(123.45m, decimalValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out decimal decimalValue1)); + Assert.AreEqual(67890.12m, decimalValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out decimal? nullableDecimalValue0)); + Assert.AreEqual(123.45m, nullableDecimalValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out decimal? nullableDecimalValue1)); + Assert.AreEqual(67890.12m, nullableDecimalValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out decimal? nullableDecimalValue2)); + Assert.AreEqual(null, nullableDecimalValue2); + + Assert.AreEqual("{\"array\":[123.45,67890.12,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Double() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 123.45); + jp.Append("$.array"u8, 67890.12); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(123.45, jp.GetDouble("$.array[0]"u8)); + Assert.AreEqual(67890.12, jp.GetDouble("$.array[1]"u8)); + + Assert.AreEqual(123.45, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(67890.12, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out double doubleValue0)); + Assert.AreEqual(123.45, doubleValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out double doubleValue1)); + Assert.AreEqual(67890.12, doubleValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out double? nullableDoubleValue0)); + Assert.AreEqual(123.45, nullableDoubleValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out double? nullableDoubleValue1)); + Assert.AreEqual(67890.12, nullableDoubleValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out double? nullableDoubleValue2)); + Assert.AreEqual(null, nullableDoubleValue2); + + Assert.AreEqual("{\"array\":[123.45,67890.12,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Float() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 123.5f); + jp.Append("$.array"u8, 67890.5f); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(123.5f, jp.GetFloat("$.array[0]"u8)); + Assert.AreEqual(67890.5f, jp.GetFloat("$.array[1]"u8)); + + Assert.AreEqual(123.5f, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(67890.5f, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out float floatValue0)); + Assert.AreEqual(123.5f, floatValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out float floatValue1)); + Assert.AreEqual(67890.5f, floatValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out float? nullableFloatValue0)); + Assert.AreEqual(123.5f, nullableFloatValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out float? nullableFloatValue1)); + Assert.AreEqual(67890.5f, nullableFloatValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out float? nullableFloatValue2)); + Assert.AreEqual(null, nullableFloatValue2); + + Assert.AreEqual("{\"array\":[123.5,67890.5,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Guid() + { + JsonPatch jp = new(); + + Guid guid1 = Guid.NewGuid(); + Guid guid2 = Guid.NewGuid(); + + jp.Append("$.array"u8, guid1); + jp.Append("$.array"u8, guid2); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(guid1, jp.GetGuid("$.array[0]"u8)); + Assert.AreEqual(guid2, jp.GetGuid("$.array[1]"u8)); + + Assert.AreEqual(guid1, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(guid2, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out Guid guidValue0)); + Assert.AreEqual(guid1, guidValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out Guid guidValue1)); + Assert.AreEqual(guid2, guidValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out Guid? nullableGuidValue0)); + Assert.AreEqual(guid1, nullableGuidValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out Guid? nullableGuidValue1)); + Assert.AreEqual(guid2, nullableGuidValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out Guid? nullableGuidValue2)); + Assert.AreEqual(null, nullableGuidValue2); + + Assert.AreEqual($"{{\"array\":[\"{guid1}\",\"{guid2}\",null]}}", jp.ToString("J")); + } + + [Test] + public void Append_Int32() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 42); + jp.Append("$.array"u8, 255); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(42, jp.GetInt32("$.array[0]"u8)); + Assert.AreEqual(255, jp.GetInt32("$.array[1]"u8)); + + Assert.AreEqual(42, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(255, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out int intValue0)); + Assert.AreEqual(42, intValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out int intValue1)); + Assert.AreEqual(255, intValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out int? nullableIntValue0)); + Assert.AreEqual(42, nullableIntValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out int? nullableIntValue1)); + Assert.AreEqual(255, nullableIntValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out int? nullableIntValue2)); + Assert.AreEqual(null, nullableIntValue2); + + Assert.AreEqual("{\"array\":[42,255,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Int64() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 42000000000L); + jp.Append("$.array"u8, 25500000000L); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(42000000000L, jp.GetInt64("$.array[0]"u8)); + Assert.AreEqual(25500000000L, jp.GetInt64("$.array[1]"u8)); + + Assert.AreEqual(42000000000L, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(25500000000L, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out long longValue0)); + Assert.AreEqual(42000000000L, longValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out long longValue1)); + Assert.AreEqual(25500000000L, longValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out long? nullableLongValue0)); + Assert.AreEqual(42000000000L, nullableLongValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out long? nullableLongValue1)); + Assert.AreEqual(25500000000L, nullableLongValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out long? nullableLongValue2)); + Assert.AreEqual(null, nullableLongValue2); + + Assert.AreEqual("{\"array\":[42000000000,25500000000,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Int8() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, (sbyte)42); + jp.Append("$.array"u8, (sbyte)-100); + jp.AppendNull("$.array"u8); + + Assert.AreEqual((sbyte)42, jp.GetInt8("$.array[0]"u8)); + Assert.AreEqual((sbyte)-100, jp.GetInt8("$.array[1]"u8)); + + Assert.AreEqual((sbyte)42, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual((sbyte)-100, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out sbyte sbyteValue0)); + Assert.AreEqual((sbyte)42, sbyteValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out sbyte sbyteValue1)); + Assert.AreEqual((sbyte)-100, sbyteValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out sbyte? nullableSbyteValue0)); + Assert.AreEqual((sbyte)42, nullableSbyteValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out sbyte? nullableSbyteValue1)); + Assert.AreEqual((sbyte)-100, nullableSbyteValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out sbyte? nullableSbyteValue2)); + Assert.AreEqual(null, nullableSbyteValue2); + + Assert.AreEqual("{\"array\":[42,-100,null]}", jp.ToString("J")); + } + + [Test] + public void Append_Int16() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, (short)32000); + jp.Append("$.array"u8, (short)-16000); + jp.AppendNull("$.array"u8); + + Assert.AreEqual((short)32000, jp.GetInt16("$.array[0]"u8)); + Assert.AreEqual((short)-16000, jp.GetInt16("$.array[1]"u8)); + + Assert.AreEqual((short)32000, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual((short)-16000, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out short shortValue0)); + Assert.AreEqual((short)32000, shortValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out short shortValue1)); + Assert.AreEqual((short)-16000, shortValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out short? nullableShortValue0)); + Assert.AreEqual((short)32000, nullableShortValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out short? nullableShortValue1)); + Assert.AreEqual((short)-16000, nullableShortValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out short? nullableShortValue2)); + Assert.AreEqual(null, nullableShortValue2); + + Assert.AreEqual("{\"array\":[32000,-16000,null]}", jp.ToString("J")); + } + + [Test] + public void Append_TimeSpan() + { + JsonPatch jp = new(); + + TimeSpan ts1 = TimeSpan.FromHours(1.5); + TimeSpan ts2 = TimeSpan.FromDays(2); + + jp.Append("$.array"u8, ts1); + jp.Append("$.array"u8, ts2); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(ts1, jp.GetTimeSpan("$.array[0]"u8)); + Assert.AreEqual(ts2, jp.GetTimeSpan("$.array[1]"u8)); + + Assert.AreEqual(ts1, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(ts2, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out TimeSpan timeSpanValue0)); + Assert.AreEqual(ts1, timeSpanValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out TimeSpan timeSpanValue1)); + Assert.AreEqual(ts2, timeSpanValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out TimeSpan? nullableTimeSpanValue0)); + Assert.AreEqual(ts1, nullableTimeSpanValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out TimeSpan? nullableTimeSpanValue1)); + Assert.AreEqual(ts2, nullableTimeSpanValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out TimeSpan? nullableTimeSpanValue2)); + Assert.AreEqual(null, nullableTimeSpanValue2); + + Assert.AreEqual("{\"array\":[\"01:30:00\",\"2.00:00:00\",null]}", jp.ToString("J")); + } + + [Test] + public void Append_UInt32() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 42u); + jp.Append("$.array"u8, 255u); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(42u, jp.GetUInt32("$.array[0]"u8)); + Assert.AreEqual(255u, jp.GetUInt32("$.array[1]"u8)); + + Assert.AreEqual(42u, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(255u, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out uint uintValue0)); + Assert.AreEqual(42u, uintValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out uint uintValue1)); + Assert.AreEqual(255u, uintValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out uint? nullableUintValue0)); + Assert.AreEqual(42u, nullableUintValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out uint? nullableUintValue1)); + Assert.AreEqual(255u, nullableUintValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out uint? nullableUintValue2)); + Assert.AreEqual(null, nullableUintValue2); + + Assert.AreEqual("{\"array\":[42,255,null]}", jp.ToString("J")); + } + + [Test] + public void Append_UInt64() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, 42000000000UL); + jp.Append("$.array"u8, 25500000000UL); + jp.AppendNull("$.array"u8); + + Assert.AreEqual(42000000000UL, jp.GetUInt64("$.array[0]"u8)); + Assert.AreEqual(25500000000UL, jp.GetUInt64("$.array[1]"u8)); + + Assert.AreEqual(42000000000UL, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual(25500000000UL, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out ulong ulongValue0)); + Assert.AreEqual(42000000000UL, ulongValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out ulong ulongValue1)); + Assert.AreEqual(25500000000UL, ulongValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out ulong? nullableUlongValue0)); + Assert.AreEqual(42000000000UL, nullableUlongValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out ulong? nullableUlongValue1)); + Assert.AreEqual(25500000000UL, nullableUlongValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out ulong? nullableUlongValue2)); + Assert.AreEqual(null, nullableUlongValue2); + + Assert.AreEqual("{\"array\":[42000000000,25500000000,null]}", jp.ToString("J")); + } + + [Test] + public void Append_UInt16() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, (ushort)32000); + jp.Append("$.array"u8, (ushort)16000); + jp.AppendNull("$.array"u8); + + Assert.AreEqual((ushort)32000, jp.GetUInt16("$.array[0]"u8)); + Assert.AreEqual((ushort)16000, jp.GetUInt16("$.array[1]"u8)); + + Assert.AreEqual((ushort)32000, jp.GetNullableValue("$.array[0]"u8)); + Assert.AreEqual((ushort)16000, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out ushort ushortValue0)); + Assert.AreEqual((ushort)32000, ushortValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out ushort ushortValue1)); + Assert.AreEqual((ushort)16000, ushortValue1); + + Assert.IsTrue(jp.TryGetNullableValue("$.array[0]"u8, out ushort? nullableUshortValue0)); + Assert.AreEqual((ushort)32000, nullableUshortValue0); + Assert.IsTrue(jp.TryGetNullableValue("$.array[1]"u8, out ushort? nullableUshortValue1)); + Assert.AreEqual((ushort)16000, nullableUshortValue1); + Assert.IsTrue(jp.TryGetNullableValue("$.array[2]"u8, out ushort? nullableUshortValue2)); + Assert.AreEqual(null, nullableUshortValue2); + + Assert.AreEqual("{\"array\":[32000,16000,null]}", jp.ToString("J")); + } + + [Test] + public void Append_String() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, "Hello"); + jp.Append("$.array"u8, "World"); + jp.AppendNull("$.array"u8); + + Assert.AreEqual("Hello", jp.GetString("$.array[0]"u8)); + Assert.AreEqual("World", jp.GetString("$.array[1]"u8)); + Assert.AreEqual(null, jp.GetString("$.array[2]"u8)); + + Assert.IsTrue(jp.TryGetValue("$.array[0]"u8, out string? stringValue0)); + Assert.AreEqual("Hello", stringValue0); + Assert.IsTrue(jp.TryGetValue("$.array[1]"u8, out string? stringValue1)); + Assert.AreEqual("World", stringValue1); + Assert.IsTrue(jp.TryGetValue("$.array[2]"u8, out string? stringValue2)); + Assert.AreEqual(null, stringValue2); + + Assert.AreEqual("{\"array\":[\"Hello\",\"World\",null]}", jp.ToString("J")); + } + + [Test] + public void Append_Mixed() + { + JsonPatch jp = new(); + + jp.Append("$.array"u8, "Hello"); + jp.Append("$.array"u8, 42); + jp.Append("$.array"u8, true); + jp.AppendNull("$.array"u8); + + Assert.AreEqual("Hello", jp.GetString("$.array[0]"u8)); + Assert.AreEqual(42, jp.GetInt32("$.array[1]"u8)); + Assert.AreEqual(42, jp.GetNullableValue("$.array[1]"u8)); + Assert.AreEqual(true, jp.GetBoolean("$.array[2]"u8)); + Assert.AreEqual(true, jp.GetNullableValue("$.array[2]"u8)); + Assert.AreEqual(null, jp.GetString("$.array[3]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[3]"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.array[3]"u8)); + + var ex = Assert.Throws(() => jp.GetInt32("$.array[0]"u8)); + Assert.AreEqual("Value at '$.array[0]' is not a System.Int32.", ex!.Message); + ex = Assert.Throws(() => jp.GetBoolean("$.array[1]"u8)); + Assert.AreEqual("Value at '$.array[1]' is not a System.Boolean.", ex!.Message); + + // string always works + Assert.AreEqual("Hello", jp.GetString("$.array[0]"u8)); + Assert.AreEqual("42", jp.GetString("$.array[1]"u8)); + Assert.AreEqual("true", jp.GetString("$.array[2]"u8)); + Assert.AreEqual(null, jp.GetString("$.array[3]"u8)); + + Assert.AreEqual("{\"array\":[\"Hello\",42,true,null]}", jp.ToString("J")); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchAppendTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchAppendTests.cs new file mode 100644 index 000000000000..bf28ff00a227 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchAppendTests.cs @@ -0,0 +1,2146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + public class JsonPatchAppendTests + { + [Test] + public void AppendWithMoreThanTenAndARemove() + { + JsonPatch jp = new("{\"arr\":[1,2,3,4,5,6,7,8,9,10,11],\"x\":1}"u8.ToArray()); + + jp.Remove("$.x"u8); + jp.Append("$.arr"u8, 12); + jp.Append("$.arr"u8, 13); + + Assert.AreEqual("[1,2,3,4,5,6,7,8,9,10,11,12,13]", jp.GetJson("$.arr"u8).ToString()); + Assert.AreEqual(1, jp.GetInt32("$.arr[0]"u8)); + Assert.AreEqual(2, jp.GetInt32("$.arr[1]"u8)); + Assert.AreEqual(3, jp.GetInt32("$.arr[2]"u8)); + Assert.AreEqual(4, jp.GetInt32("$.arr[3]"u8)); + Assert.AreEqual(5, jp.GetInt32("$.arr[4]"u8)); + Assert.AreEqual(6, jp.GetInt32("$.arr[5]"u8)); + Assert.AreEqual(7, jp.GetInt32("$.arr[6]"u8)); + Assert.AreEqual(8, jp.GetInt32("$.arr[7]"u8)); + Assert.AreEqual(9, jp.GetInt32("$.arr[8]"u8)); + Assert.AreEqual(10, jp.GetInt32("$.arr[9]"u8)); + Assert.AreEqual(11, jp.GetInt32("$.arr[10]"u8)); + Assert.AreEqual(12, jp.GetInt32("$.arr[11]"u8)); + Assert.AreEqual(13, jp.GetInt32("$.arr[12]"u8)); + + Assert.AreEqual("{\"arr\":[1,2,3,4,5,6,7,8,9,10,11,12,13]}", jp.ToString("J")); + } + + [Test] + public void AppendWithMoreThanTen() + { + JsonPatch jp = new("{\"arr\":[1,2,3,4,5,6,7,8,9,10,11]}"u8.ToArray()); + + jp.Append("$.arr"u8, 12); + jp.Append("$.arr"u8, 13); + + Assert.AreEqual("[1,2,3,4,5,6,7,8,9,10,11,12,13]", jp.GetJson("$.arr"u8).ToString()); + Assert.AreEqual(1, jp.GetInt32("$.arr[0]"u8)); + Assert.AreEqual(2, jp.GetInt32("$.arr[1]"u8)); + Assert.AreEqual(3, jp.GetInt32("$.arr[2]"u8)); + Assert.AreEqual(4, jp.GetInt32("$.arr[3]"u8)); + Assert.AreEqual(5, jp.GetInt32("$.arr[4]"u8)); + Assert.AreEqual(6, jp.GetInt32("$.arr[5]"u8)); + Assert.AreEqual(7, jp.GetInt32("$.arr[6]"u8)); + Assert.AreEqual(8, jp.GetInt32("$.arr[7]"u8)); + Assert.AreEqual(9, jp.GetInt32("$.arr[8]"u8)); + Assert.AreEqual(10, jp.GetInt32("$.arr[9]"u8)); + Assert.AreEqual(11, jp.GetInt32("$.arr[10]"u8)); + Assert.AreEqual(12, jp.GetInt32("$.arr[11]"u8)); + Assert.AreEqual(13, jp.GetInt32("$.arr[12]"u8)); + + Assert.AreEqual("{\"arr\":[1,2,3,4,5,6,7,8,9,10,11,12,13]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_ParentBeforeOutOfOrder() + { + JsonPatch jp = new("{\"a\":{\"b\":[[\"value1\",\"value3\"],[\"value2\"]]}}"u8.ToArray()); + + Assert.AreEqual("{\"b\":[[\"value1\",\"value3\"],[\"value2\"]]}", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value3\"],[\"value2\"]]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value3\"]", jp.GetJson("$.a.b[0]"u8).ToArray()); + Assert.AreEqual("value1", jp.GetString("$.a.b[0][0]"u8)); + Assert.AreEqual("value3", jp.GetString("$.a.b[0][1]"u8)); + Assert.AreEqual("[\"value2\"]", jp.GetJson("$.a.b[1]"u8).ToArray()); + Assert.AreEqual("value2", jp.GetString("$.a.b[1][0]"u8)); + + jp.Append("$.a.b"u8, "[\"value4\"]"u8); + + Assert.AreEqual("[[\"value1\",\"value3\"],[\"value2\"],[\"value4\"]]", jp.GetJson("$.a.b"u8).ToString()); + + jp.Append("$.a.b[1]"u8, "value5"); + + Assert.AreEqual("[[\"value1\",\"value3\"],[\"value2\",\"value5\"],[\"value4\"]]", jp.GetJson("$.a.b"u8).ToString()); + + jp.Append("$.a.b[0]"u8, "value6"); + + Assert.AreEqual("[[\"value1\",\"value3\",\"value6\"],[\"value2\",\"value5\"],[\"value4\"]]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value3\",\"value6\"]", jp.GetJson("$.a.b[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.a.b[0][0]"u8)); + Assert.AreEqual("value3", jp.GetString("$.a.b[0][1]"u8)); + Assert.AreEqual("value6", jp.GetString("$.a.b[0][2]"u8)); + Assert.AreEqual("[\"value2\",\"value5\"]", jp.GetJson("$.a.b[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.a.b[1][0]"u8)); + Assert.AreEqual("value5", jp.GetString("$.a.b[1][1]"u8)); + Assert.AreEqual("[\"value4\"]", jp.GetJson("$.a.b[2]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.a.b[2][0]"u8)); + + Assert.AreEqual("{\"a\":{\"b\":[[\"value1\",\"value3\",\"value6\"],[\"value2\",\"value5\"],[\"value4\"]]}}", jp.ToString("J")); + } + + [Test] + public void Insert_ParentBeforeOutOfOrder() + { + JsonPatch jp = new(); + + jp.Append("$.a.b"u8, "[\"value1\"]"u8); + + Assert.AreEqual("{\"b\":[[\"value1\"]]}", jp.GetJson("$.a"u8).ToString()); + + jp.Append("$.a.b[1]"u8, "value2"); + + Assert.AreEqual("{\"b\":[[\"value1\"],[\"value2\"]]}", jp.GetJson("$.a"u8).ToString()); + + jp.Append("$.a.b[0]"u8, "value3"); + + Assert.AreEqual("{\"b\":[[\"value1\",\"value3\"],[\"value2\"]]}", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value3\"],[\"value2\"]]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value3\"]", jp.GetJson("$.a.b[0]"u8).ToArray()); + Assert.AreEqual("value1", jp.GetString("$.a.b[0][0]"u8)); + Assert.AreEqual("value3", jp.GetString("$.a.b[0][1]"u8)); + Assert.AreEqual("[\"value2\"]", jp.GetJson("$.a.b[1]"u8).ToArray()); + Assert.AreEqual("value2", jp.GetString("$.a.b[1][0]"u8)); + + Assert.AreEqual("{\"a\":{\"b\":[[\"value1\",\"value3\"],[\"value2\"]]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OutOfOrder() + { + JsonPatch jp = new("{\"a\":{\"b\":[[\"value1\"],[\"value2\"]]}}"u8.ToArray()); + + Assert.AreEqual("{\"b\":[[\"value1\"],[\"value2\"]]}", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual("[[\"value1\"],[\"value2\"]]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("[\"value1\"]", jp.GetJson("$.a.b[0]"u8).ToArray()); + Assert.AreEqual("value1", jp.GetString("$.a.b[0][0]"u8)); + Assert.AreEqual("[\"value2\"]", jp.GetJson("$.a.b[1]"u8).ToArray()); + Assert.AreEqual("value2", jp.GetString("$.a.b[1][0]"u8)); + + jp.Append("$.a.b[0]"u8, "value1b"); + + Assert.AreEqual("[\"value1\",\"value1b\"]", jp.GetJson("$.a.b[0]"u8).ToString()); + + jp.Append("$.a.b[1]"u8, "value2b"); + + Assert.AreEqual("[\"value2\",\"value2b\"]", jp.GetJson("$.a.b[1]"u8).ToString()); + + jp.Append("$.a.b[3]"u8, "value4"); + + Assert.AreEqual("[\"value4\"]", jp.GetJson("$.a.b[3]"u8).ToString()); + + jp.Append("$.a.b[2]"u8, "value3"); + + Assert.AreEqual("[\"value3\"]", jp.GetJson("$.a.b[2]"u8).ToString()); + + jp.Append("$.a.b"u8, "[\"value5\"]"u8); + + Assert.AreEqual("[[\"value1\",\"value1b\"],[\"value2\",\"value2b\"],[\"value3\"],[\"value4\"],[\"value5\"]]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value1b\"]", jp.GetJson("$.a.b[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.a.b[0][0]"u8)); + Assert.AreEqual("value1b", jp.GetString("$.a.b[0][1]"u8)); + Assert.AreEqual("[\"value2\",\"value2b\"]", jp.GetJson("$.a.b[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.a.b[1][0]"u8)); + Assert.AreEqual("value2b", jp.GetString("$.a.b[1][1]"u8)); + Assert.AreEqual("[\"value3\"]", jp.GetJson("$.a.b[2]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.a.b[2][0]"u8)); + Assert.AreEqual("[\"value4\"]", jp.GetJson("$.a.b[3]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.a.b[3][0]"u8)); + Assert.AreEqual("[\"value5\"]", jp.GetJson("$.a.b[4]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.a.b[4][0]"u8)); + + Assert.AreEqual("{\"a\":{\"b\":[[\"value1\",\"value1b\"],[\"value2\",\"value2b\"],[\"value3\"],[\"value4\"],[\"value5\"]]}}", jp.ToString("J")); + } + + [Test] + public void Insert_OutOfOrder() + { + JsonPatch jp = new(); + + jp.Append("$.a.b[1]"u8, "value2"); + + Assert.AreEqual("{\"b\":[null,[\"value2\"]]}", jp.GetJson("$.a"u8).ToString()); + + jp.Append("$.a.b[0]"u8, "value1"); + + Assert.AreEqual("{\"b\":[[\"value1\"],[\"value2\"]]}", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual("[[\"value1\"],[\"value2\"]]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("[\"value1\"]", jp.GetJson("$.a.b[0]"u8).ToArray()); + Assert.AreEqual("value1", jp.GetString("$.a.b[0][0]"u8)); + Assert.AreEqual("[\"value2\"]", jp.GetJson("$.a.b[1]"u8).ToArray()); + Assert.AreEqual("value2", jp.GetString("$.a.b[1][0]"u8)); + + Assert.AreEqual("{\"a\":{\"b\":[[\"value1\"],[\"value2\"]]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_ThreeDimensionTwoDimension_WithProperty() + { + JsonPatch jp = new("{\"x\":{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}]", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}}", jp.GetJson("$.x.y[0][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}", jp.GetJson("$.x.y[0][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]", jp.GetJson("$.x.y[0][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value1\"},{\"b\":\"value2\"}]", jp.GetJson("$.x.y[0][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value1\"}", jp.GetJson("$.x.y[0][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0][0].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value2\"}", jp.GetJson("$.x.y[0][0][0].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][0][0].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"b\":\"value3\"},{\"b\":\"value4\"}]", jp.GetJson("$.x.y[0][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value3\"}", jp.GetJson("$.x.y[0][0][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[0][0][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value4\"}", jp.GetJson("$.x.y[0][0][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[0][0][0].z.a[1][1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}", jp.GetJson("$.x.y[0][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}", jp.GetJson("$.x.y[0][0][1].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]", jp.GetJson("$.x.y[0][0][1].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value5\"},{\"b\":\"value6\"}]", jp.GetJson("$.x.y[0][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value5\"}", jp.GetJson("$.x.y[0][0][1].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][0][1].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value6\"}", jp.GetJson("$.x.y[0][0][1].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][0][1].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}", jp.GetJson("$.x.y[0][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}", jp.GetJson("$.x.y[0][1][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]", jp.GetJson("$.x.y[0][1][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value7\"},{\"b\":\"value8\"}]", jp.GetJson("$.x.y[0][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value7\"}", jp.GetJson("$.x.y[0][1][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[0][1][0].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value8\"}", jp.GetJson("$.x.y[0][1][0].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x.y[0][1][0].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"b\":\"value9\"},{\"b\":\"value10\"}]", jp.GetJson("$.x.y[0][1][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value9\"}", jp.GetJson("$.x.y[0][1][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[0][1][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value10\"}", jp.GetJson("$.x.y[0][1][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value10", jp.GetString("$.x.y[0][1][0].z.a[1][1].b"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}", jp.GetJson("$.x.y[1][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value11\"}]]}", jp.GetJson("$.x.y[1][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value11\"}]]", jp.GetJson("$.x.y[1][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value11\"}]", jp.GetJson("$.x.y[1][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value11\"}", jp.GetJson("$.x.y[1][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value11", jp.GetString("$.x.y[1][0][0].z.a[0][0].b"u8)); + + jp.Append("$.x.y"u8, "[[{\"z\":{\"a\":[[{\"b\":\"value12\"}]]}}]]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[1][0]"u8, "{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[1][0][0].z.a"u8, "[{\"b\":\"value15\"}]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[2][0][0].z.a"u8, "[{\"b\":\"value16\"}]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[2][0][0].z.a[1]"u8, "{\"b\":\"value17\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}]", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}}", jp.GetJson("$.x.y[0][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}", jp.GetJson("$.x.y[0][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]", jp.GetJson("$.x.y[0][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value1\"},{\"b\":\"value2\"}]", jp.GetJson("$.x.y[0][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value1\"}", jp.GetJson("$.x.y[0][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0][0].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value2\"}", jp.GetJson("$.x.y[0][0][0].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][0][0].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"b\":\"value3\"},{\"b\":\"value4\"}]", jp.GetJson("$.x.y[0][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value3\"}", jp.GetJson("$.x.y[0][0][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[0][0][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value4\"}", jp.GetJson("$.x.y[0][0][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[0][0][0].z.a[1][1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}", jp.GetJson("$.x.y[0][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}", jp.GetJson("$.x.y[0][0][1].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]", jp.GetJson("$.x.y[0][0][1].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value5\"},{\"b\":\"value6\"}]", jp.GetJson("$.x.y[0][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value5\"}", jp.GetJson("$.x.y[0][0][1].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][0][1].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value6\"}", jp.GetJson("$.x.y[0][0][1].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][0][1].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}", jp.GetJson("$.x.y[0][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}", jp.GetJson("$.x.y[0][1][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]", jp.GetJson("$.x.y[0][1][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value7\"},{\"b\":\"value8\"}]", jp.GetJson("$.x.y[0][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value7\"}", jp.GetJson("$.x.y[0][1][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[0][1][0].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value8\"}", jp.GetJson("$.x.y[0][1][0].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x.y[0][1][0].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"b\":\"value9\"},{\"b\":\"value10\"}]", jp.GetJson("$.x.y[0][1][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value9\"}", jp.GetJson("$.x.y[0][1][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[0][1][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value10\"}", jp.GetJson("$.x.y[0][1][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value10", jp.GetString("$.x.y[0][1][0].z.a[1][1].b"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}]", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}}", jp.GetJson("$.x.y[1][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}", jp.GetJson("$.x.y[1][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]", jp.GetJson("$.x.y[1][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value11\"}]", jp.GetJson("$.x.y[1][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value11\"}", jp.GetJson("$.x.y[1][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value11", jp.GetString("$.x.y[1][0][0].z.a[0][0].b"u8)); + Assert.AreEqual("[{\"b\":\"value15\"}]", jp.GetJson("$.x.y[1][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value15\"}", jp.GetJson("$.x.y[1][0][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value15", jp.GetString("$.x.y[1][0][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}", jp.GetJson("$.x.y[1][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value14\"}]]}", jp.GetJson("$.x.y[1][0][1].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value14\"}]]", jp.GetJson("$.x.y[1][0][1].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value14\"}]", jp.GetJson("$.x.y[1][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value14\"}", jp.GetJson("$.x.y[1][0][1].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value14", jp.GetString("$.x.y[1][0][1].z.a[0][0].b"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]", jp.GetJson("$.x.y[1][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}", jp.GetJson("$.x.y[1][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value13\"}]]}", jp.GetJson("$.x.y[1][1][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value13\"}]]", jp.GetJson("$.x.y[1][1][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value13\"}]", jp.GetJson("$.x.y[1][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value13\"}", jp.GetJson("$.x.y[1][1][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value13", jp.GetString("$.x.y[1][1][0].z.a[0][0].b"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]}}]]", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]}}]", jp.GetJson("$.x.y[2][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]}}", jp.GetJson("$.x.y[2][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]}", jp.GetJson("$.x.y[2][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]", jp.GetJson("$.x.y[2][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value12\"}]", jp.GetJson("$.x.y[2][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value12\"}", jp.GetJson("$.x.y[2][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value12", jp.GetString("$.x.y[2][0][0].z.a[0][0].b"u8)); + Assert.AreEqual("[{\"b\":\"value16\"},{\"b\":\"value17\"}]", jp.GetJson("$.x.y[2][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value16\"}", jp.GetJson("$.x.y[2][0][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value16", jp.GetString("$.x.y[2][0][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value17\"}", jp.GetJson("$.x.y[2][0][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value17", jp.GetString("$.x.y[2][0][0].z.a[1][1].b"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}],[{\"b\":\"value15\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value14\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value13\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value12\"}],[{\"b\":\"value16\"},{\"b\":\"value17\"}]]}}]]]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_ThreeDimensionTwoDimension() + { + JsonPatch jp = new("{\"x\":{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}]", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}}", jp.GetJson("$.x.y[0][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}", jp.GetJson("$.x.y[0][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]", jp.GetJson("$.x.y[0][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0][0].z.a[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][0][0].z.a[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[0][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[0][0][0].z.a[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[0][0][0].z.a[1][1]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}", jp.GetJson("$.x.y[0][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value5\",\"value6\"]]}", jp.GetJson("$.x.y[0][0][1].z"u8).ToString()); + Assert.AreEqual("[[\"value5\",\"value6\"]]", jp.GetJson("$.x.y[0][0][1].z.a"u8).ToString()); + Assert.AreEqual("[\"value5\",\"value6\"]", jp.GetJson("$.x.y[0][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][0][1].z.a[0][0]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][0][1].z.a[0][1]"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}", jp.GetJson("$.x.y[0][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}", jp.GetJson("$.x.y[0][1][0].z"u8).ToString()); + Assert.AreEqual("[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]", jp.GetJson("$.x.y[0][1][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value7\",\"value8\"]", jp.GetJson("$.x.y[0][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[0][1][0].z.a[0][0]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x.y[0][1][0].z.a[0][1]"u8)); + Assert.AreEqual("[\"value9\",\"value10\"]", jp.GetJson("$.x.y[0][1][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[0][1][0].z.a[1][0]"u8)); + Assert.AreEqual("value10", jp.GetString("$.x.y[0][1][0].z.a[1][1]"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value11\"]]}}]]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value11\"]]}}]", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value11\"]]}}", jp.GetJson("$.x.y[1][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value11\"]]}", jp.GetJson("$.x.y[1][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value11\"]]", jp.GetJson("$.x.y[1][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value11\"]", jp.GetJson("$.x.y[1][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value11", jp.GetString("$.x.y[1][0][0].z.a[0][0]"u8)); + + jp.Append("$.x.y"u8, "[[{\"z\":{\"a\":[[\"value12\"]]}}]]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]],[[{\"z\":{\"a\":[[\"value12\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "[{\"z\":{\"a\":[[\"value13\"]]}}]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}],[{\"z\":{\"a\":[[\"value13\"]]}}]],[[{\"z\":{\"a\":[[\"value12\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[1][0]"u8, "{\"z\":{\"a\":[[\"value14\"]]}}"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}},{\"z\":{\"a\":[[\"value14\"]]}}],[{\"z\":{\"a\":[[\"value13\"]]}}]],[[{\"z\":{\"a\":[[\"value12\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[1][0][0].z.a"u8, "[\"value15\"]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"],[\"value15\"]]}},{\"z\":{\"a\":[[\"value14\"]]}}],[{\"z\":{\"a\":[[\"value13\"]]}}]],[[{\"z\":{\"a\":[[\"value12\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[2][0][0].z.a"u8, "[\"value16\"]"u8); + + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"],[\"value15\"]]}},{\"z\":{\"a\":[[\"value14\"]]}}],[{\"z\":{\"a\":[[\"value13\"]]}}]],[[{\"z\":{\"a\":[[\"value12\"],[\"value16\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y[2][0][0].z.a[1]"u8, "value17"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"],[\"value15\"]]}},{\"z\":{\"a\":[[\"value14\"]]}}],[{\"z\":{\"a\":[[\"value13\"]]}}]],[[{\"z\":{\"a\":[[\"value12\"],[\"value16\",\"value17\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}]", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}}", jp.GetJson("$.x.y[0][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}", jp.GetJson("$.x.y[0][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]", jp.GetJson("$.x.y[0][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0][0].z.a[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][0][0].z.a[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[0][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[0][0][0].z.a[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[0][0][0].z.a[1][1]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}", jp.GetJson("$.x.y[0][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value5\",\"value6\"]]}", jp.GetJson("$.x.y[0][0][1].z"u8).ToString()); + Assert.AreEqual("[[\"value5\",\"value6\"]]", jp.GetJson("$.x.y[0][0][1].z.a"u8).ToString()); + Assert.AreEqual("[\"value5\",\"value6\"]", jp.GetJson("$.x.y[0][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][0][1].z.a[0][0]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][0][1].z.a[0][1]"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}", jp.GetJson("$.x.y[0][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}", jp.GetJson("$.x.y[0][1][0].z"u8).ToString()); + Assert.AreEqual("[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]", jp.GetJson("$.x.y[0][1][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value7\",\"value8\"]", jp.GetJson("$.x.y[0][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[0][1][0].z.a[0][0]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x.y[0][1][0].z.a[0][1]"u8)); + Assert.AreEqual("[\"value9\",\"value10\"]", jp.GetJson("$.x.y[0][1][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[0][1][0].z.a[1][0]"u8)); + Assert.AreEqual("value10", jp.GetString("$.x.y[0][1][0].z.a[1][1]"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value11\"],[\"value15\"]]}},{\"z\":{\"a\":[[\"value14\"]]}}],[{\"z\":{\"a\":[[\"value13\"]]}}]]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value11\"],[\"value15\"]]}},{\"z\":{\"a\":[[\"value14\"]]}}]", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value11\"],[\"value15\"]]}}", jp.GetJson("$.x.y[1][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value11\"],[\"value15\"]]}", jp.GetJson("$.x.y[1][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value11\"],[\"value15\"]]", jp.GetJson("$.x.y[1][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value11\"]", jp.GetJson("$.x.y[1][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value11", jp.GetString("$.x.y[1][0][0].z.a[0][0]"u8)); + Assert.AreEqual("[\"value15\"]", jp.GetJson("$.x.y[1][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value15", jp.GetString("$.x.y[1][0][0].z.a[1][0]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[\"value14\"]]}}", jp.GetJson("$.x.y[1][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value14\"]]}", jp.GetJson("$.x.y[1][0][1].z"u8).ToString()); + Assert.AreEqual("[[\"value14\"]]", jp.GetJson("$.x.y[1][0][1].z.a"u8).ToString()); + Assert.AreEqual("[\"value14\"]", jp.GetJson("$.x.y[1][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("value14", jp.GetString("$.x.y[1][0][1].z.a[0][0]"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value13\"]]}}]", jp.GetJson("$.x.y[1][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value13\"]]}}", jp.GetJson("$.x.y[1][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value13\"]]}", jp.GetJson("$.x.y[1][1][0].z"u8).ToString()); + Assert.AreEqual("[[\"value13\"]]", jp.GetJson("$.x.y[1][1][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value13\"]", jp.GetJson("$.x.y[1][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value13", jp.GetString("$.x.y[1][1][0].z.a[0][0]"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value12\"],[\"value16\",\"value17\"]]}}]]", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value12\"],[\"value16\",\"value17\"]]}}]", jp.GetJson("$.x.y[2][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value12\"],[\"value16\",\"value17\"]]}}", jp.GetJson("$.x.y[2][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value12\"],[\"value16\",\"value17\"]]}", jp.GetJson("$.x.y[2][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value12\"],[\"value16\",\"value17\"]]", jp.GetJson("$.x.y[2][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value12\"]", jp.GetJson("$.x.y[2][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value12", jp.GetString("$.x.y[2][0][0].z.a[0][0]"u8)); + Assert.AreEqual("[\"value16\",\"value17\"]", jp.GetJson("$.x.y[2][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value16", jp.GetString("$.x.y[2][0][0].z.a[1][0]"u8)); + Assert.AreEqual("value17", jp.GetString("$.x.y[2][0][0].z.a[1][1]"u8)); + } + + [Test] + public void Insert_ThreeDimensionTwoDimension_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x.y[0][0][0].z.a[0]"u8, "{\"b\":\"value1\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][0].z.a[0]"u8, "{\"b\":\"value2\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][0].z.a[1]"u8, "{\"b\":\"value3\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][0].z.a[1]"u8, "{\"b\":\"value4\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0]"u8, "{\"z\":{\"a\":[[{\"b\":\"value5\"}]]}}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][1].z.a[0]"u8, "{\"b\":\"value6\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1]"u8, "{\"z\":{\"a\":[[{\"b\":\"value7\"}]]}}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1][0].z.a[0]"u8, "{\"b\":\"value8\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1][0].z.a[1]"u8, "{\"b\":\"value9\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1][0].z.a[1]"u8, "{\"b\":\"value10\"}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}]", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}}", jp.GetJson("$.x.y[0][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}", jp.GetJson("$.x.y[0][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]", jp.GetJson("$.x.y[0][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value1\"},{\"b\":\"value2\"}]", jp.GetJson("$.x.y[0][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value1\"}", jp.GetJson("$.x.y[0][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0][0].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value2\"}", jp.GetJson("$.x.y[0][0][0].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][0][0].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"b\":\"value3\"},{\"b\":\"value4\"}]", jp.GetJson("$.x.y[0][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value3\"}", jp.GetJson("$.x.y[0][0][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[0][0][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value4\"}", jp.GetJson("$.x.y[0][0][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[0][0][0].z.a[1][1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}", jp.GetJson("$.x.y[0][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}", jp.GetJson("$.x.y[0][0][1].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]", jp.GetJson("$.x.y[0][0][1].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value5\"},{\"b\":\"value6\"}]", jp.GetJson("$.x.y[0][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value5\"}", jp.GetJson("$.x.y[0][0][1].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][0][1].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value6\"}", jp.GetJson("$.x.y[0][0][1].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][0][1].z.a[0][1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}", jp.GetJson("$.x.y[0][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}", jp.GetJson("$.x.y[0][1][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]", jp.GetJson("$.x.y[0][1][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value7\"},{\"b\":\"value8\"}]", jp.GetJson("$.x.y[0][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value7\"}", jp.GetJson("$.x.y[0][1][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[0][1][0].z.a[0][0].b"u8)); + Assert.AreEqual("{\"b\":\"value8\"}", jp.GetJson("$.x.y[0][1][0].z.a[0][1]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x.y[0][1][0].z.a[0][1].b"u8)); + Assert.AreEqual("[{\"b\":\"value9\"},{\"b\":\"value10\"}]", jp.GetJson("$.x.y[0][1][0].z.a[1]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value9\"}", jp.GetJson("$.x.y[0][1][0].z.a[1][0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[0][1][0].z.a[1][0].b"u8)); + Assert.AreEqual("{\"b\":\"value10\"}", jp.GetJson("$.x.y[0][1][0].z.a[1][1]"u8).ToString()); + Assert.AreEqual("value10", jp.GetString("$.x.y[0][1][0].z.a[1][1].b"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}", jp.GetJson("$.x.y[1][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[{\"b\":\"value11\"}]]}", jp.GetJson("$.x.y[1][0][0].z"u8).ToString()); + Assert.AreEqual("[[{\"b\":\"value11\"}]]", jp.GetJson("$.x.y[1][0][0].z.a"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value11\"}]", jp.GetJson("$.x.y[1][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("{\"b\":\"value11\"}", jp.GetJson("$.x.y[1][0][0].z.a[0][0]"u8).ToString()); + Assert.AreEqual("value11", jp.GetString("$.x.y[1][0][0].z.a[0][0].b"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[[{\"z\":{\"a\":[[{\"b\":\"value1\"},{\"b\":\"value2\"}],[{\"b\":\"value3\"},{\"b\":\"value4\"}]]}},{\"z\":{\"a\":[[{\"b\":\"value5\"},{\"b\":\"value6\"}]]}}],[{\"z\":{\"a\":[[{\"b\":\"value7\"},{\"b\":\"value8\"}],[{\"b\":\"value9\"},{\"b\":\"value10\"}]]}}]],[[{\"z\":{\"a\":[[{\"b\":\"value11\"}]]}}]]]}}", jp.ToString("J")); + } + + [Test] + public void Insert_ThreeDimensionTwoDimension() + { + JsonPatch jp = new(); + + jp.Append("$.x.y[0][0][0].z.a[0]"u8, "value1"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][0].z.a[0]"u8, "value2"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][0].z.a[1]"u8, "value3"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][0].z.a[1]"u8, "value4"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0]"u8, "{\"z\":{\"a\":[[\"value5\"]]}}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][0][1].z.a[0]"u8, "value6"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1]"u8, "{\"z\":{\"a\":[[\"value7\"]]}}"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1][0].z.a[0]"u8, "value8"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1][0].z.a[1]"u8, "value9"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0][1][0].z.a[1]"u8, "value10"); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "[{\"z\":{\"a\":[[\"value11\"]]}}]"u8); + + Assert.AreEqual("{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}]", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}}", jp.GetJson("$.x.y[0][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}", jp.GetJson("$.x.y[0][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]", jp.GetJson("$.x.y[0][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0][0].z.a[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][0][0].z.a[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[0][0][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[0][0][0].z.a[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[0][0][0].z.a[1][1]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}", jp.GetJson("$.x.y[0][0][1]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value5\",\"value6\"]]}", jp.GetJson("$.x.y[0][0][1].z"u8).ToString()); + Assert.AreEqual("[[\"value5\",\"value6\"]]", jp.GetJson("$.x.y[0][0][1].z.a"u8).ToString()); + Assert.AreEqual("[\"value5\",\"value6\"]", jp.GetJson("$.x.y[0][0][1].z.a[0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][0][1].z.a[0][0]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][0][1].z.a[0][1]"u8)); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}", jp.GetJson("$.x.y[0][1][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}", jp.GetJson("$.x.y[0][1][0].z"u8).ToString()); + Assert.AreEqual("[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]", jp.GetJson("$.x.y[0][1][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value7\",\"value8\"]", jp.GetJson("$.x.y[0][1][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[0][1][0].z.a[0][0]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x.y[0][1][0].z.a[0][1]"u8)); + Assert.AreEqual("[\"value9\",\"value10\"]", jp.GetJson("$.x.y[0][1][0].z.a[1]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[0][1][0].z.a[1][0]"u8)); + Assert.AreEqual("value10", jp.GetString("$.x.y[0][1][0].z.a[1][1]"u8)); + Assert.AreEqual("[[{\"z\":{\"a\":[[\"value11\"]]}}]]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[[\"value11\"]]}}]", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[[\"value11\"]]}}", jp.GetJson("$.x.y[1][0][0]"u8).ToString()); + Assert.AreEqual("{\"a\":[[\"value11\"]]}", jp.GetJson("$.x.y[1][0][0].z"u8).ToString()); + Assert.AreEqual("[[\"value11\"]]", jp.GetJson("$.x.y[1][0][0].z.a"u8).ToString()); + Assert.AreEqual("[\"value11\"]", jp.GetJson("$.x.y[1][0][0].z.a[0]"u8).ToString()); + Assert.AreEqual("value11", jp.GetString("$.x.y[1][0][0].z.a[0][0]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[[{\"z\":{\"a\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}},{\"z\":{\"a\":[[\"value5\",\"value6\"]]}}],[{\"z\":{\"a\":[[\"value7\",\"value8\"],[\"value9\",\"value10\"]]}}]],[[{\"z\":{\"a\":[[\"value11\"]]}}]]]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoDimensionTwoLevel_WithProperty() + { + JsonPatch jp = new("{\"x\":{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x.y[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[1][1].a"u8)); + + jp.Append("$.x.y[0]"u8, "{\"a\":\"value5\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"}]", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[0]"u8, "{\"a\":\"value6\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "{\"a\":\"value7\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"}]", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "{\"a\":\"value8\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y"u8, "[{\"a\":\"value9\"}]"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}],[{\"a\":\"value9\"}]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][1].a"u8)); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x.y[0][2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][2].a"u8)); + Assert.AreEqual("{\"a\":\"value6\"}", jp.GetJson("$.x.y[0][3]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][3].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x.y[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[1][1].a"u8)); + Assert.AreEqual("{\"a\":\"value7\"}", jp.GetJson("$.x.y[1][2]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[1][2].a"u8)); + Assert.AreEqual("{\"a\":\"value8\"}", jp.GetJson("$.x.y[1][3]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x.y[1][3].a"u8)); + Assert.AreEqual("[{\"a\":\"value9\"}]", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value9\"}", jp.GetJson("$.x.y[2][0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[2][0].a"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}],[{\"a\":\"value9\"}]]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoDimensionTwoLevel() + { + JsonPatch jp = new("{\"x\":{\"y\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[1][1]"u8)); + + jp.Append("$.x.y[0]"u8, "value5"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value5\"]", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[0]"u8, "value6"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "value7"); + + Assert.AreEqual("[\"value3\",\"value4\",\"value7\"]", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "value8"); + + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y"u8, "[\"value9\"]"u8); + + Assert.AreEqual("{\"y\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value2\",\"value5\",\"value6\"],[\"value3\",\"value4\",\"value7\",\"value8\"],[\"value9\"]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][1]"u8)); + Assert.AreEqual("value5", jp.GetString("$.x.y[0][2]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x.y[0][3]"u8)); + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[1][1]"u8)); + Assert.AreEqual("value7", jp.GetString("$.x.y[1][2]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x.y[1][3]"u8)); + Assert.AreEqual("[\"value9\"]", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[2][0]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[\"value1\",\"value2\",\"value5\",\"value6\"],[\"value3\",\"value4\",\"value7\",\"value8\"],[\"value9\"]]}}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoDimensionTwoLevel_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x.y[0]"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("{\"y\":[[{\"a\":\"value1\"}]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0]"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"}]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y"u8, "[{\"a\":\"value5\"}]"u8); + + Assert.AreEqual("{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x.y[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x.y[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x.y[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x.y[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[1][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value5\"}]", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x.y[2][0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[2][0].a"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]}}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoDimensionTwoLevel() + { + JsonPatch jp = new(); + + jp.Append("$.x.y[0]"u8, "value1"); + + Assert.AreEqual("{\"y\":[[\"value1\"]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0]"u8, "value2"); + + Assert.AreEqual("{\"y\":[[\"value1\",\"value2\"]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "value3"); + + Assert.AreEqual("{\"y\":[[\"value1\",\"value2\"],[\"value3\"]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1]"u8, "value4"); + + Assert.AreEqual("{\"y\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y"u8, "[\"value5\"]"u8); + + Assert.AreEqual("{\"y\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[1][1]"u8)); + Assert.AreEqual("[\"value5\"]", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[2][0]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoDimensionOneLevel_WithProperty() + { + JsonPatch jp = new("{\"x\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]}"u8.ToArray()); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1][1].a"u8)); + + jp.Append("$.x[0]"u8, "{\"a\":\"value5\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"}]", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[0]"u8, "{\"a\":\"value6\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[1]"u8, "{\"a\":\"value7\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"}]", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x[1]"u8, "{\"a\":\"value8\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x"u8, "[{\"a\":\"value9\"}]"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}],[{\"a\":\"value9\"}]]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0][1].a"u8)); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x[0][2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[0][2].a"u8)); + Assert.AreEqual("{\"a\":\"value6\"}", jp.GetJson("$.x[0][3]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x[0][3].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1][1].a"u8)); + Assert.AreEqual("{\"a\":\"value7\"}", jp.GetJson("$.x[1][2]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x[1][2].a"u8)); + Assert.AreEqual("{\"a\":\"value8\"}", jp.GetJson("$.x[1][3]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x[1][3].a"u8)); + Assert.AreEqual("[{\"a\":\"value9\"}]", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value9\"}", jp.GetJson("$.x[2][0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x[2][0].a"u8)); + + Assert.AreEqual("{\"x\":[[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}],[{\"a\":\"value9\"}]]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoDimensionOneLevel() + { + JsonPatch jp = new("{\"x\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]}"u8.ToArray()); + + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1][1]"u8)); + + jp.Append("$.x[0]"u8, "value5"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value5\"]", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[0]"u8, "value6"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[1]"u8, "value7"); + + Assert.AreEqual("[\"value3\",\"value4\",\"value7\"]", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x[1]"u8, "value8"); + + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x"u8, "[\"value9\"]"u8); + + Assert.AreEqual("[[\"value1\",\"value2\",\"value5\",\"value6\"],[\"value3\",\"value4\",\"value7\",\"value8\"],[\"value9\"]]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0][1]"u8)); + Assert.AreEqual("value5", jp.GetString("$.x[0][2]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x[0][3]"u8)); + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1][1]"u8)); + Assert.AreEqual("value7", jp.GetString("$.x[1][2]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x[1][3]"u8)); + Assert.AreEqual("[\"value9\"]", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x[2][0]"u8)); + + Assert.AreEqual("{\"x\":[[\"value1\",\"value2\",\"value5\",\"value6\"],[\"value3\",\"value4\",\"value7\",\"value8\"],[\"value9\"]]}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoDimensionOneLevel_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x[0]"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"}]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[0]"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1]"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"}]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1]"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "[{\"a\":\"value5\"}]"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value5\"}]", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x[2][0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[2][0].a"u8)); + + Assert.AreEqual("{\"x\":[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoDimensionOneLevel() + { + JsonPatch jp = new(); + + jp.Append("$.x[0]"u8, "value1"); + + Assert.AreEqual("[[\"value1\"]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[0]"u8, "value2"); + + Assert.AreEqual("[[\"value1\",\"value2\"]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1]"u8, "value3"); + + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\"]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1]"u8, "value4"); + + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "[\"value5\"]"u8); + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1][1]"u8)); + Assert.AreEqual("[\"value5\"]", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[2][0]"u8)); + + Assert.AreEqual("{\"x\":[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoDimension_WithProperty() + { + JsonPatch jp = new("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}]]"u8.ToArray()); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$[0][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$[1][1].a"u8)); + + jp.Append("$[0]"u8, "{\"a\":\"value5\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"}]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[0]"u8, "{\"a\":\"value6\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[1]"u8, "{\"a\":\"value7\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"}]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$[1]"u8, "{\"a\":\"value8\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$"u8, "[{\"a\":\"value9\"}]"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}],[{\"a\":\"value9\"}]]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$[0][1].a"u8)); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$[0][2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$[0][2].a"u8)); + Assert.AreEqual("{\"a\":\"value6\"}", jp.GetJson("$[0][3]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$[0][3].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$[1][1].a"u8)); + Assert.AreEqual("{\"a\":\"value7\"}", jp.GetJson("$[1][2]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$[1][2].a"u8)); + Assert.AreEqual("{\"a\":\"value8\"}", jp.GetJson("$[1][3]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$[1][3].a"u8)); + Assert.AreEqual("[{\"a\":\"value9\"}]", jp.GetJson("$[2]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value9\"}", jp.GetJson("$[2][0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$[2][0].a"u8)); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}],[{\"a\":\"value9\"}]]", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoDimension() + { + JsonPatch jp = new("[[\"value1\",\"value2\"],[\"value3\",\"value4\"]]"u8.ToArray()); + + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$[1][1]"u8)); + + jp.Append("$[0]"u8, "value5"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value5\"]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[0]"u8, "value6"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[1]"u8, "value7"); + + Assert.AreEqual("[\"value3\",\"value4\",\"value7\"]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$[1]"u8, "value8"); + + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$"u8, "[\"value9\"]"u8); + + Assert.AreEqual("[[\"value1\",\"value2\",\"value5\",\"value6\"],[\"value3\",\"value4\",\"value7\",\"value8\"],[\"value9\"]]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$[0][1]"u8)); + Assert.AreEqual("value5", jp.GetString("$[0][2]"u8)); + Assert.AreEqual("value6", jp.GetString("$[0][3]"u8)); + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$[1][1]"u8)); + Assert.AreEqual("value7", jp.GetString("$[1][2]"u8)); + Assert.AreEqual("value8", jp.GetString("$[1][3]"u8)); + Assert.AreEqual("[\"value9\"]", jp.GetJson("$[2]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$[2][0]"u8)); + + Assert.AreEqual("[[\"value1\",\"value2\",\"value5\",\"value6\"],[\"value3\",\"value4\",\"value7\",\"value8\"],[\"value9\"]]", jp.ToString("J")); + } + + [Test] + public void Insert_TwoDimension_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$[0]"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"}]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[0]"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[1]"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"}]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$[1]"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$"u8, "[{\"a\":\"value5\"}]"u8); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$[0][0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0][0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$[0][1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$[0][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$[1][0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[1][0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$[1][1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$[1][1].a"u8)); + Assert.AreEqual("[{\"a\":\"value5\"}]", jp.GetJson("$[2]"u8).ToString()); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$[2][0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$[2][0].a"u8)); + + Assert.AreEqual("[[{\"a\":\"value1\"},{\"a\":\"value2\"}],[{\"a\":\"value3\"},{\"a\":\"value4\"}],[{\"a\":\"value5\"}]]", jp.ToString("J")); + } + + [Test] + public void Insert_TwoDimension() + { + JsonPatch jp = new(); + + jp.Append("$[0]"u8, "value1"); + + Assert.AreEqual("[\"value1\"]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[0]"u8, "value2"); + + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$[0]"u8).ToString()); + + jp.Append("$[1]"u8, "value3"); + + Assert.AreEqual("[\"value3\"]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$[1]"u8, "value4"); + + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$[1]"u8).ToString()); + + jp.Append("$"u8, "[\"value5\"]"u8); + + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0][0]"u8)); + Assert.AreEqual("value2", jp.GetString("$[0][1]"u8)); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[1][0]"u8)); + Assert.AreEqual("value4", jp.GetString("$[1][1]"u8)); + Assert.AreEqual("[\"value5\"]", jp.GetJson("$[2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$[2][0]"u8)); + + Assert.AreEqual("[[\"value1\",\"value2\"],[\"value3\",\"value4\"],[\"value5\"]]", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoLevelTwoLevel_WithProperty() + { + JsonPatch jp = new("{\"x\":{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}", jp.GetJson("$.x.y[0].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value1\"},{\"b\":\"value2\"}]", jp.GetJson("$.x.y[0].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value1\"}", jp.GetJson("$.x.y[0].z.a[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].z.a[0].b"u8)); + Assert.AreEqual("{\"b\":\"value2\"}", jp.GetJson("$.x.y[0].z.a[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0].z.a[1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}", jp.GetJson("$.x.y[1].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value3\"},{\"b\":\"value4\"}]", jp.GetJson("$.x.y[1].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value3\"}", jp.GetJson("$.x.y[1].z.a[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1].z.a[0].b"u8)); + Assert.AreEqual("{\"b\":\"value4\"}", jp.GetJson("$.x.y[1].z.a[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[1].z.a[1].b"u8)); + + jp.Append("$.x.y[0].z.a"u8, "{\"b\":\"value5\"}"u8); + + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"}]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[0].z.a"u8, "{\"b\":\"value6\"}"u8); + + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"},{\"b\":\"value6\"}]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "{\"b\":\"value7\"}"u8); + + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"}]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "{\"b\":\"value8\"}"u8); + + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"},{\"b\":\"value8\"}]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y"u8, "{\"z\":{\"a\":[{\"b\":\"value9\"}]}}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"},{\"b\":\"value6\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"},{\"b\":\"value8\"}]}},{\"z\":{\"a\":[{\"b\":\"value9\"}]}}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"},{\"b\":\"value6\"}]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"},{\"b\":\"value6\"}]}", jp.GetJson("$.x.y[0].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"},{\"b\":\"value6\"}]", jp.GetJson("$.x.y[0].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value1\"}", jp.GetJson("$.x.y[0].z.a[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].z.a[0].b"u8)); + Assert.AreEqual("{\"b\":\"value2\"}", jp.GetJson("$.x.y[0].z.a[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0].z.a[1].b"u8)); + Assert.AreEqual("{\"b\":\"value5\"}", jp.GetJson("$.x.y[0].z.a[2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[0].z.a[2].b"u8)); + Assert.AreEqual("{\"b\":\"value6\"}", jp.GetJson("$.x.y[0].z.a[3]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x.y[0].z.a[3].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"},{\"b\":\"value8\"}]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"},{\"b\":\"value8\"}]}", jp.GetJson("$.x.y[1].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"},{\"b\":\"value8\"}]", jp.GetJson("$.x.y[1].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value3\"}", jp.GetJson("$.x.y[1].z.a[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1].z.a[0].b"u8)); + Assert.AreEqual("{\"b\":\"value4\"}", jp.GetJson("$.x.y[1].z.a[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[1].z.a[1].b"u8)); + Assert.AreEqual("{\"b\":\"value7\"}", jp.GetJson("$.x.y[1].z.a[2]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x.y[1].z.a[2].b"u8)); + Assert.AreEqual("{\"b\":\"value8\"}", jp.GetJson("$.x.y[1].z.a[3]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x.y[1].z.a[3].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value9\"}]}}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value9\"}]}", jp.GetJson("$.x.y[2].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value9\"}]", jp.GetJson("$.x.y[2].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value9\"}", jp.GetJson("$.x.y[2].z.a[0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[2].z.a[0].b"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"},{\"b\":\"value5\"},{\"b\":\"value6\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"},{\"b\":\"value7\"},{\"b\":\"value8\"}]}},{\"z\":{\"a\":[{\"b\":\"value9\"}]}}]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoLevelTwoLevel() + { + JsonPatch jp = new("{\"x\":{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}}]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[\"value1\",\"value2\"]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value1\",\"value2\"]}", jp.GetJson("$.x.y[0].z"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0].z.a"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].z.a[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0].z.a[1]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[\"value3\",\"value4\"]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value3\",\"value4\"]}", jp.GetJson("$.x.y[1].z"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[1].z.a"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1].z.a[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[1].z.a[1]"u8)); + + jp.Append("$.x.y[0].z.a"u8, "value5"); + + Assert.AreEqual("{\"z\":{\"a\":[\"value1\",\"value2\",\"value5\"]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[0].z.a"u8, "value6"); + + Assert.AreEqual("{\"z\":{\"a\":[\"value1\",\"value2\",\"value5\",\"value6\"]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "value7"); + + Assert.AreEqual("{\"z\":{\"a\":[\"value3\",\"value4\",\"value7\"]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "value8"); + + Assert.AreEqual("{\"z\":{\"a\":[\"value3\",\"value4\",\"value7\",\"value8\"]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + + jp.Append("$.x.y"u8, "{\"z\":{\"a\":[\"value9\"]}}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[\"value1\",\"value2\",\"value5\",\"value6\"]}},{\"z\":{\"a\":[\"value3\",\"value4\",\"value7\",\"value8\"]}},{\"z\":{\"a\":[\"value9\"]}}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[\"value1\",\"value2\",\"value5\",\"value6\"]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value1\",\"value2\",\"value5\",\"value6\"]}", jp.GetJson("$.x.y[0].z"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x.y[0].z.a"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].z.a[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0].z.a[1]"u8)); + Assert.AreEqual("value5", jp.GetString("$.x.y[0].z.a[2]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x.y[0].z.a[3]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[\"value3\",\"value4\",\"value7\",\"value8\"]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value3\",\"value4\",\"value7\",\"value8\"]}", jp.GetJson("$.x.y[1].z"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x.y[1].z.a"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1].z.a[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[1].z.a[1]"u8)); + Assert.AreEqual("value7", jp.GetString("$.x.y[1].z.a[2]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x.y[1].z.a[3]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[\"value9\"]}}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value9\"]}", jp.GetJson("$.x.y[2].z"u8).ToString()); + Assert.AreEqual("[\"value9\"]", jp.GetJson("$.x.y[2].z.a"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x.y[2].z.a[0]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\",\"value5\",\"value6\"]}},{\"z\":{\"a\":[\"value3\",\"value4\",\"value7\",\"value8\"]}},{\"z\":{\"a\":[\"value9\"]}}]}}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoLevelTwoLevel_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x.y[0].z.a"u8, "{\"b\":\"value1\"}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0].z.a"u8, "{\"b\":\"value2\"}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "{\"b\":\"value3\"}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "{\"b\":\"value4\"}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y"u8, "{\"z\":{\"a\":[{\"b\":\"value5\"}]}}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}},{\"z\":{\"a\":[{\"b\":\"value5\"}]}}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}},{\"z\":{\"a\":[{\"b\":\"value5\"}]}}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}", jp.GetJson("$.x.y[0].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value1\"},{\"b\":\"value2\"}]", jp.GetJson("$.x.y[0].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value1\"}", jp.GetJson("$.x.y[0].z.a[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].z.a[0].b"u8)); + Assert.AreEqual("{\"b\":\"value2\"}", jp.GetJson("$.x.y[0].z.a[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[0].z.a[1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}", jp.GetJson("$.x.y[1].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value3\"},{\"b\":\"value4\"}]", jp.GetJson("$.x.y[1].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value3\"}", jp.GetJson("$.x.y[1].z.a[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1].z.a[0].b"u8)); + Assert.AreEqual("{\"b\":\"value4\"}", jp.GetJson("$.x.y[1].z.a[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[1].z.a[1].b"u8)); + Assert.AreEqual("{\"z\":{\"a\":[{\"b\":\"value5\"}]}}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("{\"a\":[{\"b\":\"value5\"}]}", jp.GetJson("$.x.y[2].z"u8).ToString()); + Assert.AreEqual("[{\"b\":\"value5\"}]", jp.GetJson("$.x.y[2].z.a"u8).ToString()); + Assert.AreEqual("{\"b\":\"value5\"}", jp.GetJson("$.x.y[2].z.a[0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[2].z.a[0].b"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[{\"z\":{\"a\":[{\"b\":\"value1\"},{\"b\":\"value2\"}]}},{\"z\":{\"a\":[{\"b\":\"value3\"},{\"b\":\"value4\"}]}},{\"z\":{\"a\":[{\"b\":\"value5\"}]}}]}}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoLevelTwoLevel() + { + JsonPatch jp = new(); + + jp.Append("$.x.y[0].z.a"u8, "value1"); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\"]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[0].z.a"u8, "value2"); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "value3"); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\"]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y[1].z.a"u8, "value4"); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y"u8, "{\"z\":{\"a\":[\"value5\"]}}"u8); + + Assert.AreEqual("{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}},{\"z\":{\"a\":[\"value5\"]}}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}},{\"z\":{\"a\":[\"value5\"]}}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"z\":{\"a\":[\"value1\",\"value2\"]}}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value1\",\"value2\"]}", jp.GetJson("$.x.y[0].z"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y[0].z.a"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].z.a[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[0].z.a[1]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[\"value3\",\"value4\"]}}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value3\",\"value4\"]}", jp.GetJson("$.x.y[1].z"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x.y[1].z.a"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[1].z.a[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[1].z.a[1]"u8)); + Assert.AreEqual("{\"z\":{\"a\":[\"value5\"]}}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("{\"a\":[\"value5\"]}", jp.GetJson("$.x.y[2].z"u8).ToString()); + Assert.AreEqual("[\"value5\"]", jp.GetJson("$.x.y[2].z.a"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x.y[2].z.a[0]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[{\"z\":{\"a\":[\"value1\",\"value2\"]}},{\"z\":{\"a\":[\"value3\",\"value4\"]}},{\"z\":{\"a\":[\"value5\"]}}]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoLevel_WithProperty() + { + JsonPatch jp = new("{\"x\":{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[1].a"u8)); + + jp.Append("$.x.y"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"}]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[1].a"u8)); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x.y[2].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x.y[3]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x.y[3].a"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"},{\"a\":\"value4\"}]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_TwoLevel() + { + JsonPatch jp = new("{\"x\":{\"y\":[\"value1\",\"value2\"]}}"u8.ToArray()); + + Assert.AreEqual("{\"y\":[\"value1\",\"value2\"]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[1]"u8)); + + jp.Append("$.x.y"u8, "value3"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value3\"]", jp.GetJson("$.x.y"u8).ToString()); + + jp.Append("$.x.y"u8, "value4"); + + Assert.AreEqual("{\"y\":[\"value1\",\"value2\"]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value3\",\"value4\"]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[1]"u8)); + Assert.AreEqual("value3", jp.GetString("$.x.y[2]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x.y[3]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[\"value1\",\"value2\",\"value3\",\"value4\"]}}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoLevel_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x.y"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"}]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x.y[1].a"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}}", jp.ToString("J")); + } + + [Test] + public void Insert_TwoLevel() + { + JsonPatch jp = new(); + + jp.Append("$.x.y"u8, "value1"); + + Assert.AreEqual("{\"y\":[\"value1\"]}", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x.y"u8, "value2"); + + Assert.AreEqual("{\"y\":[\"value1\",\"value2\"]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x.y[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x.y[1]"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[\"value1\",\"value2\"]}}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OneLevelTwoLevel_WithProperty() + { + JsonPatch jp = new("{\"x\":[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}}]}"u8.ToArray()); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x[0].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0].y.z[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y.z[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0].y.z[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0].y.z[1].a"u8)); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x[1].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1].y.z[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y.z[0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1].y.z[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1].y.z[1].a"u8)); + + jp.Append("$.x[0].y.z"u8, "{\"a\":\"value5\"}"u8); + + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"}]}}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[0].y.z"u8, "{\"a\":\"value6\"}"u8); + + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "{\"a\":\"value7\"}"u8); + + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"}]}}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "{\"a\":\"value8\"}"u8); + + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":{\"z\":[{\"a\":\"value9\"}]}}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}},{\"y\":{\"z\":[{\"a\":\"value9\"}]}}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x[0].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0].y.z[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y.z[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0].y.z[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0].y.z[1].a"u8)); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x[0].y.z[2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[0].y.z[2].a"u8)); + Assert.AreEqual("{\"a\":\"value6\"}", jp.GetJson("$.x[0].y.z[3]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x[0].y.z[3].a"u8)); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$.x[1].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1].y.z[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y.z[0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1].y.z[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1].y.z[1].a"u8)); + Assert.AreEqual("{\"a\":\"value7\"}", jp.GetJson("$.x[1].y.z[2]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x[1].y.z[2].a"u8)); + Assert.AreEqual("{\"a\":\"value8\"}", jp.GetJson("$.x[1].y.z[3]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x[1].y.z[3].a"u8)); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value9\"}]}}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value9\"}]}", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value9\"}]", jp.GetJson("$.x[2].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value9\"}", jp.GetJson("$.x[2].y.z[0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x[2].y.z[0].a"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}},{\"y\":{\"z\":[{\"a\":\"value9\"}]}}]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OneLevelTwoLevel() + { + JsonPatch jp = new("{\"x\":[{\"y\":{\"z\":[\"value1\",\"value2\"]}},{\"y\":{\"z\":[\"value3\",\"value4\"]}}]}"u8.ToArray()); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\",\"value2\"]}},{\"y\":{\"z\":[\"value3\",\"value4\"]}}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":{\"z\":[\"value1\",\"value2\"]}}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value1\",\"value2\"]}", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x[0].y.z"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y.z[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0].y.z[1]"u8)); + Assert.AreEqual("{\"y\":{\"z\":[\"value3\",\"value4\"]}}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value3\",\"value4\"]}", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x[1].y.z"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y.z[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1].y.z[1]"u8)); + + jp.Append("$.x[0].y.z"u8, "value5"); + + Assert.AreEqual("{\"y\":{\"z\":[\"value1\",\"value2\",\"value5\"]}}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[0].y.z"u8, "value6"); + + Assert.AreEqual("{\"y\":{\"z\":[\"value1\",\"value2\",\"value5\",\"value6\"]}}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "value7"); + + Assert.AreEqual("{\"y\":{\"z\":[\"value3\",\"value4\",\"value7\"]}}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "value8"); + + Assert.AreEqual("{\"y\":{\"z\":[\"value3\",\"value4\",\"value7\",\"value8\"]}}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":{\"z\":[\"value9\"]}}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\",\"value2\",\"value5\",\"value6\"]}},{\"y\":{\"z\":[\"value3\",\"value4\",\"value7\",\"value8\"]}},{\"y\":{\"z\":[\"value9\"]}}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":{\"z\":[\"value1\",\"value2\",\"value5\",\"value6\"]}}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value1\",\"value2\",\"value5\",\"value6\"]}", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x[0].y.z"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y.z[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0].y.z[1]"u8)); + Assert.AreEqual("value5", jp.GetString("$.x[0].y.z[2]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x[0].y.z[3]"u8)); + Assert.AreEqual("{\"y\":{\"z\":[\"value3\",\"value4\",\"value7\",\"value8\"]}}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value3\",\"value4\",\"value7\",\"value8\"]}", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x[1].y.z"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y.z[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1].y.z[1]"u8)); + Assert.AreEqual("value7", jp.GetString("$.x[1].y.z[2]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x[1].y.z[3]"u8)); + Assert.AreEqual("{\"y\":{\"z\":[\"value9\"]}}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value9\"]}", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("[\"value9\"]", jp.GetJson("$.x[2].y.z"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x[2].y.z[0]"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":{\"z\":[\"value1\",\"value2\",\"value5\",\"value6\"]}},{\"y\":{\"z\":[\"value3\",\"value4\",\"value7\",\"value8\"]}},{\"y\":{\"z\":[\"value9\"]}}]}", jp.ToString("J")); + } + + [Test] + public void Insert_OneLevelTwoLevel_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x[0].y.z"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"}]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[0].y.z"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"}]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":{\"z\":[{\"a\":\"value5\"}]}}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}},{\"y\":{\"z\":[{\"a\":\"value5\"}]}}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x[0].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0].y.z[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y.z[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0].y.z[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0].y.z[1].a"u8)); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x[1].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1].y.z[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y.z[0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1].y.z[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1].y.z[1].a"u8)); + Assert.AreEqual("{\"y\":{\"z\":[{\"a\":\"value5\"}]}}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("{\"z\":[{\"a\":\"value5\"}]}", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value5\"}]", jp.GetJson("$.x[2].y.z"u8).ToString()); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x[2].y.z[0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[2].y.z[0].a"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":{\"z\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}},{\"y\":{\"z\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}},{\"y\":{\"z\":[{\"a\":\"value5\"}]}}]}", jp.ToString("J")); + } + + [Test] + public void Insert_OneLevelTwoLevel() + { + JsonPatch jp = new(); + + jp.Append("$.x[0].y.z"u8, "value1"); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\"]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[0].y.z"u8, "value2"); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\",\"value2\"]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "value3"); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\",\"value2\"]}},{\"y\":{\"z\":[\"value3\"]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y.z"u8, "value4"); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\",\"value2\"]}},{\"y\":{\"z\":[\"value3\",\"value4\"]}}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":{\"z\":[\"value5\"]}}"u8); + + Assert.AreEqual("[{\"y\":{\"z\":[\"value1\",\"value2\"]}},{\"y\":{\"z\":[\"value3\",\"value4\"]}},{\"y\":{\"z\":[\"value5\"]}}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":{\"z\":[\"value1\",\"value2\"]}}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value1\",\"value2\"]}", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x[0].y.z"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y.z[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0].y.z[1]"u8)); + Assert.AreEqual("{\"y\":{\"z\":[\"value3\",\"value4\"]}}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value3\",\"value4\"]}", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x[1].y.z"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y.z[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1].y.z[1]"u8)); + Assert.AreEqual("{\"y\":{\"z\":[\"value5\"]}}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("{\"z\":[\"value5\"]}", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("[\"value5\"]", jp.GetJson("$.x[2].y.z"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[2].y.z[0]"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":{\"z\":[\"value1\",\"value2\"]}},{\"y\":{\"z\":[\"value3\",\"value4\"]}},{\"y\":{\"z\":[\"value5\"]}}]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OneLevelTwice_WithProperty() + { + JsonPatch jp = new(new("{\"x\":[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}]}"u8.ToArray())); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0].y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0].y[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0].y[1].a"u8)); + Assert.AreEqual("{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1].y[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1].y[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1].y[1].a"u8)); + + jp.Append("$.x[0].y"u8, "{\"a\":\"value5\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"}]}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[0].y"u8, "{\"a\":\"value6\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[1].y"u8, "{\"a\":\"value7\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"}]}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x[1].y"u8, "{\"a\":\"value8\"}"u8); + + Assert.AreEqual("{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":[{\"a\":\"value9\"}]}"u8); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]},{\"y\":[{\"a\":\"value9\"}]}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0].y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0].y[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0].y[1].a"u8)); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x[0].y[2]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[0].y[2].a"u8)); + Assert.AreEqual("{\"a\":\"value6\"}", jp.GetJson("$.x[0].y[3]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x[0].y[3].a"u8)); + Assert.AreEqual("{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1].y[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1].y[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1].y[1].a"u8)); + Assert.AreEqual("{\"a\":\"value7\"}", jp.GetJson("$.x[1].y[2]"u8).ToString()); + Assert.AreEqual("value7", jp.GetString("$.x[1].y[2].a"u8)); + Assert.AreEqual("{\"a\":\"value8\"}", jp.GetJson("$.x[1].y[3]"u8).ToString()); + Assert.AreEqual("value8", jp.GetString("$.x[1].y[3].a"u8)); + Assert.AreEqual("{\"y\":[{\"a\":\"value9\"}]}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value9\"}]", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value9\"}", jp.GetJson("$.x[2].y[0]"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x[2].y[0].a"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value5\"},{\"a\":\"value6\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"},{\"a\":\"value7\"},{\"a\":\"value8\"}]},{\"y\":[{\"a\":\"value9\"}]}]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OneLevelTwice() + { + JsonPatch jp = new(new("{\"x\":[{\"y\":[\"value1\",\"value2\"]},{\"y\":[\"value3\",\"value4\"]}]}"u8.ToArray())); + + Assert.AreEqual("[{\"y\":[\"value1\",\"value2\"]},{\"y\":[\"value3\",\"value4\"]}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":[\"value1\",\"value2\"]}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0].y[1]"u8)); + Assert.AreEqual("{\"y\":[\"value3\",\"value4\"]}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1].y[1]"u8)); + + jp.Append("$.x[0].y"u8, "value5"); + + Assert.AreEqual("{\"y\":[\"value1\",\"value2\",\"value5\"]}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[0].y"u8, "value6"); + + Assert.AreEqual("{\"y\":[\"value1\",\"value2\",\"value5\",\"value6\"]}", jp.GetJson("$.x[0]"u8).ToString()); + + jp.Append("$.x[1].y"u8, "value7"); + + Assert.AreEqual("{\"y\":[\"value3\",\"value4\",\"value7\"]}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x[1].y"u8, "value8"); + + Assert.AreEqual("{\"y\":[\"value3\",\"value4\",\"value7\",\"value8\"]}", jp.GetJson("$.x[1]"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":[\"value9\"]}"u8); + + Assert.AreEqual("[{\"y\":[\"value1\",\"value2\",\"value5\",\"value6\"]},{\"y\":[\"value3\",\"value4\",\"value7\",\"value8\"]},{\"y\":[\"value9\"]}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":[\"value1\",\"value2\",\"value5\",\"value6\"]}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\",\"value5\",\"value6\"]", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0].y[1]"u8)); + Assert.AreEqual("value5", jp.GetString("$.x[0].y[2]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x[0].y[3]"u8)); + Assert.AreEqual("{\"y\":[\"value3\",\"value4\",\"value7\",\"value8\"]}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\",\"value7\",\"value8\"]", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1].y[1]"u8)); + Assert.AreEqual("value7", jp.GetString("$.x[1].y[2]"u8)); + Assert.AreEqual("value8", jp.GetString("$.x[1].y[3]"u8)); + Assert.AreEqual("{\"y\":[\"value9\"]}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("[\"value9\"]", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("value9", jp.GetString("$.x[2].y[0]"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":[\"value1\",\"value2\",\"value5\",\"value6\"]},{\"y\":[\"value3\",\"value4\",\"value7\",\"value8\"]},{\"y\":[\"value9\"]}]}", jp.ToString("J")); + } + + [Test] + public void Insert_OneLevelTwice_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x[0].y"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"}]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[0].y"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]},{\"y\":[{\"a\":\"value3\"}]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":[{\"a\":\"value5\"},{\"a\":\"value6\"}]}"u8); + + Assert.AreEqual("[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]},{\"y\":[{\"a\":\"value5\"},{\"a\":\"value6\"}]}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0].y[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[0].y[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[0].y[1].a"u8)); + Assert.AreEqual("{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[1].y[0]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[1].y[1]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[1].y[1].a"u8)); + Assert.AreEqual("{\"y\":[{\"a\":\"value5\"},{\"a\":\"value6\"}]}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("[{\"a\":\"value5\"},{\"a\":\"value6\"}]", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("{\"a\":\"value5\"}", jp.GetJson("$.x[2].y[0]"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[2].y[0].a"u8)); + Assert.AreEqual("{\"a\":\"value6\"}", jp.GetJson("$.x[2].y[1]"u8).ToString()); + Assert.AreEqual("value6", jp.GetString("$.x[2].y[1].a"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]},{\"y\":[{\"a\":\"value3\"},{\"a\":\"value4\"}]},{\"y\":[{\"a\":\"value5\"},{\"a\":\"value6\"}]}]}", jp.ToString("J")); + } + + [Test] + public void Insert_OneLevelTwice() + { + JsonPatch jp = new(); + + jp.Append("$.x[0].y"u8, "value1"); + + Assert.AreEqual("[{\"y\":[\"value1\"]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[0].y"u8, "value2"); + + Assert.AreEqual("[{\"y\":[\"value1\",\"value2\"]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y"u8, "value3"); + + Assert.AreEqual("[{\"y\":[\"value1\",\"value2\"]},{\"y\":[\"value3\"]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x[1].y"u8, "value4"); + + Assert.AreEqual("[{\"y\":[\"value1\",\"value2\"]},{\"y\":[\"value3\",\"value4\"]}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "{\"y\":[\"value5\",\"value6\"]}"u8); + + Assert.AreEqual("[{\"y\":[\"value1\",\"value2\"]},{\"y\":[\"value3\",\"value4\"]},{\"y\":[\"value5\",\"value6\"]}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"y\":[\"value1\",\"value2\"]}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x[0].y"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].y[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[0].y[1]"u8)); + Assert.AreEqual("{\"y\":[\"value3\",\"value4\"]}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("[\"value3\",\"value4\"]", jp.GetJson("$.x[1].y"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[1].y[0]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[1].y[1]"u8)); + Assert.AreEqual("{\"y\":[\"value5\",\"value6\"]}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("[\"value5\",\"value6\"]", jp.GetJson("$.x[2].y"u8).ToString()); + Assert.AreEqual("value5", jp.GetString("$.x[2].y[0]"u8)); + Assert.AreEqual("value6", jp.GetString("$.x[2].y[1]"u8)); + + Assert.AreEqual("{\"x\":[{\"y\":[\"value1\",\"value2\"]},{\"y\":[\"value3\",\"value4\"]},{\"y\":[\"value5\",\"value6\"]}]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OneLevel_WithProperty() + { + JsonPatch jp = new(new("{\"x\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}"u8.ToArray())); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[1].a"u8)); + + jp.Append("$.x"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[1].a"u8)); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$.x[2]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$.x[2].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$.x[3]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$.x[3].a"u8)); + + Assert.AreEqual("{\"x\":[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"},{\"a\":\"value4\"}]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_OneLevel() + { + JsonPatch jp = new(new("{\"x\":[\"value1\",\"value2\"]}"u8.ToArray())); + + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[1]"u8)); + + jp.Append("$.x"u8, "value3"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value3\"]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "value4"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value3\",\"value4\"]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[1]"u8)); + Assert.AreEqual("value3", jp.GetString("$.x[2]"u8)); + Assert.AreEqual("value4", jp.GetString("$.x[3]"u8)); + + Assert.AreEqual("{\"x\":[\"value1\",\"value2\",\"value3\",\"value4\"]}", jp.ToString("J")); + } + + [Test] + public void Insert_OneLevel_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$.x"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"}]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$.x[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$.x[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$.x[1].a"u8)); + + Assert.AreEqual("{\"x\":[{\"a\":\"value1\"},{\"a\":\"value2\"}]}", jp.ToString("J")); + } + + [Test] + public void Insert_OneLevel() + { + JsonPatch jp = new(); + + jp.Append("$.x"u8, "value1"); + + Assert.AreEqual("[\"value1\"]", jp.GetJson("$.x"u8).ToString()); + + jp.Append("$.x"u8, "value2"); + + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$.x[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$.x[1]"u8)); + + Assert.AreEqual("{\"x\":[\"value1\",\"value2\"]}", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_Root_WithProperty() + { + JsonPatch jp = new(new("[{\"a\":\"value1\"},{\"a\":\"value2\"}]"u8.ToArray())); + + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$[1].a"u8)); + + jp.Append("$"u8, "{\"a\":\"value3\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"}]", jp.GetJson("$"u8).ToString()); + + jp.Append("$"u8, "{\"a\":\"value4\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$[1].a"u8)); + Assert.AreEqual("{\"a\":\"value3\"}", jp.GetJson("$[2]"u8).ToString()); + Assert.AreEqual("value3", jp.GetString("$[2].a"u8)); + Assert.AreEqual("{\"a\":\"value4\"}", jp.GetJson("$[3]"u8).ToString()); + Assert.AreEqual("value4", jp.GetString("$[3].a"u8)); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"},{\"a\":\"value3\"},{\"a\":\"value4\"}]", jp.ToString("J")); + } + + [Test] + public void RootAndInsert_Root() + { + JsonPatch jp = new(new("[\"value1\",\"value2\"]"u8.ToArray())); + + Assert.AreEqual("value1", jp.GetString("$[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$[1]"u8)); + + jp.Append("$"u8, "value3"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value3\"]", jp.GetJson("$"u8).ToString()); + + jp.Append("$"u8, "value4"); + + Assert.AreEqual("[\"value1\",\"value2\",\"value3\",\"value4\"]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$[1]"u8)); + Assert.AreEqual("value3", jp.GetString("$[2]"u8)); + Assert.AreEqual("value4", jp.GetString("$[3]"u8)); + + Assert.AreEqual("[\"value1\",\"value2\",\"value3\",\"value4\"]", jp.ToString("J")); + } + + [Test] + public void Insert_Root_WithProperty() + { + JsonPatch jp = new(); + + jp.Append("$"u8, "{\"a\":\"value1\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"}]", jp.GetJson("$"u8).ToString()); + + jp.Append("$"u8, "{\"a\":\"value2\"}"u8); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("{\"a\":\"value1\"}", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0].a"u8)); + Assert.AreEqual("{\"a\":\"value2\"}", jp.GetJson("$[1]"u8).ToString()); + Assert.AreEqual("value2", jp.GetString("$[1].a"u8)); + + Assert.AreEqual("[{\"a\":\"value1\"},{\"a\":\"value2\"}]", jp.ToString("J")); + } + + [Test] + public void Insert_Root() + { + JsonPatch jp = new(); + + jp.Append("$"u8, "value1"); + + Assert.AreEqual("[\"value1\"]", jp.GetJson("$"u8).ToString()); + + jp.Append("$"u8, "value2"); + + Assert.AreEqual("[\"value1\",\"value2\"]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0]"u8)); + Assert.AreEqual("value2", jp.GetString("$[1]"u8)); + + Assert.AreEqual("[\"value1\",\"value2\"]", jp.ToString("J")); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchEncodedValueTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchEncodedValueTests.cs new file mode 100644 index 000000000000..b4c81173caf3 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchEncodedValueTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal class JsonPatchEncodedValueTests + { + [Test] + public void GetNullFromBytes() + { + byte[] nullBytes = "null"u8.ToArray(); + JsonPatch.EncodedValue value = new(nullBytes); + Assert.AreEqual(JsonPatch.ValueKind.Null, value.Kind); + Assert.IsTrue(nullBytes.AsSpan().SequenceEqual(value.Value.Span)); + + value = new(nullBytes.AsSpan()); + Assert.AreEqual(JsonPatch.ValueKind.Null, value.Kind); + Assert.IsTrue(nullBytes.AsSpan().SequenceEqual(value.Value.Span)); + } + + [Test] + public void GetBooleanTrueFromBytes() + { + byte[] trueBytes = "true"u8.ToArray(); + JsonPatch.EncodedValue trueValue = new(trueBytes); + Assert.AreEqual(JsonPatch.ValueKind.BooleanTrue, trueValue.Kind); + Assert.IsTrue(trueBytes.AsSpan().SequenceEqual(trueValue.Value.Span)); + + trueValue = new(trueBytes.AsSpan()); + Assert.AreEqual(JsonPatch.ValueKind.BooleanTrue, trueValue.Kind); + Assert.IsTrue(trueBytes.AsSpan().SequenceEqual(trueValue.Value.Span)); + } + + [Test] + public void GetBooleanFalseFromBytes() + { + byte[] falseBytes = "false"u8.ToArray(); + JsonPatch.EncodedValue falseValue = new(falseBytes); + Assert.AreEqual(JsonPatch.ValueKind.BooleanFalse, falseValue.Kind); + Assert.IsTrue(falseBytes.AsSpan().SequenceEqual(falseValue.Value.Span)); + + falseValue = new(falseBytes.AsSpan()); + Assert.AreEqual(JsonPatch.ValueKind.BooleanFalse, falseValue.Kind); + Assert.IsTrue(falseBytes.AsSpan().SequenceEqual(falseValue.Value.Span)); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchExtensions.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchExtensions.cs new file mode 100644 index 000000000000..b313f266e1f8 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Internal; +using System.Diagnostics.CodeAnalysis; + +namespace System.ClientModel.Tests.ModelReaderWriterTests; + +/// +/// Extension methods for JsonPatch. +/// +[Experimental("SCME0001")] +public static class JsonPatchExtensions +{ + /// + /// Sets the value at the specified JSON path in the JsonPatch object. + /// + /// The JsonPatch to set on. + /// The JSON path to set. + /// The to write into the . + public static void Set(ref this JsonPatch jsonPatch, ReadOnlySpan jsonPath, T value) + where T : IJsonModel + { + var writer = new ModelWriter(value, ModelReaderWriterOptions.Json); + using var reader = writer.ExtractReader(); + jsonPatch.Set(jsonPath, reader.ToBinaryData()); + } + + /// + /// Appends the value to an array at the specified JSON path in the JsonPatch object. + /// + /// The JsonPatch to set on. + /// The JSON path to set. + /// The to write into the . + public static void Append(ref this JsonPatch jsonPatch, ReadOnlySpan jsonPath, T value) + where T : IJsonModel + { + var writer = new ModelWriter(value, ModelReaderWriterOptions.Json); + using var reader = writer.ExtractReader(); + jsonPatch.Append(jsonPath, reader.ToBinaryData()); + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchPrimitiveTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchPrimitiveTests.cs new file mode 100644 index 000000000000..93d544404864 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchPrimitiveTests.cs @@ -0,0 +1,1078 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal class JsonPatchPrimitiveTests + { + [Test] + public void GetBoolean_Success() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, true); + jp.Set("$.property2"u8, false); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.property2"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(true, jp.GetBoolean("$.property"u8)); + Assert.AreEqual(false, jp.GetBoolean("$.property2"u8)); + Assert.AreEqual(true, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(false, jp.GetNullableValue("$.property2"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + Assert.IsTrue(jp.TryGetValue("$.property"u8, out bool property)); + Assert.AreEqual(true, property); + Assert.IsTrue(jp.TryGetValue("$.property2"u8, out bool property2)); + Assert.AreEqual(false, property2); + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out bool? nullableProperty)); + Assert.AreEqual(true, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.property2"u8, out bool? nullableProperty2)); + Assert.AreEqual(false, nullableProperty2); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out bool? nullablePropertyNull)); + Assert.AreEqual(null, nullablePropertyNull); + + Assert.AreEqual("{\"property\":true,\"property2\":false,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetBoolean_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetBoolean("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Boolean.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetBoolean("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetBoolean("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Boolean.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Boolean].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out bool property)); + Assert.AreEqual(default(bool), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out bool notPresent)); + Assert.AreEqual(default(bool), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out bool nullProperty)); + Assert.AreEqual(default(bool), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out bool? nullableProperty)); + Assert.AreEqual(default(bool?), nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out bool? nullableNotPresent)); + Assert.AreEqual(default(bool?), nullableNotPresent); + } + + [Test] + public void GetByte_Success() + { + JsonPatch jp = new(); + byte value = 5; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetByte("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out byte property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out byte? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out byte? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":5,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetByte_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetByte("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Byte.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetByte("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetByte("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Byte.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Byte].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out byte property)); + Assert.AreEqual(default(byte), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out byte notPresent)); + Assert.AreEqual(default(byte), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out byte nullProperty)); + Assert.AreEqual(default(byte), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out byte? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out byte? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetDateTime_Success() + { + JsonPatch jp = new(); + DateTime value = new(2025, 12, 25, 6, 7, 8); + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetDateTime("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out DateTime property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out DateTime? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out DateTime? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":\"12/25/2025 06:07:08\",\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetDateTime_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetDateTime("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.DateTime.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetDateTime("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetDateTime("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.DateTime.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.DateTime].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out DateTime property)); + Assert.AreEqual(default(DateTime), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out DateTime notPresent)); + Assert.AreEqual(default(DateTime), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out DateTime nullProperty)); + Assert.AreEqual(default(DateTime), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out DateTime? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out DateTime? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetDateTimeOffset_Success() + { + JsonPatch jp = new(); + DateTimeOffset value = new(2025, 12, 25, 6, 7, 8, new TimeSpan(2, 0, 0)); + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetDateTimeOffset("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out DateTimeOffset property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out DateTimeOffset? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out DateTimeOffset? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":\"12/25/2025 06:07:08 \\u002B02:00\",\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetDateTimeOffset_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetDateTimeOffset("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.DateTimeOffset.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetDateTimeOffset("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetDateTimeOffset("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.DateTimeOffset.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.DateTimeOffset].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out DateTimeOffset property)); + Assert.AreEqual(default(DateTimeOffset), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out DateTimeOffset notPresent)); + Assert.AreEqual(default(DateTimeOffset), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out DateTimeOffset nullProperty)); + Assert.AreEqual(default(DateTimeOffset), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out DateTimeOffset? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out DateTimeOffset? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetDecimal_Success() + { + JsonPatch jp = new(); + decimal value = (decimal)24.56; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetDecimal("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out decimal property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out decimal? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out decimal? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":24.56,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetDecimal_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetDecimal("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Decimal.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetDecimal("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetDecimal("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Decimal.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Decimal].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out decimal property)); + Assert.AreEqual(default(decimal), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out decimal notPresent)); + Assert.AreEqual(default(decimal), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out decimal nullProperty)); + Assert.AreEqual(default(decimal), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out decimal? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out decimal? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetDouble_Success() + { + JsonPatch jp = new(); + double value = 24.56; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetDouble("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out double property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out double? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out double? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":24.56,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetDouble_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetDouble("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Double.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetDouble("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetDouble("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Double.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Double].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out double property)); + Assert.AreEqual(default(double), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out double notPresent)); + Assert.AreEqual(default(double), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out double nullProperty)); + Assert.AreEqual(default(double), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out double? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out double? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetFloat_Success() + { + JsonPatch jp = new(); + float value = (float)24.5; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetFloat("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out float property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out float? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out float? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":24.5,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetFloat_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetFloat("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Single.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetFloat("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetFloat("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Single.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Single].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out float property)); + Assert.AreEqual(default(float), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out float notPresent)); + Assert.AreEqual(default(float), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out float nullProperty)); + Assert.AreEqual(default(float), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out float? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out float? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetGuid_Success() + { + JsonPatch jp = new(); + Guid value = new("12345678-1234-1234-1234-1234567890ab"); + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetGuid("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out Guid property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out Guid? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out Guid? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":\"12345678-1234-1234-1234-1234567890ab\",\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetGuid_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetGuid("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Guid.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetGuid("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetGuid("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Guid.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Guid].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out Guid property)); + Assert.AreEqual(default(Guid), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out Guid notPresent)); + Assert.AreEqual(default(Guid), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out Guid nullProperty)); + Assert.AreEqual(default(Guid), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out Guid? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out Guid? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetInt32_Success() + { + JsonPatch jp = new(); + int value = 123; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetInt32("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out int property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out int? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out int? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":123,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetInt32_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetInt32("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Int32.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetInt32("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetInt32("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Int32.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Int32].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out int property)); + Assert.AreEqual(default(int), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out int notPresent)); + Assert.AreEqual(default(int), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out int nullProperty)); + Assert.AreEqual(default(int), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out int? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out int? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetInt64_Success() + { + JsonPatch jp = new(); + long value = (long)int.MaxValue + 1; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetInt64("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out long property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out long? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out long? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":2147483648,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetInt64_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetInt64("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Int64.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetInt64("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetInt64("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Int64.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Int64].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out long property)); + Assert.AreEqual(default(long), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out long notPresent)); + Assert.AreEqual(default(long), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out long nullProperty)); + Assert.AreEqual(default(long), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out long? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out long? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetInt8_Success() + { + JsonPatch jp = new(); + sbyte value = 64; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetInt8("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out sbyte property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out sbyte? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out sbyte? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":64,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetInt8_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetInt8("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.SByte.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetInt8("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetInt8("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.SByte.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.SByte].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out sbyte property)); + Assert.AreEqual(default(sbyte), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out sbyte notPresent)); + Assert.AreEqual(default(sbyte), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out sbyte nullProperty)); + Assert.AreEqual(default(sbyte), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out sbyte? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out sbyte? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetInt16_Success() + { + JsonPatch jp = new(); + short value = sbyte.MaxValue + 1; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetInt16("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out short property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out short? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out short? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":128,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetInt16_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetInt16("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Int16.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetInt16("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetInt16("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.Int16.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.Int16].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out short property)); + Assert.AreEqual(default(short), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out short notPresent)); + Assert.AreEqual(default(short), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out short nullProperty)); + Assert.AreEqual(default(short), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out short? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out short? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetTimeSpan_Success() + { + JsonPatch jp = new(); + TimeSpan value = new(1, 1, 1, 1, 1); + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetTimeSpan("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out TimeSpan property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out TimeSpan? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out TimeSpan? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":\"1.01:01:01.0010000\",\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetTimeSpan_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetTimeSpan("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.TimeSpan.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetTimeSpan("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetTimeSpan("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.TimeSpan.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.TimeSpan].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out TimeSpan property)); + Assert.AreEqual(default(TimeSpan), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out TimeSpan notPresent)); + Assert.AreEqual(default(TimeSpan), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out TimeSpan nullProperty)); + Assert.AreEqual(default(TimeSpan), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out TimeSpan? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out TimeSpan? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetUInt32_Success() + { + JsonPatch jp = new(); + uint value = (uint)int.MaxValue + 1; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetUInt32("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out uint property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out uint? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out uint? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":2147483648,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetUInt32_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetUInt32("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.UInt32.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetUInt32("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetUInt32("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.UInt32.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.UInt32].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out uint property)); + Assert.AreEqual(default(uint), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out uint notPresent)); + Assert.AreEqual(default(uint), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out uint nullProperty)); + Assert.AreEqual(default(uint), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out uint? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out uint? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetUInt64_Success() + { + JsonPatch jp = new(); + ulong value = (ulong)long.MaxValue + 1; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetUInt64("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out ulong property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out ulong? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out ulong? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":9223372036854775808,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetUInt64_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetUInt64("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.UInt64.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetUInt64("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetUInt64("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.UInt64.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.UInt64].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out ulong property)); + Assert.AreEqual(default(ulong), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out ulong notPresent)); + Assert.AreEqual(default(ulong), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out ulong nullProperty)); + Assert.AreEqual(default(ulong), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out ulong? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out ulong? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + + [Test] + public void GetUInt16_Success() + { + JsonPatch jp = new(); + ushort value = (ushort)short.MaxValue + 1; + + jp.Set("$.property"u8, value); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + Assert.AreEqual(value, jp.GetUInt16("$.property"u8)); + + Assert.AreEqual(value, jp.GetNullableValue("$.property"u8)); + Assert.AreEqual(null, jp.GetNullableValue("$.nullProperty"u8)); + + Assert.IsTrue(jp.TryGetValue("$.property"u8, out ushort property)); + Assert.AreEqual(value, property); + + Assert.IsTrue(jp.TryGetNullableValue("$.property"u8, out ushort? nullableProperty)); + Assert.AreEqual(value, nullableProperty); + Assert.IsTrue(jp.TryGetNullableValue("$.nullProperty"u8, out ushort? nullProperty)); + Assert.AreEqual(null, nullProperty); + + Assert.AreEqual("{\"property\":32768,\"nullProperty\":null}", jp.ToString("J")); + } + + [Test] + public void GetUInt16_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "nonbool"); + jp.SetNull("$.nullProperty"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.nullProperty"u8)); + + var formatException = Assert.Throws(() => jp.GetUInt16("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.UInt16.", formatException!.Message); + var keyNotFoundException = Assert.Throws(() => jp.GetUInt16("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + formatException = Assert.Throws(() => jp.GetUInt16("$.nullProperty"u8)); + Assert.AreEqual("Value at '$.nullProperty' is not a System.UInt16.", formatException!.Message); + + formatException = Assert.Throws(() => jp.GetNullableValue("$.property"u8)); + Assert.AreEqual("Value at '$.property' is not a System.Nullable`1[System.UInt16].", formatException!.Message); + keyNotFoundException = Assert.Throws(() => jp.GetNullableValue("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", keyNotFoundException!.Message); + + Assert.IsFalse(jp.TryGetValue("$.property"u8, out ushort property)); + Assert.AreEqual(default(ushort), property); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out ushort notPresent)); + Assert.AreEqual(default(ushort), property); + Assert.IsFalse(jp.TryGetValue("$.nullProperty"u8, out ushort nullProperty)); + Assert.AreEqual(default(ushort), property); + + Assert.IsFalse(jp.TryGetNullableValue("$.property"u8, out ushort? nullableProperty)); + Assert.AreEqual(null, nullableProperty); + Assert.IsFalse(jp.TryGetNullableValue("$.notPresent"u8, out ushort? nullableNotPresent)); + Assert.AreEqual(null, nullableNotPresent); + } + [Test] + public void GetString_Success() + { + JsonPatch jp = new(); + string value = "value"; + + jp.Set("$.property"u8, value); + jp.SetNull("$.property2"u8); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.IsTrue(jp.Contains("$.property2"u8)); + + Assert.AreEqual(value, jp.GetString("$.property"u8)); + Assert.AreEqual(null, jp.GetString("$.property2"u8)); + Assert.IsTrue(jp.TryGetValue("$.property"u8, out string? property)); + Assert.AreEqual(value, property); + Assert.IsTrue(jp.TryGetValue("$.property2"u8, out property)); + Assert.AreEqual(null, property); + + Assert.AreEqual("{\"property\":\"value\",\"property2\":null}", jp.ToString("J")); + } + + [Test] + public void GetString_Fail() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, true); + + Assert.IsTrue(jp.Contains("$.property"u8)); + + Assert.AreEqual("true", jp.GetString("$.property"u8)); + Assert.IsTrue(jp.TryGetValue("$.property"u8, out string? property)); + Assert.AreEqual("true", property); + var ex = Assert.Throws(() => jp.GetString("$.notPresent"u8)); + Assert.AreEqual("No value found at JSON path '$.notPresent'.", ex!.Message); + Assert.IsFalse(jp.TryGetValue("$.notPresent"u8, out property)); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchRemoveTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchRemoveTests.cs new file mode 100644 index 000000000000..b360641d30c3 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchRemoveTests.cs @@ -0,0 +1,1239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal class JsonPatchRemoveTests + { + [Test] + public void Boolean_Empty() + { + JsonPatch jp = new(); + + jp.Set("$.x"u8, true); + jp.Set("$.y"u8, false); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(true, jp.GetBoolean("$.x"u8)); + Assert.AreEqual(false, jp.GetBoolean("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetBoolean("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetBoolean("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Boolean_NonEmpty() + { + JsonPatch jp = new("{\"x\":true,\"y\":false}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(true, jp.GetBoolean("$.x"u8)); + Assert.AreEqual(false, jp.GetBoolean("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetBoolean("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetBoolean("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Byte_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, (byte)1); + jp.Set("$.y"u8, (byte)2); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((byte)1, jp.GetByte("$.x"u8)); + Assert.AreEqual((byte)2, jp.GetByte("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetByte("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetByte("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Byte_NonEmpty() + { + JsonPatch jp = new("{\"x\":1,\"y\":2}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((byte)1, jp.GetByte("$.x"u8)); + Assert.AreEqual((byte)2, jp.GetByte("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetByte("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetByte("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Decimal_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 1.23m); + jp.Set("$.y"u8, 4.56m); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1.23m, jp.GetDecimal("$.x"u8)); + Assert.AreEqual(4.56m, jp.GetDecimal("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDecimal("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDecimal("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Decimal_NonEmpty() + { + JsonPatch jp = new("{\"x\":1.23,\"y\":4.56}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1.23m, jp.GetDecimal("$.x"u8)); + Assert.AreEqual(4.56m, jp.GetDecimal("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDecimal("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDecimal("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Double_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 1.23); + jp.Set("$.y"u8, 4.56); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1.23, jp.GetDouble("$.x"u8)); + Assert.AreEqual(4.56, jp.GetDouble("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDouble("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDouble("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Double_NonEmpty() + { + JsonPatch jp = new("{\"x\":1.23,\"y\":4.56}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1.23, jp.GetDouble("$.x"u8)); + Assert.AreEqual(4.56, jp.GetDouble("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDouble("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDouble("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Float_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 1.23f); + jp.Set("$.y"u8, 4.56f); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1.23f, jp.GetFloat("$.x"u8)); + Assert.AreEqual(4.56f, jp.GetFloat("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetFloat("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetFloat("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Float_NonEmpty() + { + JsonPatch jp = new("{\"x\":1.23,\"y\":4.56}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1.23f, jp.GetFloat("$.x"u8)); + Assert.AreEqual(4.56f, jp.GetFloat("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetFloat("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetFloat("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int32_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 1); + jp.Set("$.y"u8, 2); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1, jp.GetInt32("$.x"u8)); + Assert.AreEqual(2, jp.GetInt32("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt32("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt32("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int32_NonEmpty() + { + JsonPatch jp = new("{\"x\":1,\"y\":2}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1, jp.GetInt32("$.x"u8)); + Assert.AreEqual(2, jp.GetInt32("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt32("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt32("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int64_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 1234567890123L); + jp.Set("$.y"u8, 9876543210123L); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1234567890123L, jp.GetInt64("$.x"u8)); + Assert.AreEqual(9876543210123L, jp.GetInt64("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt64("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt64("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int64_NonEmpty() + { + JsonPatch jp = new("{\"x\":1234567890123,\"y\":9876543210123}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(1234567890123L, jp.GetInt64("$.x"u8)); + Assert.AreEqual(9876543210123L, jp.GetInt64("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt64("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt64("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int8_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, (sbyte)7); + jp.Set("$.y"u8, (sbyte)-3); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((sbyte)7, jp.GetInt8("$.x"u8)); + Assert.AreEqual((sbyte)-3, jp.GetInt8("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt8("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt8("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int8_NonEmpty() + { + JsonPatch jp = new("{\"x\":7,\"y\":-3}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((sbyte)7, jp.GetInt8("$.x"u8)); + Assert.AreEqual((sbyte)-3, jp.GetInt8("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt8("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt8("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int16_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, (short)32000); + jp.Set("$.y"u8, (short)-123); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((short)32000, jp.GetInt16("$.x"u8)); + Assert.AreEqual((short)-123, jp.GetInt16("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt16("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt16("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Int16_NonEmpty() + { + JsonPatch jp = new("{\"x\":32000,\"y\":-123}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((short)32000, jp.GetInt16("$.x"u8)); + Assert.AreEqual((short)-123, jp.GetInt16("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetInt16("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetInt16("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void UInt32_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 10u); + jp.Set("$.y"u8, 20u); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(10u, jp.GetUInt32("$.x"u8)); + Assert.AreEqual(20u, jp.GetUInt32("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetUInt32("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetUInt32("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void UInt32_NonEmpty() + { + JsonPatch jp = new("{\"x\":10,\"y\":20}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(10u, jp.GetUInt32("$.x"u8)); + Assert.AreEqual(20u, jp.GetUInt32("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetUInt32("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetUInt32("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void UInt64_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 10000000000UL); + jp.Set("$.y"u8, 20000000000UL); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(10000000000UL, jp.GetUInt64("$.x"u8)); + Assert.AreEqual(20000000000UL, jp.GetUInt64("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetUInt64("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetUInt64("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void UInt64_NonEmpty() + { + JsonPatch jp = new("{\"x\":10000000000,\"y\":20000000000}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(10000000000UL, jp.GetUInt64("$.x"u8)); + Assert.AreEqual(20000000000UL, jp.GetUInt64("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetUInt64("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetUInt64("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void UInt16_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, (ushort)60000); + jp.Set("$.y"u8, (ushort)1234); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((ushort)60000, jp.GetUInt16("$.x"u8)); + Assert.AreEqual((ushort)1234, jp.GetUInt16("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetUInt16("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetUInt16("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void UInt16_NonEmpty() + { + JsonPatch jp = new("{\"x\":60000,\"y\":1234}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual((ushort)60000, jp.GetUInt16("$.x"u8)); + Assert.AreEqual((ushort)1234, jp.GetUInt16("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetUInt16("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetUInt16("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void String_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, "foo"); + jp.Set("$.y"u8, "bar"); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual("foo", jp.GetString("$.x"u8)); + Assert.AreEqual("bar", jp.GetString("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetString("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetString("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void String_NonEmpty() + { + JsonPatch jp = new("{\"x\":\"foo\",\"y\":\"bar\"}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual("foo", jp.GetString("$.x"u8)); + Assert.AreEqual("bar", jp.GetString("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetString("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetString("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Json_Empty() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, "{\"a\":1}"u8.ToArray()); + jp.Set("$.y"u8, "{\"b\":2}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual("{\"a\":1}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"b\":2}", jp.GetJson("$.y"u8).ToString()); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetJson("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetJson("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Json_NonEmpty() + { + JsonPatch jp = new("{\"x\":{\"a\":1},\"y\":{\"b\":2}}"u8.ToArray()); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual("{\"a\":1}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("{\"b\":2}", jp.GetJson("$.y"u8).ToString()); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetJson("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetJson("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void DateTime_Empty() + { + JsonPatch jp = new(); + var dt1 = new DateTime(2024,1,2,3,4,5, DateTimeKind.Utc); + var dt2 = new DateTime(2025,2,3,4,5,6, DateTimeKind.Utc); + + jp.Set("$.x"u8, dt1); + jp.Set("$.y"u8, dt2); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(dt1, jp.GetDateTime("$.x"u8)); + Assert.AreEqual(dt2, jp.GetDateTime("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDateTime("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDateTime("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void DateTime_NonEmpty() + { + // Use numeric placeholders to make initial JSON valid. + JsonPatch jp = new("{\"x\":\"12/25/2025 06:07:08\",\"y\":\"12/26/2025 06:07:08\"}"u8.ToArray()); + var dt1 = new DateTime(2025, 12, 25, 6, 7, 8); + var dt2 = new DateTime(2025, 12, 26, 6, 7, 8); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(dt1, jp.GetDateTime("$.x"u8)); + Assert.AreEqual(dt2, jp.GetDateTime("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDateTime("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDateTime("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void DateTimeOffset_Empty() + { + JsonPatch jp = new(); + var dto1 = new DateTimeOffset(2024,1,2,3,4,5, TimeSpan.Zero); + var dto2 = new DateTimeOffset(2025,2,3,4,5,6, TimeSpan.Zero); + + jp.Set("$.x"u8, dto1); + jp.Set("$.y"u8, dto2); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(dto1, jp.GetDateTimeOffset("$.x"u8)); + Assert.AreEqual(dto2, jp.GetDateTimeOffset("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDateTimeOffset("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDateTimeOffset("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void DateTimeOffset_NonEmpty() + { + JsonPatch jp = new("{\"x\":\"12/25/2025 06:07:08 +02:00\",\"y\":\"12/26/2025 06:07:08 +02:00\"}"u8.ToArray()); + var dto1 = new DateTimeOffset(2025, 12, 25, 6, 7, 8, TimeSpan.FromHours(2)); + var dto2 = new DateTimeOffset(2025, 12, 26, 6, 7, 8, TimeSpan.FromHours(2)); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(dto1, jp.GetDateTimeOffset("$.x"u8)); + Assert.AreEqual(dto2, jp.GetDateTimeOffset("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetDateTimeOffset("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetDateTimeOffset("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Guid_Empty() + { + JsonPatch jp = new(); + var g1 = new Guid("00000000-0000-0000-0000-000000000001"); + var g2 = new Guid("00000000-0000-0000-0000-000000000002"); + + jp.Set("$.x"u8, g1); + jp.Set("$.y"u8, g2); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(g1, jp.GetGuid("$.x"u8)); + Assert.AreEqual(g2, jp.GetGuid("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetGuid("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetGuid("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void Guid_NonEmpty() + { + JsonPatch jp = new("{\"x\":\"00000000-0000-0000-0000-000000000001\",\"y\":\"00000000-0000-0000-0000-000000000002\"}"u8.ToArray()); + var g1 = new Guid("00000000-0000-0000-0000-000000000001"); + var g2 = new Guid("00000000-0000-0000-0000-000000000002"); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(g1, jp.GetGuid("$.x"u8)); + Assert.AreEqual(g2, jp.GetGuid("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetGuid("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetGuid("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void TimeSpan_Empty() + { + JsonPatch jp = new(); + var t1 = TimeSpan.FromHours(1); + var t2 = TimeSpan.FromMinutes(90); + + jp.Set("$.x"u8, t1); + jp.Set("$.y"u8, t2); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(t1, jp.GetTimeSpan("$.x"u8)); + Assert.AreEqual(t2, jp.GetTimeSpan("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetTimeSpan("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetTimeSpan("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void TimeSpan_NonEmpty() + { + JsonPatch jp = new("{\"x\":\"1.01:01:01.0010000\",\"y\":\"2.01:01:01.0010000\"}"u8.ToArray()); + var t1 = new TimeSpan(1, 1, 1, 1, 1); + var t2 = new TimeSpan(2, 1, 1, 1, 1); + + Assert.IsFalse(jp.IsRemoved("$.x"u8)); + Assert.IsFalse(jp.IsRemoved("$.y"u8)); + + Assert.AreEqual(t1, jp.GetTimeSpan("$.x"u8)); + Assert.AreEqual(t2, jp.GetTimeSpan("$.y"u8)); + + jp.Remove("$.x"u8); + jp.Remove("$.y"u8); + + Assert.IsTrue(jp.Contains("$.x"u8)); + Assert.IsTrue(jp.Contains("$.y"u8)); + Assert.IsTrue(jp.IsRemoved("$.x"u8)); + Assert.IsTrue(jp.IsRemoved("$.y"u8)); + + var ex = Assert.Throws(() => jp.GetTimeSpan("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetTimeSpan("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.x"u8)); + Assert.AreEqual($"No value found at JSON path '$.x'.", ex!.Message); + ex = Assert.Throws(() => jp.GetNullableValue("$.y"u8)); + Assert.AreEqual($"No value found at JSON path '$.y'.", ex!.Message); + + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void RemoveFromMixedArray_Seed() + { + JsonPatch jp = new("[1, \"two\", {\"three\":3}, [4]]"u8.ToArray()); + + jp.Remove("$[1]"u8); + + Assert.IsTrue(jp.Contains("$[1]"u8)); + + Assert.IsTrue(jp.IsRemoved("$[1]"u8)); + + Assert.AreEqual("[1, {\"three\":3}, [4]]", jp.ToString("J")); + } + + [Test] + public void RemoveFromMixedArray_New() + { + JsonPatch jp = new(); + + jp.Set("$"u8, "[1, \"two\", {\"three\":3}, [4]]"u8); + + jp.Remove("$[1]"u8); + + Assert.AreEqual("[1, {\"three\":3}, [4]]", jp.ToString("J")); + } + + [Test] + public void RemoveThenSetFromMixedArray_New() + { + JsonPatch jp = new(); + + jp.Set("$"u8, "[1,\"two\",{\"three\":3},[4]]"u8); + + jp.Remove("$[1]"u8); + jp.Set("$[1]"u8, "new"); + + Assert.AreEqual("[1,\"new\",[4]]", jp.ToString("J")); + } + + [Test] + public void RemoveFromMixedArray_Append() + { + JsonPatch jp = new(); + + jp.Append("$"u8, 1); + jp.Append("$"u8, "two"); + jp.Append("$"u8, "{\"three\":3}"u8); + jp.Append("$"u8, "[4]"u8); + + jp.Remove("$[1]"u8); + + Assert.AreEqual("[1,{\"three\":3},[4]]", jp.ToString("J")); + } + + [Test] + public void RemoveFromMultiDimensionalMixedArray() + { + JsonPatch jp = new("[[1,\"two\"],[{\"three\":3},[4]]]"u8.ToArray()); + + jp.Remove("$[0][1]"u8); + jp.Remove("$[1][0]"u8); + + Assert.IsTrue(jp.IsRemoved("$[0][1]"u8)); + Assert.IsTrue(jp.IsRemoved("$[1][0]"u8)); + + Assert.AreEqual("[[1],[[4]]]", jp.ToString("J")); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchTests.cs new file mode 100644 index 000000000000..5fe24d082ab4 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchTests.cs @@ -0,0 +1,465 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Internal; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal class JsonPatchTests + { + [Test] + public void AddPrimitive_StringAndInt() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "value"); + jp.Set("$.property2"u8, 10); + + Assert.IsTrue(jp.Contains("$.property"u8)); + Assert.AreEqual("value", jp.GetString("$.property"u8)); + Assert.AreEqual(10, jp.GetInt32("$.property2"u8)); + + Assert.AreEqual("{\"property\":\"value\",\"property2\":10}", jp.ToString("J")); + } + + [Test] + public void AddRootArray_String() + { + JsonPatch jp = new(); + + jp.Append("$"u8, "value"); + + Assert.AreEqual("[\"value\"]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("value", jp.GetString("$[0]"u8)); + + Assert.AreEqual("[\"value\"]", jp.ToString("J")); + } + + [Test] + public void AddRootArray_Property_String() + { + JsonPatch jp = new(); + + jp.Set("$[0].property"u8, "value"); + + Assert.AreEqual("[{\"property\":\"value\"}]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("{\"property\":\"value\"}", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value", jp.GetString("$[0].property"u8)); + + Assert.AreEqual("[{\"property\":\"value\"}]", jp.ToString("J")); + } + + [Test] + public void TryGetJson_RemovedProperty_EntryExists() + { + JsonPatch jp = new(); + + jp.Set("$[0].property1"u8, "value1"); + jp.Set("$[0].property2"u8, "value2"); + + Assert.AreEqual("[{\"property1\":\"value1\",\"property2\":\"value2\"}]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("{\"property1\":\"value1\",\"property2\":\"value2\"}", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0].property1"u8)); + Assert.AreEqual("value2", jp.GetString("$[0].property2"u8)); + + Assert.AreEqual("[{\"property1\":\"value1\",\"property2\":\"value2\"}]", jp.ToString("J")); + + jp.Remove("$[0].property1"u8); + + Assert.AreEqual("[{\"property2\":\"value2\"}]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("{\"property2\":\"value2\"}", jp.GetJson("$[0]"u8).ToString()); + var ex = Assert.Throws(() => jp.GetString("$[0].property1"u8)); + Assert.AreEqual("$[0].property1 was not found in the JSON structure.", ex!.Message); + Assert.AreEqual("value2", jp.GetString("$[0].property2"u8)); + + Assert.AreEqual("[{\"property2\":\"value2\"}]", jp.ToString("J")); + } + + [Test] + public void TryGetJson_RemovedProperty_EntryDoesNotExist() + { + JsonPatch jp = new(); + jp.Set("$[0].property1"u8, "value1"); + jp.Set("$[0].property2"u8, "value2"); + + Assert.AreEqual("[{\"property1\":\"value1\",\"property2\":\"value2\"}]", jp.GetJson("$"u8).ToString()); + Assert.AreEqual("{\"property1\":\"value1\",\"property2\":\"value2\"}", jp.GetJson("$[0]"u8).ToString()); + Assert.AreEqual("value1", jp.GetString("$[0].property1"u8)); + Assert.AreEqual("value2", jp.GetString("$[0].property2"u8)); + + Assert.AreEqual("[{\"property1\":\"value1\",\"property2\":\"value2\"}]", jp.ToString("J")); + + var ex = Assert.Throws(() => jp.Remove("$[0].property3"u8)); + Assert.AreEqual("$[0].property3 was not found in the JSON structure.", ex!.Message); + } + + [Test] + public void ProjectMultipleJsons() + { + JsonPatch jp = new(); + jp.Set("$.x.y[2]['z']"u8, 5); + + Assert.AreEqual("{\"y\":[null,null,{\"z\":5}]}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[null,null,{\"z\":5}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"z\":5}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual(5, jp.GetInt32("$.x.y[2]['z']"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[null,null,{\"z\":5}]}}", jp.ToString("J")); + + jp.Set("$.x.z.a[0].b"u8, 10); + + Assert.AreEqual("{\"y\":[null,null,{\"z\":5}],\"z\":{\"a\":[{\"b\":10}]}}", jp.GetJson("$.x"u8).ToString()); + Assert.AreEqual("[null,null,{\"z\":5}]", jp.GetJson("$.x.y"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.x.y[0]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.x.y[1]"u8).ToString()); + Assert.AreEqual("{\"z\":5}", jp.GetJson("$.x.y[2]"u8).ToString()); + Assert.AreEqual(5, jp.GetInt32("$.x.y[2]['z']"u8)); + Assert.AreEqual("{\"a\":[{\"b\":10}]}", jp.GetJson("$.x.z"u8).ToString()); + Assert.AreEqual("[{\"b\":10}]", jp.GetJson("$.x.z.a"u8).ToString()); + Assert.AreEqual("{\"b\":10}", jp.GetJson("$.x.z.a[0]"u8).ToString()); + Assert.AreEqual(10, jp.GetInt32("$.x.z.a[0].b"u8)); + + Assert.AreEqual("{\"x\":{\"y\":[null,null,{\"z\":5}],\"z\":{\"a\":[{\"b\":10}]}}}", jp.ToString("J")); + } + + [Test] + public void AddPropertyNameWithDot() + { + JsonPatch jp = new(); + jp.Set("$['pro.perty']"u8, "value"); + Assert.IsTrue(jp.Contains("$['pro.perty']"u8)); + Assert.AreEqual("value", jp.GetString("$['pro.perty']"u8)); + + Assert.AreEqual("{\"pro.perty\":\"value\"}", jp.ToString("J")); + } + + [Test] + public void Contains_ArrayAppend_DoesNotReportArrayPath() + { + JsonPatch jp = new(); + jp.Append("$.items"u8, "value1"); + Assert.IsFalse(jp.Contains("$.items"u8), "Append should not cause Contains(path) to report true for the array container path."); + + Assert.AreEqual("value1", jp.GetString("$.items[0]"u8)); + } + + [Test] + public void Contains_PrefixProperty_ReportsProperty() + { + JsonPatch jp = new("{\"parent\":{\"child\":1}}"u8.ToArray()); + jp.Set("$.parent.child"u8, 10); + + Assert.IsTrue(jp.Contains("$.parent.child"u8)); + Assert.IsTrue(jp.Contains("$.parent"u8, "child"u8)); + + Assert.IsFalse(jp.Contains("$.parent"u8, "missing"u8)); + Assert.IsFalse(jp.Contains("$.parent.child"u8, "grand"u8)); + } + + [Test] + public void Contains_PrefixProperty_ArrayAppendPotentialInconsistency() + { + JsonPatch jp = new(); + jp.Append("$.arr"u8, 5); + + Assert.IsFalse(jp.Contains("$.arr"u8), "Array container path should not be considered 'contained' after only an append."); + + Assert.IsTrue(jp.Contains("$"u8, "arr"u8), "Prefix/property Contains currently ignores ArrayItemAppend and reports true."); + } + + [Test] + public void IsRemoved_Behavior() + { + JsonPatch jp = new("{\"obj\":{\"a\":0,\"b\":1}}"u8.ToArray()); + + jp.Set("$.obj.a"u8, 1); + jp.Set("$.obj.b"u8, 2); + + Assert.IsFalse(jp.IsRemoved("$.obj.a"u8)); + Assert.IsFalse(jp.IsRemoved("$.obj.b"u8)); + + jp.Remove("$.obj.a"u8); + + Assert.IsTrue(jp.IsRemoved("$.obj.a"u8)); + Assert.IsFalse(jp.IsRemoved("$.obj.b"u8)); + + var ex = Assert.Throws(() => jp.GetInt32("$.obj.a"u8)); + Assert.AreEqual("No value found at JSON path '$.obj.a'.", ex!.Message); + } + + [Test] + public void RemoveThenResetValue() + { + JsonPatch jp = new(); + jp.Set("$.p"u8, 5); + jp.Remove("$.p"u8); + jp.Set("$.p"u8, 10); + Assert.IsFalse(jp.IsRemoved("$.p"u8)); + Assert.AreEqual(10, jp.GetInt32("$.p"u8)); + } + + [Test] + public void UnsupportedNullableType_Throws() + { + JsonPatch jp = new(); + jp.Set("$.c"u8, 1); // store as int + var ex = Assert.Throws(() => jp.GetNullableValue("$.c"u8)); + Assert.AreEqual("Type 'System.Char' is not supported by GetNullableValue.", ex!.Message); + } + + [Test] + public void ToString_InvalidFormat_Throws() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 1); + var ex = Assert.Throws(() => jp.ToString("INVALID")); + Assert.AreEqual("The format 'INVALID' is not supported.", ex!.Message); + } + + [Test] + public void Overwrite_Property_WithDifferentTypes() + { + JsonPatch jp = new(); + jp.Set("$.v"u8, 10); + Assert.AreEqual(10, jp.GetInt32("$.v"u8)); + jp.Set("$.v"u8, "str"); + Assert.AreEqual("str", jp.GetString("$.v"u8)); + } + + [Test] + public void TryGetValue_Removed_ReturnsFalse() + { + JsonPatch jp = new(); + jp.Set("$.x"u8, 5); + Assert.IsTrue(jp.TryGetValue("$.x"u8, out int v) && v == 5); + jp.Remove("$.x"u8); + Assert.IsFalse(jp.TryGetValue("$.x"u8, out int _)); + } + + [Test] + public void TryGetNullableValue_UnsupportedType_ReturnsFalse() + { + JsonPatch jp = new(); + jp.Set("$.a"u8, 1); + bool found = jp.TryGetNullableValue("$.a"u8, out char? c); + Assert.IsFalse(found); + Assert.False(c.HasValue); + } + + [Test] + public void Append_ToExistingArrayThenSetConcreteIndex() + { + JsonPatch jp = new(); + jp.Append("$.arr"u8, 1); + jp.Append("$.arr"u8, 2); + Assert.IsFalse(jp.Contains("$.arr"u8)); + jp.Set("$.arr[0]"u8, 10); + Assert.AreEqual(10, jp.GetInt32("$.arr[0]"u8)); + Assert.AreEqual(2, jp.GetInt32("$.arr[1]"u8)); + } + + [Test] + public void TryGetJson_OnNullValue() + { + JsonPatch jp = new(); + jp.SetNull("$.n"u8); + Assert.IsTrue(jp.TryGetJson("$.n"u8, out var mem)); + Assert.AreEqual("null", Encoding.UTF8.GetString(mem.Span.ToArray())); + } + + [Test] + public void Set_DeepNestedArrayAndProperties() + { + JsonPatch jp = new(); + + jp.Set("$.a.b[3].c.d[2].e"u8, 1); + + Assert.AreEqual("{\"b\":[null,null,null,{\"c\":{\"d\":[null,null,{\"e\":1}]}}]}", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual("[null,null,null,{\"c\":{\"d\":[null,null,{\"e\":1}]}}]", jp.GetJson("$.a.b"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a.b[0]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a.b[1]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a.b[2]"u8).ToString()); + Assert.AreEqual("{\"c\":{\"d\":[null,null,{\"e\":1}]}}", jp.GetJson("$.a.b[3]"u8).ToString()); + Assert.AreEqual("{\"d\":[null,null,{\"e\":1}]}", jp.GetJson("$.a.b[3].c"u8).ToString()); + Assert.AreEqual("[null,null,{\"e\":1}]", jp.GetJson("$.a.b[3].c.d"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a.b[3].c.d[0]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a.b[3].c.d[1]"u8).ToString()); + Assert.AreEqual("{\"e\":1}", jp.GetJson("$.a.b[3].c.d[2]"u8).ToString()); + Assert.AreEqual(1, jp.GetInt32("$.a.b[3].c.d[2].e"u8)); + + Assert.AreEqual("{\"a\":{\"b\":[null,null,null,{\"c\":{\"d\":[null,null,{\"e\":1}]}}]}}", jp.ToString("J")); + } + + [Test] + public void Append_ArrayItemPath_AppendsArrayInsideArrayIndex() + { + JsonPatch jp = new(); + + jp.Append("$.a[3]"u8, 5); + + Assert.AreEqual("[null,null,null,[5]]", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a[0]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a[1]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.a[2]"u8).ToString()); + Assert.AreEqual("[5]", jp.GetJson("$.a[3]"u8).ToString()); + Assert.AreEqual(5, jp.GetInt32("$.a[3][0]"u8)); + + Assert.AreEqual("{\"a\":[null,null,null,[5]]}", jp.ToString("J")); + } + + [Test] + public void Append_PropertyLeaf_AppendsArrayAtProperty() + { + JsonPatch jp = new(); + + jp.Append("$.a"u8, 5); + + Assert.AreEqual("[5]", jp.GetJson("$.a"u8).ToString()); + Assert.AreEqual(5, jp.GetInt32("$.a[0]"u8)); + + Assert.AreEqual("{\"a\":[5]}", jp.ToString("J")); + } + + [Test] + public void Append_PropertyLeaf_NestedAfterArrayIndex() + { + JsonPatch jp = new(); + + jp.Append("$.arr[2].items"u8, "x"); + + Assert.AreEqual("[null,null,{\"items\":[\"x\"]}]", jp.GetJson("$.arr"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.arr[0]"u8).ToString()); + Assert.AreEqual("null", jp.GetJson("$.arr[1]"u8).ToString()); + Assert.AreEqual("{\"items\":[\"x\"]}", jp.GetJson("$.arr[2]"u8).ToString()); + Assert.AreEqual("[\"x\"]", jp.GetJson("$.arr[2].items"u8).ToString()); + Assert.AreEqual("x", jp.GetString("$.arr[2].items[0]"u8)); + + Assert.AreEqual("{\"arr\":[null,null,{\"items\":[\"x\"]}]}", jp.ToString("J")); + } + + [Test] + public void Branch_ArrayIndex_Final_Append() + { + JsonPatch jp = new(); + + jp.Append("$.arr[2]"u8, 10); + + Assert.AreEqual("[null,null,[10]]", jp.GetJson("$.arr"u8).ToString()); + + Assert.AreEqual("{\"arr\":[null,null,[10]]}", jp.ToString("J")); + } + + [Test] + public void Branch_ArrayIndex_Final_Set() + { + JsonPatch jp = new(); + + jp.Set("$.arr[3]"u8, 11); + + Assert.AreEqual("[null,null,null,11]", jp.GetJson("$.arr"u8).ToString()); + + Assert.AreEqual("{\"arr\":[null,null,null,11]}", jp.ToString("J")); + } + + [Test] + public void Branch_PropertySeparator_Final() + { + JsonPatch jp = new(); + var ex = Assert.Throws(() => jp.Set("$.trailing."u8, 5)); + StringAssert.Contains("property", ex!.Message.ToLowerInvariant()); + } + + [Test] + public void Branch_DirectRoot_Final() + { + JsonPatch jp = new(); + + jp.Set("$"u8, "{\"x\":1}"u8); + + Assert.AreEqual("{\"x\":1}", jp.GetJson("$"u8).ToString()); + + Assert.AreEqual("{\"x\":1}", jp.ToString("J")); + } + + [Test] + public void WriteTo_SubPath_Removed() + { + JsonPatch jp = new("{\"a\":{\"b\":{\"x\":1,\"y\":2,\"z\":3}}}"u8.ToArray()); + + jp.Remove("$.a.b"u8); + + using var buffer = new UnsafeBufferSequence(); + using var writer = new Utf8JsonWriter(buffer); + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteStartObject(); + jp.WriteTo(writer, "$.a.b"u8); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.Flush(); + + using var reader = buffer.ExtractReader(); + Assert.AreEqual("{\"a\":{}}", reader.ToBinaryData().ToString()); + } + + [Test] + public void SerializeEmptyPatch() + { + JsonPatch jp = new(); + + Assert.AreEqual("[]", jp.ToString()); + Assert.AreEqual("{}", jp.ToString("J")); + } + + [Test] + public void SetEmptyJson() + { + JsonPatch jp = new(); + + jp.Set("$.a"u8, ""u8); + + var ex = Assert.Throws(() => jp.ToString("J")); + Assert.AreEqual("Empty encoded value", ex!.Message); + } + + [Test] + public void SerializeSeededEmptyPatch() + { + JsonPatch jp = new("{\"x\":1}"u8.ToArray()); + + Assert.AreEqual("[]", jp.ToString()); + Assert.AreEqual("{\"x\":1}", jp.ToString("J")); + } + + [Test] + public void AddMultiplePropertiesToRoot() + { + JsonPatch jp = new("{\"z\":100}"u8.ToArray()); + + jp.Set("$.a"u8, 1); + jp.Set("$.b"u8, 2); + jp.Set("$.c"u8, 3); + + Assert.AreEqual("{\"z\":100,\"a\":1,\"b\":2,\"c\":3}", jp.ToString("J")); + } + + [Test] + public void NullException_Format() + { + JsonPatch jp = new(); + var ex = Assert.Throws(() => jp.ToString(null!)); +#if NETFRAMEWORK + Assert.AreEqual("Value cannot be null.\r\nParameter name: format", ex!.Message); +#else + Assert.AreEqual("Value cannot be null. (Parameter 'format')", ex!.Message); +#endif + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchToJPFormatTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchToJPFormatTests.cs new file mode 100644 index 000000000000..41d207295d47 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/JsonPatchToJPFormatTests.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal class JsonPatchToJPFormatTests + { + [Test] + public void Add_Primitive() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "value"); + + Assert.IsTrue(jp.Contains("$.property"u8)); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/property\",\"value\":\"value\"}]", jp.ToString()); + } + + [Test] + public void Add_Multiple() + { + JsonPatch jp = new(); + + jp.Set("$.property"u8, "value"); + jp.Set("$.x.y.property2"u8, "value"); + + Assert.IsTrue(jp.Contains("$.property"u8)); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/property\",\"value\":\"value\"},{\"op\":\"add\",\"path\":\"/x\",\"value\":{\"y\":{\"property2\":\"value\"}}}]", jp.ToString()); + } + + [Test] + public void Add_AppendToArray() + { + JsonPatch jp = new(); + + jp.Append("$"u8, "value"); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/-\",\"value\":\"value\"}]", jp.ToString()); + } + + [Test] + public void Add_IndexedArrayItem() + { + JsonPatch jp = new(); + + jp.Set("$.x[0].y"u8, "value"); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/x\",\"value\":[{\"y\":\"value\"}]}]", jp.ToString()); + } + + [Test] + public void Add_AppendToIndexedArrayItem() + { + JsonPatch jp = new(); + + jp.Append("$.x[0].y"u8, "value"); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/x\",\"value\":[{\"y\":[\"value\"]}]}]", jp.ToString()); + } + + [Test] + public void Replace_Primitive() + { + JsonPatch jp = new("{\"property\":\"value\"}"u8.ToArray()); + + jp.Set("$.property"u8, "value2"); + + Assert.IsTrue(jp.Contains("$.property"u8)); + + Assert.AreEqual("[{\"op\":\"replace\",\"path\":\"/property\",\"value\":\"value2\"}]", jp.ToString()); + } + + [Test] + public void Replace_Multiple() + { + JsonPatch jp = new("{\"property\":\"value\",\"x\":{\"y\":{\"property2\":\"value\"}}}"u8.ToArray()); + + jp.Set("$.property"u8, "value2"); + jp.Set("$.x.y.property2"u8, "value2"); + + Assert.IsTrue(jp.Contains("$.property"u8)); + + Assert.AreEqual("[{\"op\":\"replace\",\"path\":\"/property\",\"value\":\"value2\"},{\"op\":\"replace\",\"path\":\"/x/y/property2\",\"value\":\"value2\"}]", jp.ToString()); + } + + [Test] + public void Replace_IndexedArrayItem() + { + JsonPatch jp = new("{\"x\":[{\"y\":\"value\"}]}"u8.ToArray()); + + jp.Set("$.x[0].y"u8, "value2"); + + Assert.AreEqual("[{\"op\":\"replace\",\"path\":\"/x/0/y\",\"value\":\"value2\"}]", jp.ToString()); + } + + [Test] + public void SetValueButNoChange() + { + JsonPatch jp = new("{\"x\":[{\"y\":\"value\"}]}"u8.ToArray()); + + jp.Set("$.x[0].y"u8, "value"); + + Assert.AreEqual("[]", jp.ToString()); + } + + [Test] + public void JsonPatch_ToString_JP_PathEscaping() + { + JsonPatch jp = new(); + + jp.Set("$['a~b/c']"u8, "v"); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/a~0b~1c\",\"value\":\"v\"}]", jp.ToString()); + } + + [Test] + public void JsonPatch_ToString_JP_ArrayAppendRoot() + { + JsonPatch jp = new(); + + jp.Append("$"u8, 5); + + Assert.AreEqual("[{\"op\":\"add\",\"path\":\"/-\",\"value\":5}]", jp.ToString()); + } + + [Test] + public void Remove_Primitive() + { + JsonPatch jp = new("{\"property\":\"value\"}"u8.ToArray()); + + jp.Remove("$.property"u8); + + Assert.AreEqual("[{\"op\":\"remove\",\"path\":\"/property\"}]", jp.ToString()); + } + + [Test] + public void Remove_IndexedArrayItem() + { + JsonPatch jp = new("{\"x\":[{\"y\":\"value\"}]}"u8.ToArray()); + + jp.Remove("$.x[0]"u8); + + Assert.AreEqual("[{\"op\":\"remove\",\"path\":\"/x/0\"}]", jp.ToString()); + } + + [Test] + public void Remove_IndexedArrayItem_Property() + { + JsonPatch jp = new("{\"x\":[{\"y\":\"value\"}]}"u8.ToArray()); + + jp.Remove("$.x[0].y"u8); + + Assert.AreEqual("[{\"op\":\"remove\",\"path\":\"/x/0/y\"}]", jp.ToString()); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.Array.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.Array.cs new file mode 100644 index 000000000000..9ad97d0df4e0 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.Array.cs @@ -0,0 +1,755 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client; +using System.ClientModel.Tests.Client.ModelReaderWriterTests.Models; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.ClientModel.Tests.Client.Models.ResourceManager.Resources; +using System.IO; +using System.Text; +using System.Text.Json; +using Azure.Core; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests.Models +{ + internal partial class AvailabilitySetDataTests + { + private static readonly JsonSerializerOptions s_camelCaseOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + [Test] + public void AddItemToExistingArrayThatIsEmpty() + { + var model = GetInitialModel(); + + model.Patch.Append("$.properties.virtualMachines"u8, "{\"id\":\"myNewVmId\"}"u8); + + Assert.AreEqual("[{\"id\":\"myNewVmId\"}]", model.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId\"}", model.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myNewVmId\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(1, model2.VirtualMachines.Count); + Assert.AreEqual("myNewVmId", model2.VirtualMachines[0].Id); + + AssertCommon(model, model2); + } + + [Test] + public void AddItemToNewArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "{\"x\":\"value1\"}"u8); + + Assert.AreEqual("[{\"x\":\"value1\"}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"}]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[{\"x\":\"value1\"}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void AddPrimitivesToNewArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, 5); + model.Patch.Append("$.newArray"u8, 10); + + Assert.AreEqual("[5,10]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("5", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("10", model.Patch.GetJson("$.newArray[1]"u8).ToString()); + Assert.AreEqual(5, model.Patch.GetInt32("$.newArray[0]"u8)); + Assert.AreEqual(10, model.Patch.GetInt32("$.newArray[1]"u8)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[5,10]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[5,10]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("5", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("10", model2.Patch.GetJson("$.newArray[1]"u8).ToString()); + Assert.AreEqual(5, model2.Patch.GetInt32("$.newArray[0]"u8)); + Assert.AreEqual(10, model2.Patch.GetInt32("$.newArray[1]"u8)); + + AssertCommon(model, model2); + } + + [Test] + public void ReplaceEntireArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "5"u8); + model.Patch.Append("$.newArray"u8, "10"u8); + + Assert.AreEqual("[5,10]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("5", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("10", model.Patch.GetJson("$.newArray[1]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[5,10]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[5,10]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("5", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("10", model2.Patch.GetJson("$.newArray[1]"u8).ToString()); + + AssertCommon(model, model2); + + model2.Patch.Set("$.newArray"u8, "[1,2,3]"u8); + + Assert.AreEqual("[1,2,3]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual(1, model2.Patch.GetInt32("$.newArray[0]"u8)); + Assert.AreEqual(2, model2.Patch.GetInt32("$.newArray[1]"u8)); + Assert.AreEqual(3, model2.Patch.GetInt32("$.newArray[2]"u8)); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[1,2,3]}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + + Assert.AreEqual("[1,2,3]", model3.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual(1, model3.Patch.GetInt32("$.newArray[0]"u8)); + Assert.AreEqual(2, model3.Patch.GetInt32("$.newArray[1]"u8)); + Assert.AreEqual(3, model3.Patch.GetInt32("$.newArray[2]"u8)); + } + + [Test] + public void AddItemToArrayThatIsNotEmpty() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId" }); + + model.Patch.Append("$.properties.virtualMachines"u8, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { id = "myNewVmId" }, s_camelCaseOptions))); + + Assert.AreEqual("[{\"id\":\"myNewVmId\"}]", model.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId\"}", model.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("myNewVmId", model.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId\"},{\"id\":\"myNewVmId\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(2, model2.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId", model2.VirtualMachines[0].Id); + Assert.AreEqual("myNewVmId", model2.VirtualMachines[1].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId\"},{\"id\":\"myNewVmId\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + Assert.AreEqual("myExistingVmId", model2.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + Assert.AreEqual("myNewVmId", model2.Patch.GetString("$.properties.virtualMachines[1].id"u8)); + + AssertCommon(model, model2); + } + + [Test] + public void AddMultipleItemsToExistingArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.properties.virtualMachines"u8, "{\"id\":\"myNewVmId1\"}"u8); + model.Patch.Append("$.properties.virtualMachines"u8, new WritableSubResource() { Id = "myNewVmId2" }); + model.Patch.Append("$.properties.virtualMachines"u8, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { id = "myNewVmId3" }, s_camelCaseOptions))); + model.Patch.Append("$.properties.virtualMachines"u8, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { Id = "myNewVmId4" }, s_camelCaseOptions))); + + Assert.AreEqual("[{\"id\":\"myNewVmId1\"},{\"id\":\"myNewVmId2\"},{\"id\":\"myNewVmId3\"},{\"id\":\"myNewVmId4\"}]", model.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId1\"}", model.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId2\"}", model.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId3\"}", model.Patch.GetJson("$.properties.virtualMachines[2]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId4\"}", model.Patch.GetJson("$.properties.virtualMachines[3]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myNewVmId1\"},{\"id\":\"myNewVmId2\"},{\"id\":\"myNewVmId3\"},{\"id\":\"myNewVmId4\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(4, model2.VirtualMachines.Count); + Assert.AreEqual("myNewVmId1", model2.VirtualMachines[0].Id); + Assert.AreEqual("myNewVmId2", model2.VirtualMachines[1].Id); + Assert.AreEqual("myNewVmId3", model2.VirtualMachines[2].Id); + Assert.AreEqual("myNewVmId4", model2.VirtualMachines[3].Id); + + Assert.AreEqual("[{\"id\":\"myNewVmId1\"},{\"id\":\"myNewVmId2\"},{\"id\":\"myNewVmId3\"},{\"id\":\"myNewVmId4\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId2\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId3\"}", model2.Patch.GetJson("$.properties.virtualMachines[2]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId4\"}", model2.Patch.GetJson("$.properties.virtualMachines[3]"u8).ToString()); + Assert.AreEqual("myNewVmId1", model2.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + Assert.AreEqual("myNewVmId2", model2.Patch.GetString("$.properties.virtualMachines[1].id"u8)); + Assert.AreEqual("myNewVmId3", model2.Patch.GetString("$.properties.virtualMachines[2].id"u8)); + Assert.AreEqual("myNewVmId4", model2.Patch.GetString("$.properties.virtualMachines[3].id"u8)); + + AssertCommon(model, model2); + } + + [Test] + public void ReplaceItemInArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "{\"x\":\"value1\"}"u8); + + Assert.AreEqual("[{\"x\":\"value1\"}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"}]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[{\"x\":\"value1\"}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + + model2.Patch.Set("$.newArray[0]"u8, "{\"x\":\"value2\"}"u8); + + Assert.AreEqual("[{\"x\":\"value2\"}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value2\"}]}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual("[{\"x\":\"value2\"}]", model3.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model3.Patch.GetJson("$.newArray[0]"u8).ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void ReplaceItemInArrayClr() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId1" }); + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId2" }); + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId3" }); + + var data = ModelReaderWriter.Write(model); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(3, model2.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId1", model2.VirtualMachines[0].Id); + Assert.AreEqual("myExistingVmId2", model2.VirtualMachines[1].Id); + Assert.AreEqual("myExistingVmId3", model2.VirtualMachines[2].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"},{\"id\":\"myExistingVmId2\"},{\"id\":\"myExistingVmId3\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId2\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId3\"}", model2.Patch.GetJson("$.properties.virtualMachines[2]"u8).ToString()); + Assert.AreEqual("myExistingVmId1", model2.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + Assert.AreEqual("myExistingVmId2", model2.Patch.GetString("$.properties.virtualMachines[1].id"u8)); + Assert.AreEqual("myExistingVmId3", model2.Patch.GetString("$.properties.virtualMachines[2].id"u8)); + + model2.Patch.Set("$.properties.virtualMachines[1]"u8, new WritableSubResource() { Id = "myNewVmId" }); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId1\"},{\"id\":\"myNewVmId\"},{\"id\":\"myExistingVmId3\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual(3, model3.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId1", model3.VirtualMachines[0].Id); + Assert.AreEqual("myNewVmId", model3.VirtualMachines[1].Id); + Assert.AreEqual("myExistingVmId3", model3.VirtualMachines[2].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"},{\"id\":\"myNewVmId\"},{\"id\":\"myExistingVmId3\"}]", model3.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model3.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId\"}", model3.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId3\"}", model3.Patch.GetJson("$.properties.virtualMachines[2]"u8).ToString()); + + Assert.AreEqual("myExistingVmId1", model3.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + Assert.AreEqual("myNewVmId", model3.Patch.GetString("$.properties.virtualMachines[1].id"u8)); + Assert.AreEqual("myExistingVmId3", model3.Patch.GetString("$.properties.virtualMachines[2].id"u8)); + } + + [Test] + public void ValidateArrayIndexes() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId1" }); + + var data = ModelReaderWriter.Write(model); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(1, model2.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId1", model2.VirtualMachines[0].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("myExistingVmId1", model2.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + + model2.Patch.Append("$.properties.virtualMachines"u8, new WritableSubResource() { Id = "myNewVmId1" }); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"},{\"id\":\"myNewVmId1\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("myExistingVmId1", model2.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + Assert.AreEqual("{\"id\":\"myNewVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + Assert.AreEqual("myNewVmId1", model2.Patch.GetString("$.properties.virtualMachines[1].id"u8)); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId1\"},{\"id\":\"myNewVmId1\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual(2, model3.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId1", model3.VirtualMachines[0].Id); + Assert.AreEqual("myNewVmId1", model3.VirtualMachines[1].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"},{\"id\":\"myNewVmId1\"}]", model3.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model3.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId1\"}", model3.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + + Assert.AreEqual("myExistingVmId1", model3.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + Assert.AreEqual("myNewVmId1", model3.Patch.GetString("$.properties.virtualMachines[1].id"u8)); + } + + [Test] + public void RemoveItemFromArrayClr() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId1" }); + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId2" }); + + model.Patch.Remove("$.properties.virtualMachines[0]"u8); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId2\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(1, model2.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId2", model2.VirtualMachines[0].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId2\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId2\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void RemoveEntireExistingArray() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId1" }); + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId2" }); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId1\"},{\"id\":\"myExistingVmId2\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(2, model2.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId1", model2.VirtualMachines[0].Id); + Assert.AreEqual("myExistingVmId2", model2.VirtualMachines[1].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"},{\"id\":\"myExistingVmId2\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId2\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + + model2.Patch.Remove("$.properties.virtualMachines"u8); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + + Assert.AreEqual(0, model3.VirtualMachines.Count); + + AssertCommon(model, model2); + } + + [Test] + public void RemoveInvalidIndexFromArrayClrDoesNothing() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId1" }); + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId2" }); + + var ex = Assert.Throws(() => model.Patch.Remove("$.properties.virtualMachines[4]"u8)); + Assert.AreEqual($"Cannot remove non-existing array item at path '$.properties.virtualMachines[4]'.", ex!.Message); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId1\"},{\"id\":\"myExistingVmId2\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(2, model2.VirtualMachines.Count); + Assert.AreEqual("myExistingVmId1", model2.VirtualMachines[0].Id); + Assert.AreEqual("myExistingVmId2", model2.VirtualMachines[1].Id); + + Assert.AreEqual("[{\"id\":\"myExistingVmId1\"},{\"id\":\"myExistingVmId2\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId1\"}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId2\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void RemoveItemFromArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "{\"x\":\"value1\"}"u8); + + Assert.AreEqual("[{\"x\":\"value1\"}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"}]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[{\"x\":\"value1\"}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + + model2.Patch.Remove("$.newArray[0]"u8); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[]}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual("[]", model3.Patch.GetJson("$.newArray"u8).ToString()); + + AssertCommon(model2, model3); + } + + [Test] + public void RemovePrimitiveFromArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, true); + model.Patch.Append("$.newArray"u8, false); + model.Patch.Append("$.newArray"u8, true); + + Assert.AreEqual("[true,false,true]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual(true, model.Patch.GetBoolean("$.newArray[0]"u8)); + Assert.AreEqual(false, model.Patch.GetBoolean("$.newArray[1]"u8)); + Assert.AreEqual(true, model.Patch.GetBoolean("$.newArray[2]"u8)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[true,false,true]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[true,false,true]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual(true, model2.Patch.GetBoolean("$.newArray[0]"u8)); + Assert.AreEqual(false, model2.Patch.GetBoolean("$.newArray[1]"u8)); + Assert.AreEqual(true, model2.Patch.GetBoolean("$.newArray[2]"u8)); + + model2.Patch.Remove("$.newArray[1]"u8); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[true,true]}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual("[true,true]", model3.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual(true, model3.Patch.GetBoolean("$.newArray[0]"u8)); + Assert.AreEqual(true, model3.Patch.GetBoolean("$.newArray[1]"u8)); + + AssertCommon(model2, model3); + } + + [Test] + public void RemoveItemFromArrayMulti() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "{\"x\":\"value1\"}"u8); + model.Patch.Append("$.newArray"u8, "{\"x\":\"value2\"}"u8); + + Assert.AreEqual("[{\"x\":\"value1\"},{\"x\":\"value2\"}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model.Patch.GetJson("$.newArray[1]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"},{\"x\":\"value2\"}]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[{\"x\":\"value1\"},{\"x\":\"value2\"}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model2.Patch.GetJson("$.newArray[1]"u8).ToString()); + + model2.Patch.Remove("$.newArray[0]"u8); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value2\"}]}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual("[{\"x\":\"value2\"}]", model3.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model3.Patch.GetJson("$.newArray[0]"u8).ToString()); + + AssertCommon(model2, model3); + } + + [Test] + public void RemoveMiddelItemFromArrayMulti() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "{\"x\":\"value1\"}"u8); + model.Patch.Append("$.newArray"u8, "{\"x\":\"value2\"}"u8); + model.Patch.Append("$.newArray"u8, "{\"x\":\"value3\"}"u8); + + Assert.AreEqual("[{\"x\":\"value1\"},{\"x\":\"value2\"},{\"x\":\"value3\"}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model.Patch.GetJson("$.newArray[1]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value3\"}", model.Patch.GetJson("$.newArray[2]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"},{\"x\":\"value2\"},{\"x\":\"value3\"}]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[{\"x\":\"value1\"},{\"x\":\"value2\"},{\"x\":\"value3\"}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model2.Patch.GetJson("$.newArray[1]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value3\"}", model2.Patch.GetJson("$.newArray[2]"u8).ToString()); + + model2.Patch.Remove("$.newArray[1]"u8); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"},{\"x\":\"value3\"}]}", + data2.ToString()); + + var model3 = GetRoundTripModel(data2); + Assert.AreEqual("[{\"x\":\"value1\"},{\"x\":\"value3\"}]", model3.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model3.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value3\"}", model3.Patch.GetJson("$.newArray[1]"u8).ToString()); + + AssertCommon(model2, model3); + } + + [Test] + public void AddPropertyToItemInArray() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId" }); + + model.Patch.Set("$.properties.virtualMachines[0].newProperty"u8, "propertyValue"); + model.VirtualMachines[0].Patch.Set("$.newPropFromArrayItem"u8, "propertyValue"); + + Assert.AreEqual("propertyValue", model.Patch.GetString("$.properties.virtualMachines[0].newProperty"u8)); + Assert.AreEqual("propertyValue", model.VirtualMachines[0].Patch.GetString("$.newPropFromArrayItem"u8)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId\",\"newProperty\":\"propertyValue\",\"newPropFromArrayItem\":\"propertyValue\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(1, model2.VirtualMachines.Count); + Assert.AreEqual("propertyValue", model2.VirtualMachines[0].Patch.GetString("$.newProperty"u8)); + Assert.AreEqual("propertyValue", model2.VirtualMachines[0].Patch.GetString("$.newPropFromArrayItem"u8)); + + AssertCommon(model, model2); + } + + [Test] + public void ChangePropertyInItemInArray() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId" }); + + model.Patch.Set("$.properties.virtualMachines[0].id"u8, "changedVmId"); + + Assert.AreEqual("changedVmId", model.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"changedVmId\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(1, model2.VirtualMachines.Count); + Assert.AreEqual("changedVmId", model2.VirtualMachines[0].Id); + + AssertCommon(model, model2); + } + + [Test] + public void RemovePropertyFromItemInArrayClr() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId1" }); + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId2" }); + + model.Patch.Remove("$.properties.virtualMachines[0].id"u8); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{},{\"id\":\"myExistingVmId2\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(2, model2.VirtualMachines.Count); + Assert.AreEqual(null, model2.VirtualMachines[0].Id); + Assert.AreEqual("myExistingVmId2", model2.VirtualMachines[1].Id); + + Assert.AreEqual("[{},{\"id\":\"myExistingVmId2\"}]", model2.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{}", model2.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("{\"id\":\"myExistingVmId2\"}", model2.Patch.GetJson("$.properties.virtualMachines[1]"u8).ToString()); + + AssertCommon(model, model2); + } + [Test] + public void RemovePropertyFromItemInArray() + { + var model = GetInitialModel(); + + model.Patch.Append("$.newArray"u8, "{\"x\":\"value1\"}"u8); + model.Patch.Append("$.newArray"u8, "{\"x\":\"value2\"}"u8); + + Assert.AreEqual("[{\"x\":\"value1\"},{\"x\":\"value2\"}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model.Patch.GetJson("$.newArray[1]"u8).ToString()); + + model.Patch.Remove("$.newArray[1].x"u8); + + Assert.AreEqual("[{\"x\":\"value1\"},{}]", model.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{}", model.Patch.GetJson("$.newArray[1]"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newArray\":[{\"x\":\"value1\"},{}]}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("[{\"x\":\"value1\"},{}]", model2.Patch.GetJson("$.newArray"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newArray[0]"u8).ToString()); + Assert.AreEqual("{}", model2.Patch.GetJson("$.newArray[1]"u8).ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void GetPropertyFromUnknownArrayEntry() + { + var model = GetInitialModel(); + + Assert.Throws(() => model.Patch.GetString("$.properties.unknownArray[0].id"u8)); + } + + [Test] + public void AddItemToRootArray() + { + var json = File.ReadAllText(TestData.GetLocation("AvailabilitySetData/List/JsonFormat.json")).TrimEnd(); + + var model = ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model); + Assert.AreEqual(2, model!.Items.Count); + Assert.AreEqual("testAS-3375", model.Items[0].Name); + Assert.AreEqual("testAS-3376", model.Items[1].Name); + Assert.AreEqual("testAS-3375", model.Patch.GetString("$[0].name"u8)); + Assert.AreEqual("testAS-3376", model.Patch.GetString("$[1].name"u8)); + + model.Patch.Append("$"u8, new AvailabilitySetData(AzureLocation.BrazilSouth) + { + Name = "testAS-3377", + Id = "/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377", + ResourceType = "Microsoft.Compute/availabilitySets", + }); + + var data = ModelReaderWriter.Write(model, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + Assert.AreEqual( + "[{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3}},{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}},{\"name\":\"testAS-3377\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377\",\"type\":\"Microsoft.Compute/availabilitySets\",\"location\":\"brazilsouth\",\"properties\":{}}]", + data.ToString()); + + var model2 = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model2); + Assert.AreEqual(3, model2!.Items.Count); + Assert.AreEqual("testAS-3375", model2.Items[0].Name); + Assert.AreEqual("testAS-3376", model2.Items[1].Name); + Assert.AreEqual("testAS-3377", model2.Items[2].Name); + Assert.AreEqual("testAS-3375", model2.Patch.GetString("$[0].name"u8)); + Assert.AreEqual("testAS-3376", model2.Patch.GetString("$[1].name"u8)); + Assert.AreEqual("testAS-3377", model2.Patch.GetString("$[2].name"u8)); + + Assert.AreEqual("brazilsouth", model2.Items[2].Location.ToString()); + Assert.AreEqual("brazilsouth", model2.Patch.GetString("$[2].location"u8)); + } + + [Test] + public void DoubleSerializeArray() + { + var model = GetInitialModel(); + + model.VirtualMachines.Add(new WritableSubResource() { Id = "myExistingVmId" }); + + model.Patch.Append("$.properties.virtualMachines"u8, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { id = "myNewVmId" }, s_camelCaseOptions))); + + Assert.AreEqual("[{\"id\":\"myNewVmId\"}]", model.Patch.GetJson("$.properties.virtualMachines"u8).ToString()); + Assert.AreEqual("{\"id\":\"myNewVmId\"}", model.Patch.GetJson("$.properties.virtualMachines[0]"u8).ToString()); + Assert.AreEqual("myNewVmId", model.Patch.GetString("$.properties.virtualMachines[0].id"u8)); + + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId\"},{\"id\":\"myNewVmId\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + ModelReaderWriter.Write(model).ToString()); + + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3,\"virtualMachines\":[{\"id\":\"myExistingVmId\"},{\"id\":\"myNewVmId\"}]},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + ModelReaderWriter.Write(model).ToString()); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.Dictionary.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.Dictionary.cs new file mode 100644 index 000000000000..5ea82ef9b216 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.Dictionary.cs @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client; +using System.ClientModel.Tests.Client.ModelReaderWriterTests.Models; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.IO; +using Azure.Core; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests.Models +{ + internal partial class AvailabilitySetDataTests + { + [Test] + public void AddItemToDictionary() + { + var model = GetInitialModel(); + + model.Patch.Set("$.newDictionary['key1']"u8, "{\"x\":\"value1\"}"u8); + model.Patch.Set("$.newDictionary.key2"u8, "{\"x\":\"value2\"}"u8); + + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newDictionary['key1']"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model.Patch.GetJson("$.newDictionary.key1"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model.Patch.GetJson("$.newDictionary['key2']"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model.Patch.GetJson("$.newDictionary.key2"u8).ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"newDictionary\":{\"key1\":{\"x\":\"value1\"},\"key2\":{\"x\":\"value2\"}}}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newDictionary['key1']"u8).ToString()); + Assert.AreEqual("{\"x\":\"value1\"}", model2.Patch.GetJson("$.newDictionary.key1"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model2.Patch.GetJson("$.newDictionary['key2']"u8).ToString()); + Assert.AreEqual("{\"x\":\"value2\"}", model2.Patch.GetJson("$.newDictionary.key2"u8).ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void AddItemToDictionaryClr() + { + var model = GetInitialModel(); + + model.Patch.Set("$.tags.insertedKey"u8, "insertedValue"); + model.Patch.Set("$['tags'].insertedKey2"u8, "insertedValue2"); + + Assert.AreEqual("insertedValue", model.Patch.GetString("$.tags.insertedKey"u8)); + Assert.AreEqual("insertedValue2", model.Patch.GetString("$.tags.insertedKey2"u8)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\",\"insertedKey\":\"insertedValue\",\"insertedKey2\":\"insertedValue2\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("insertedValue", model2.Patch.GetString("$['tags']['insertedKey']"u8)); + Assert.AreEqual("insertedValue", model2.Patch.GetString("$['tags'].insertedKey"u8)); + Assert.AreEqual("insertedValue", model2.Patch.GetString("$.tags.['insertedKey']"u8)); + Assert.AreEqual("insertedValue", model2.Patch.GetString("$.tags.insertedKey"u8)); + Assert.AreEqual("insertedValue2", model2.Patch.GetString("$['tags']['insertedKey2']"u8)); + Assert.AreEqual("insertedValue2", model2.Patch.GetString("$['tags'].insertedKey2"u8)); + Assert.AreEqual("insertedValue2", model2.Patch.GetString("$.tags['insertedKey2']"u8)); + Assert.AreEqual("insertedValue2", model2.Patch.GetString("$.tags.insertedKey2"u8)); + + AssertCommon(model, model2, "tags"); + } + + [Test] + public void ReplaceItemInDictionary() + { + var json = File.ReadAllText(TestData.GetLocation("AvailabilitySetData/Dictionary/JsonFormat.json")).TrimEnd(); + + var model = ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model); + Assert.AreEqual(2, model!.Items.Count); + Assert.IsTrue(model.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model.Items.ContainsKey("testAS-3376")); + Assert.AreEqual("testAS-3375", model.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3375", model.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model.Patch.GetString("$.testAS-3376.name"u8)); + + model.Patch.Set("$.testAS-3375"u8, new AvailabilitySetData(AzureLocation.BrazilSouth) + { + Name = "testAS-3377", + Id = "/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377", + ResourceType = "Microsoft.Compute/availabilitySets", + }); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"testAS-3375\":{\"name\":\"testAS-3377\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377\",\"type\":\"Microsoft.Compute/availabilitySets\",\"location\":\"brazilsouth\",\"properties\":{}},\"testAS-3376\":{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}}}", + data.ToString()); + + var model2 = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model2); + Assert.AreEqual(2, model2!.Items.Count); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3376")); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3375")); + Assert.AreEqual("testAS-3377", model2.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model2.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3377", model2.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model2.Patch.GetString("$.testAS-3376.name"u8)); + + Assert.AreEqual("brazilsouth", model2.Items["testAS-3375"].Location.ToString()); + Assert.AreEqual("brazilsouth", model2.Patch.GetString("$.testAS-3375.location"u8)); + } + + [Test] + public void RemoveItemFromDictionary() + { + var json = File.ReadAllText(TestData.GetLocation("AvailabilitySetData/Dictionary/JsonFormat.json")).TrimEnd(); + + var model = ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model); + Assert.AreEqual(2, model!.Items.Count); + Assert.IsTrue(model.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model.Items.ContainsKey("testAS-3376")); + Assert.AreEqual("testAS-3375", model.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3375", model.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model.Patch.GetString("$.testAS-3376.name"u8)); + + model.Patch.Remove("$.testAS-3375"u8); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"testAS-3376\":{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}}}", + data.ToString()); + + var model2 = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model2); + Assert.AreEqual(1, model2!.Items.Count); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3376")); + Assert.AreEqual("testAS-3376", model2.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3376", model2.Patch.GetString("$.testAS-3376.name"u8)); + } + + [Test] + public void AddPropertyToItemInDictionary() + { + var json = File.ReadAllText(TestData.GetLocation("AvailabilitySetData/Dictionary/JsonFormat.json")).TrimEnd(); + + var model = ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model); + Assert.AreEqual(2, model!.Items.Count); + Assert.IsTrue(model.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model.Items.ContainsKey("testAS-3376")); + Assert.AreEqual("testAS-3375", model.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3375", model.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model.Patch.GetString("$.testAS-3376.name"u8)); + + model.Patch.Set("$.testAS-3377"u8, new AvailabilitySetData(AzureLocation.BrazilSouth) + { + Name = "testAS-3377", + Id = "/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377", + ResourceType = "Microsoft.Compute/availabilitySets", + }); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"testAS-3375\":{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3}},\"testAS-3376\":{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}},\"testAS-3377\":{\"name\":\"testAS-3377\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377\",\"type\":\"Microsoft.Compute/availabilitySets\",\"location\":\"brazilsouth\",\"properties\":{}}}", + data.ToString()); + + var model2 = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model2); + Assert.AreEqual(3, model2!.Items.Count); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3376")); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3377")); + Assert.AreEqual("testAS-3377", model2.Items["testAS-3377"].Name); + Assert.AreEqual("testAS-3375", model2.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model2.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3377", model2.Patch.GetString("$.testAS-3377.name"u8)); + Assert.AreEqual("testAS-3375", model2.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model2.Patch.GetString("$.testAS-3376.name"u8)); + + Assert.AreEqual("brazilsouth", model2.Items["testAS-3377"].Location.ToString()); + Assert.AreEqual("brazilsouth", model2.Patch.GetString("$.testAS-3377.location"u8)); + + model2.Patch.Set("$.testAS-3377.foobar"u8, 999); + + Assert.AreEqual(999, model2.Patch.GetInt32("$.testAS-3377.foobar"u8)); + + var data2 = ModelReaderWriter.Write(model2); + Assert.AreEqual( + "{\"testAS-3375\":{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3}},\"testAS-3376\":{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}},\"testAS-3377\":{\"name\":\"testAS-3377\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3377\",\"type\":\"Microsoft.Compute/availabilitySets\",\"location\":\"brazilsouth\",\"properties\":{},\"foobar\":999}}", + data2.ToString()); + + var model3 = ModelReaderWriter.Read(data2, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model3); + Assert.AreEqual(3, model3!.Items.Count); + Assert.IsTrue(model3.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model3.Items.ContainsKey("testAS-3376")); + Assert.IsTrue(model3.Items.ContainsKey("testAS-3377")); + Assert.AreEqual("testAS-3377", model3.Items["testAS-3377"].Name); + Assert.AreEqual("testAS-3375", model3.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model3.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3377", model3.Patch.GetString("$.testAS-3377.name"u8)); + Assert.AreEqual("testAS-3375", model3.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model3.Patch.GetString("$.testAS-3376.name"u8)); + Assert.AreEqual("brazilsouth", model3.Items["testAS-3377"].Location.ToString()); + Assert.AreEqual("brazilsouth", model3.Patch.GetString("$.testAS-3377.location"u8)); + Assert.AreEqual(999, model3.Patch.GetInt32("$.testAS-3377.foobar"u8)); + } + + [Test] + public void ChangePropertyInItemInDictionary() + { + var json = File.ReadAllText(TestData.GetLocation("AvailabilitySetData/Dictionary/JsonFormat.json")).TrimEnd(); + + var model = ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model); + Assert.AreEqual(2, model!.Items.Count); + Assert.IsTrue(model.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model.Items.ContainsKey("testAS-3376")); + Assert.AreEqual(5, model.Items["testAS-3375"].PlatformUpdateDomainCount); + Assert.AreEqual("testAS-3375", model.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3375", model.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model.Patch.GetString("$.testAS-3376.name"u8)); + + model.Patch.Set("$.testAS-3375.properties.platformUpdateDomainCount"u8, 10); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"testAS-3375\":{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformFaultDomainCount\":3,\"platformUpdateDomainCount\":10}},\"testAS-3376\":{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}}}", + data.ToString()); + + var model2 = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model2); + Assert.AreEqual(2, model2!.Items.Count); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3376")); + Assert.AreEqual(10, model2.Items["testAS-3375"].PlatformUpdateDomainCount); + Assert.AreEqual("testAS-3375", model2.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model2.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3375", model2.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model2.Patch.GetString("$.testAS-3376.name"u8)); + Assert.AreEqual(10, model2.Patch.GetInt32("$.testAS-3375.properties.platformUpdateDomainCount"u8)); + } + + [Test] + public void RemovePropertyFromItemInDictionary() + { + var json = File.ReadAllText(TestData.GetLocation("AvailabilitySetData/Dictionary/JsonFormat.json")).TrimEnd(); + + var model = ModelReaderWriter.Read(BinaryData.FromString(json), ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model); + Assert.AreEqual(2, model!.Items.Count); + Assert.IsTrue(model.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model.Items.ContainsKey("testAS-3376")); + Assert.AreEqual("testAS-3375", model.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model.Items["testAS-3376"].Name); + Assert.AreEqual("testAS-3375", model.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model.Patch.GetString("$.testAS-3376.name"u8)); + + model.Patch.Remove("$.testAS-3375.name"u8); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"testAS-3375\":{\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3}},\"testAS-3376\":{\"name\":\"testAS-3376\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3376\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":6,\"platformFaultDomainCount\":4}}}", + data.ToString()); + + var model2 = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + + Assert.IsNotNull(model2); + Assert.AreEqual(2, model2!.Items.Count); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3375")); + Assert.IsTrue(model2.Items.ContainsKey("testAS-3376")); + Assert.AreEqual(null, model2.Items["testAS-3375"].Name); + Assert.AreEqual("testAS-3376", model2.Items["testAS-3376"].Name); + Assert.Throws(() => model2.Patch.GetString("$.testAS-3375.name"u8)); + Assert.AreEqual("testAS-3376", model2.Patch.GetString("$.testAS-3376.name"u8)); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.cs new file mode 100644 index 000000000000..eb47d1a4f012 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.Json.cs @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.Text.Json; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests.Models +{ + internal partial class AvailabilitySetDataTests + { + [Test] + public void NoSeedAndNoPatches() + { + AvailabilitySetData model = new("eastus"); + + Assert.IsFalse(model.Patch.TryGetJson("$.extraSku"u8, out _)); + } + + [Test] + public void AddInt32Property() + { + var model = GetInitialModel(); + var pointer = "$.foobar"u8; + model.Patch.Set(pointer, 5); + + Assert.AreEqual(5, model.Patch.GetInt32(pointer)); + Assert.AreEqual(5, model.Patch.GetNullableValue(pointer)); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":5}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":5}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(5, model2.Patch.GetInt32(pointer)); + Assert.AreEqual(5, model2.Patch.GetNullableValue(pointer)); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void GetInt32FailsWhenNull() + { + var model = GetInitialModel(); + model.Patch.SetNull("$.foobar"u8); + + Assert.Throws(() => model.Patch.GetInt32("$.foobar"u8)); + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":null}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(null, model2.Patch.GetNullableValue("$.foobar"u8)); + Assert.Throws(() => model2.Patch.GetInt32("$.foobar"u8)); + + AssertCommon(model, model2); + } + + [Test] + public void AddStringProperty() + { + var value = "some value"; + var pointer = "$.foobar"u8; + + var model = GetInitialModel(); + model.Patch.Set(pointer, value); + + Assert.AreEqual(value, model.Patch.GetString(pointer)); + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":\"some value\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(value, model2.Patch.GetString(pointer)); + + AssertCommon(model, model2); + } + + [Test] + public void AddSameStringProperty() + { + var value = "some value"; + var pointer = "$.foobar"u8; + + var model = GetInitialModel(); + model.Patch.Set(pointer, value); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":\"some value\"}]", + model.Patch.ToString()); + + model.Patch.Set("$['foobar']"u8, "some other value"); + + Assert.AreEqual("some other value", model.Patch.GetString(pointer)); + Assert.AreEqual("some other value", model.Patch.GetString("$['foobar']"u8)); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":\"some other value\"}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":\"some other value\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("some other value", model2.Patch.GetString(pointer)); + Assert.AreEqual("some other value", model2.Patch.GetString("$['foobar']"u8)); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void AddNullProperty() + { + var pointer = "$.foobar"u8; + + var model = GetInitialModel(); + model.Patch.SetNull(pointer); + Assert.AreEqual(null, model.Patch.GetString(pointer)); + Assert.AreEqual(null, model.Patch.GetNullableValue(pointer)); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":null}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":null}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(null, model2.Patch.GetString(pointer)); + Assert.AreEqual(null, model2.Patch.GetNullableValue(pointer)); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void ChangeFlattenedProperty() + { + ReadOnlySpan propertyNameSpan = "$.properties.platformUpdateDomainCount"u8; + int expectedValue = 999; + + var model = GetInitialModel(); + + model.Patch.Set(propertyNameSpan, expectedValue); + + Assert.AreEqual(expectedValue, model.Patch.GetInt32(propertyNameSpan)); + Assert.AreEqual(expectedValue, model.Patch.GetNullableValue(propertyNameSpan)); + Assert.AreEqual( + "[{\"op\":\"replace\",\"path\":\"/properties/platformUpdateDomainCount\",\"value\":999}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformFaultDomainCount\":3,\"platformUpdateDomainCount\":999},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + + Assert.AreEqual(expectedValue, model2.Patch.GetInt32(propertyNameSpan)); + Assert.AreEqual(expectedValue, model2.PlatformUpdateDomainCount); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2, "platformUpdateDomainCount"); + } + + [Test] + public void ChangeExistingProperty() + { + ReadOnlySpan propertyNameSpan = "$.location"u8; + string propertyName = "location"; + string expectedValue = "new-location"; + + var model = GetInitialModel(); + + model.Patch.Set(propertyNameSpan, expectedValue); + + Assert.AreEqual(expectedValue, model.Patch.GetString(propertyNameSpan)); + Assert.AreEqual("eastus", model.Location); + Assert.AreEqual( + "[{\"op\":\"replace\",\"path\":\"/location\",\"value\":\"new-location\"}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"location\":\"new-location\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(expectedValue, model2.Patch.GetString(propertyNameSpan)); + Assert.AreEqual(expectedValue, model2.Location); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2, propertyName); + } + + [Test] + public void ChangeExistingNestedProperty() + { + ReadOnlySpan propertyNameSpan = "$.sku.name"u8; + string expectedValue = "new-sku-name"; + + var model = GetInitialModel(); + + Assert.AreEqual("Classic", model.Sku.Name); + + model.Patch.Set(propertyNameSpan, expectedValue); + + Assert.AreEqual(expectedValue, model.Patch.GetString(propertyNameSpan)); + Assert.AreEqual(expectedValue, model.Sku.Patch.GetString("$.name"u8)); + Assert.AreEqual("[]", model.Patch.ToString()); + Assert.AreEqual( + "[{\"op\":\"replace\",\"path\":\"/name\",\"value\":\"new-sku-name\"}]", + model.Sku.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"new-sku-name\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual(expectedValue, model2.Patch.GetString(propertyNameSpan)); + Assert.AreEqual(expectedValue, model2.Sku.Patch.GetString("$.name"u8)); + Assert.AreEqual(expectedValue, model2.Sku.Name); + Assert.AreEqual("[]", model2.Patch.ToString()); + Assert.AreEqual("[]", model2.Sku.Patch.ToString()); + + AssertCommon(model, model2, "sku"); + } + + [Test] + public void AddNewNestedProperty() + { + ReadOnlySpan propertyNameSpan = "$.sku.something"u8; + string expectedValue = "something-value"; + + var model = GetInitialModel(); + + model.Patch.Set(propertyNameSpan, expectedValue); + + Assert.AreEqual(expectedValue, model.Patch.GetString(propertyNameSpan)); + Assert.AreEqual(expectedValue, model.Sku.Patch.GetString("$.something"u8)); + Assert.AreEqual("[]", model.Patch.ToString()); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/something\",\"value\":\"something-value\"}]", + model.Sku.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\",\"something\":\"something-value\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + + Assert.AreEqual(expectedValue, model2.Patch.GetString(propertyNameSpan)); + Assert.AreEqual("Classic", model2.Sku.Name); + Assert.AreEqual(expectedValue, model2.Sku.Patch.GetString("$.something"u8)); + Assert.AreEqual("[]", model2.Patch.ToString()); + Assert.AreEqual("[]", model2.Sku.Patch.ToString()); + + AssertCommon(model, model2, "sku"); + } + + [Test] + public void AddComplexPropertyAsJson() + { + ReadOnlySpan expectedValue = "{\"x\":{\"y\":123}}"u8; + var pointer = "$.foobar"u8; + var model = GetInitialModel(); + + model.Patch.Set(pointer, expectedValue); + + CollectionAssert.AreEqual(expectedValue.ToArray(), model.Patch.GetJson(pointer).ToArray()); + Assert.AreEqual("{\"y\":123}"u8.ToArray(), model.Patch.GetJson("$.foobar.x"u8).ToArray()); + Assert.AreEqual(123, model.Patch.GetInt32("$.foobar.x.y"u8)); + Assert.AreEqual(123, model.Patch.GetNullableValue("$.foobar.x.y"u8)); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":{\"x\":{\"y\":123}}}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":{\"x\":{\"y\":123}}}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + + CollectionAssert.AreEqual(expectedValue.ToArray(), model2.Patch.GetJson(pointer).ToArray()); + Assert.AreEqual("{\"y\":123}"u8.ToArray(), model2.Patch.GetJson("$.foobar.x"u8).ToArray()); + Assert.AreEqual(123, model2.Patch.GetInt32("$.foobar.x.y"u8)); + Assert.AreEqual(123, model2.Patch.GetNullableValue("$.foobar.x.y"u8)); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void AddComplexPropertyAsAnonModel() + { + ReadOnlySpan expectedValue = "{\"x\":{\"y\":123}}"u8; + var pointer = "$.foobar"u8; + var model = GetInitialModel(); + + model.Patch.Set(pointer, JsonSerializer.SerializeToUtf8Bytes(new + { + x = new + { + y = 123 + } + })); + + CollectionAssert.AreEqual(expectedValue.ToArray(), model.Patch.GetJson(pointer).ToArray()); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":{\"x\":{\"y\":123}}}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":{\"x\":{\"y\":123}}}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + CollectionAssert.AreEqual(expectedValue.ToArray(), model2.Patch.GetJson(pointer).ToArray()); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2); + } + + [Test] + public void ReplaceExistingComplex() + { + ReadOnlySpan expectedValue = "{\"name\":\"replaced-name\",\"foo\":123}"u8; + var pointer = "$.sku"u8; + var model = GetInitialModel(); + + model.Patch.Set(pointer, expectedValue); + + CollectionAssert.AreEqual(expectedValue.ToArray(), model.Patch.GetJson(pointer).ToArray()); + Assert.AreEqual("[]", model.Patch.ToString()); + Assert.AreEqual( + "[{\"op\":\"replace\",\"path\":\"/\",\"value\":{\"name\":\"replaced-name\",\"foo\":123}}]", + model.Sku.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"replaced-name\",\"foo\":123},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + Assert.AreEqual("replaced-name", model2.Sku.Name); + Assert.AreEqual(123, model2.Sku.Patch.GetInt32("$.foo"u8)); + Assert.AreEqual("[]", model2.Patch.ToString()); + Assert.AreEqual("[]", model2.Sku.Patch.ToString()); + + model2.Patch.Set("$.sku.foo"u8, 999); + Assert.AreEqual("[{\"op\":\"replace\",\"path\":\"/foo\",\"value\":999}]", model2.Sku.Patch.ToString()); + + AssertCommon(model, model2, "sku"); + } + + [Test] + public void SetClrAfterJsonProperty() + { + var model = GetInitialModel(); + var pointer = "$.location"u8; + + model.Patch.Set(pointer, "new-location"); + + Assert.AreEqual("new-location", model.Patch.GetString(pointer)); + Assert.AreEqual( + "[{\"op\":\"replace\",\"path\":\"/location\",\"value\":\"new-location\"}]", + model.Patch.ToString()); + + //setting the location property directly does not override the patch + //if we want this we need to tie property setters to the patch which is very tricky + model.Location = "another-location"; + Assert.AreEqual("new-location", model.Patch.GetString(pointer)); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"location\":\"new-location\"}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + + Assert.AreEqual("new-location", model2.Location); + Assert.AreEqual("[]", model2.Patch.ToString()); + } + + [Test] + public void AddNewObjectThenANewPropertyToThatObject() + { + var model = GetInitialModel(); + + model.Patch.Set("$.foobar"u8, "{\"x\":\"value\"}"u8); + + Assert.AreEqual("{\"x\":\"value\"}"u8.ToArray(), model.Patch.GetJson("$.foobar"u8).ToArray()); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":{\"x\":\"value\"}}]", + model.Patch.ToString()); + + model.Patch.Set("$.foobar.name"u8, "vmName1"); + + Assert.AreEqual("vmName1", model.Patch.GetString("$.foobar.name"u8)); + Assert.AreEqual( + "[{\"op\":\"add\",\"path\":\"/foobar\",\"value\":{\"x\":\"value\",\"name\":\"vmName1\"}}]", + model.Patch.ToString()); + + var data = ModelReaderWriter.Write(model); + Assert.AreEqual( + "{\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\",\"sku\":{\"name\":\"Classic\"},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\",\"foobar\":{\"x\":\"value\",\"name\":\"vmName1\"}}", + data.ToString()); + + var model2 = GetRoundTripModel(data); + + Assert.AreEqual("{\"x\":\"value\",\"name\":\"vmName1\"}"u8.ToArray(), model2.Patch.GetJson("$.foobar"u8).ToArray()); + Assert.AreEqual("vmName1", model2.Patch.GetString("$.foobar.name"u8)); + Assert.AreEqual("[]", model2.Patch.ToString()); + + AssertCommon(model, model2); + } + + private AvailabilitySetData GetInitialModel() + { + var model = ModelReaderWriter.Read(BinaryData.FromString(JsonPayload)); + Assert.IsNotNull(model); + return model!; + } + + private static AvailabilitySetData GetRoundTripModel(BinaryData data) + { + var model2 = ModelReaderWriter.Read(data); + Assert.IsNotNull(model2); + return model2!; + } + + private static void AssertCommon(AvailabilitySetData model, AvailabilitySetData model2, params string[] skips) + { + Assert.AreEqual("extraSku", model2.Patch.GetString("$.extraSku"u8)); + Assert.AreEqual("extraRoot", model2.Patch.GetString("$.extraRoot"u8)); + CompareAvailabilitySetData(model, model2, "J", skips); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.cs index 36159b5b1e92..4fabae7587bd 100644 --- a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.cs +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/AvailabilitySetDataTests.cs @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using NUnit.Framework; -using System.Collections.Generic; -using System.IO; +using System.ClientModel.Primitives; using System.ClientModel.Tests.Client; using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; -using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; #if SOURCE_GENERATOR using System.ClientModel.SourceGeneration.Tests; #endif namespace System.ClientModel.Tests.ModelReaderWriterTests.Models { - internal class AvailabilitySetDataTests : ModelJsonTests + internal partial class AvailabilitySetDataTests : ModelJsonTests { protected override string WirePayload => File.ReadAllText(TestData.GetLocation("AvailabilitySetData/AvailabilitySetDataWireFormat.json")).TrimEnd(); @@ -31,12 +31,7 @@ protected override string GetExpectedResult(string format) if (format == "J") expectedSerializedString += "\"name\":\"testAS-3375\",\"id\":\"/subscriptions/e37510d7-33b6-4676-886f-ee75bcc01871/resourceGroups/testRG-6497/providers/Microsoft.Compute/availabilitySets/testAS-3375\",\"type\":\"Microsoft.Compute/availabilitySets\","; expectedSerializedString += "\"sku\":{\"name\":\"Classic\""; - //if (!ignoreAdditionalProperties) - // expectedSerializedString += ",\"extraSku\":\"extraSku\""; - expectedSerializedString += "},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3}"; - //if (!ignoreAdditionalProperties) - // expectedSerializedString += ",\"extraRoot\":\"extraRoot\""; - expectedSerializedString += "}"; + expectedSerializedString += "},\"tags\":{\"key\":\"value\"},\"location\":\"eastus\",\"properties\":{\"platformUpdateDomainCount\":5,\"platformFaultDomainCount\":3},\"extraSku\":\"extraSku\",\"extraRoot\":\"extraRoot\"}"; return expectedSerializedString; ; } @@ -57,7 +52,7 @@ protected override void VerifyModel(AvailabilitySetData model, string format) protected override void CompareModels(AvailabilitySetData model, AvailabilitySetData model2, string format) => CompareAvailabilitySetData(model, model2, format); - internal static void CompareAvailabilitySetData(AvailabilitySetData model, AvailabilitySetData model2, string format) + internal static void CompareAvailabilitySetData(AvailabilitySetData model, AvailabilitySetData model2, string format, params string[] propertySkips) { if (model is null) { @@ -65,15 +60,39 @@ internal static void CompareAvailabilitySetData(AvailabilitySetData model, Avail return; } - Assert.AreEqual(format == "W" ? null : model.Id, model2.Id); - Assert.AreEqual(model.Location, model2.Location); - Assert.AreEqual(format == "W" ? null : model.Name, model2.Name); - Assert.AreEqual(model.PlatformFaultDomainCount, model2.PlatformFaultDomainCount); - Assert.AreEqual(model.PlatformUpdateDomainCount, model2.PlatformUpdateDomainCount); - if (format == "J") + HashSet skips = new HashSet(propertySkips); + if (!skips.Contains("id")) + { + Assert.AreEqual(format == "W" ? null : model.Id, model2.Id); + } + if (!skips.Contains("name")) + { + Assert.AreEqual(format == "W" ? null : model.Name, model2.Name); + } + if (format == "J" && !skips.Contains("resourceType")) + { Assert.AreEqual(model.ResourceType, model2.ResourceType); - CollectionAssert.AreEquivalent(model.Tags, model2.Tags); - Assert.AreEqual(model.Sku.Name, model2.Sku.Name); + } + if (!skips.Contains("location")) + { + Assert.AreEqual(model.Location, model2.Location); + } + if (!skips.Contains("platformFaultDomainCount")) + { + Assert.AreEqual(model.PlatformFaultDomainCount, model2.PlatformFaultDomainCount); + } + if (!skips.Contains("platformUpdateDomainCount")) + { + Assert.AreEqual(model.PlatformUpdateDomainCount, model2.PlatformUpdateDomainCount); + } + if (!skips.Contains("sku")) + { + Assert.AreEqual(model.Sku.Name, model2.Sku.Name); + } + if (!skips.Contains("tags")) + { + CollectionAssert.AreEquivalent(model.Tags, model2.Tags); + } } } } diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/BaseModels/ModelInstances.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/BaseModels/ModelInstances.cs index 370c53545229..55860e47428c 100644 --- a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/BaseModels/ModelInstances.cs +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/BaseModels/ModelInstances.cs @@ -136,14 +136,14 @@ private static BaseModel CreateModelZ() private static void SetRawData(object obj, Dictionary rawData) { obj.GetType().BaseType! - .GetField("_rawData", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetField("_serializedAdditionalRawData", BindingFlags.Instance | BindingFlags.NonPublic)! .SetValue(obj, rawData); } private static Dictionary GetRawData(object obj) { return (Dictionary)obj.GetType().BaseType! - .GetField("_rawData", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetField("_serializedAdditionalRawData", BindingFlags.Instance | BindingFlags.NonPublic)! .GetValue(obj)!; } } diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs index 0005474ac41e..464f03bfd4d0 100644 --- a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs @@ -106,12 +106,17 @@ private bool AssertFailures(RoundTripStrategy strategy, string format, string internal static Dictionary GetRawData(object model) { Type modelType = model.GetType(); - while (modelType.BaseType != typeof(object) && modelType.BaseType != typeof(ValueType)) + var fieldInfo = modelType.GetField("_serializedAdditionalRawData", BindingFlags.Instance | BindingFlags.NonPublic); + + while (fieldInfo == null && + modelType.BaseType != typeof(object) && + modelType.BaseType != typeof(ValueType)) { modelType = modelType.BaseType!; + fieldInfo = modelType.GetField("_serializedAdditionalRawData", BindingFlags.Instance | BindingFlags.NonPublic); } - var propertyInfo = modelType.GetField("_rawData", BindingFlags.Instance | BindingFlags.NonPublic); - return propertyInfo?.GetValue(model) as Dictionary ?? throw new InvalidOperationException($"unable to get raw data from {model.GetType().Name}"); + + return fieldInfo?.GetValue(model) as Dictionary ?? throw new InvalidOperationException($"unable to get raw data from {model.GetType().Name}"); } [Test] diff --git a/sdk/core/System.ClientModel/tests/System.ClientModel.Tests.csproj b/sdk/core/System.ClientModel/tests/System.ClientModel.Tests.csproj index 4ecf1cdf0f06..4a92e744eed6 100644 --- a/sdk/core/System.ClientModel/tests/System.ClientModel.Tests.csproj +++ b/sdk/core/System.ClientModel/tests/System.ClientModel.Tests.csproj @@ -5,6 +5,7 @@ $(RequiredTargetFrameworks) true enable + $(NoWarn);SCME0001 diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/JsonElementExtensions.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/JsonElementExtensions.cs new file mode 100644 index 000000000000..8b6d489a2fbb --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/JsonElementExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal static class JsonElementExtensions + { + public static BinaryData GetUtf8Bytes(this JsonElement element) + { +#if NET9_0_OR_GREATER + return new BinaryData(JsonMarshal.GetRawUtf8Value(element).ToArray()); +#else + return BinaryData.FromString(element.GetRawText()); +#endif + } + } +} diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DictionaryOfAset.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DictionaryOfAset.cs new file mode 100644 index 000000000000..7b3b8efe8fc7 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DictionaryOfAset.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.ClientModel.Tests.ModelReaderWriterTests; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; + +namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models +{ + public partial class DictionaryOfAset : IJsonModel + { + [Experimental("SCME0001")] + private JsonPatch _patch; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Experimental("SCME0001")] + public ref JsonPatch Patch => ref _patch; + + public IDictionary Items { get; private set; } + + /// + /// Initializes a new instance of DictionaryOfAset. + /// + public DictionaryOfAset() + { + Items = new Dictionary(); + } + + /// + /// Initializes a new instance of DictionaryOfAset. + /// + /// The dictionary of availability set data items. + /// Additional properties for patching. +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + internal DictionaryOfAset(IDictionary items, JsonPatch patch) + { + Items = items ?? new Dictionary(); + _patch = patch; + _patch.SetPropagators(PropagateSet, PropagateGet); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(DictionaryOfAset)} does not support writing '{format}' format."); + } + + Serialize(writer, options); + } + + private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + //TODO: 12% perf hit to call this helper + //writer.WriteDictionaryWithPatch( + // options, + // ref Patch, + // ReadOnlySpan.Empty, + // "$"u8, + // Items, + // static (writer, item, options) => ((IJsonModel)item).Write(writer, options), + // static (item) => item.Patch); + + writer.WriteStartObject(); +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[256]; +#endif + foreach (var item in Items) + { + if (item.Value.Patch.TryGetJson("$"u8, out ReadOnlyMemory patchedJson)) + { + if (!patchedJson.IsEmpty) + { + writer.WritePropertyName(item.Key); + writer.WriteRawValue(patchedJson.Span); + } + continue; + } + +#if NET8_0_OR_GREATER + int bytesWritten = Encoding.UTF8.GetBytes(item.Key.AsSpan(), buffer); + bool patchContains = bytesWritten == 256 + ? Patch.Contains("$"u8, Encoding.UTF8.GetBytes(item.Key)) + : Patch.Contains("$"u8, buffer.Slice(0, bytesWritten)); +#else + bool patchContains = Patch.Contains("$"u8, Encoding.UTF8.GetBytes(item.Key)); +#endif + if (!patchContains) + { + writer.WritePropertyName(item.Key); + ((IJsonModel)item.Value).Write(writer, options); + } + } + + Patch.WriteTo(writer, "$"u8); + writer.WriteEndObject(); + +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + DictionaryOfAset IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(DictionaryOfAset)} does not support reading '{format}' format."); + } + + using JsonDocument document = JsonDocument.ParseValue(ref reader); + return DeserializeDictionaryOfAset(document.RootElement, options, null!); + } + + DictionaryOfAset IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + + switch (format) + { + case "J": + { + using JsonDocument document = JsonDocument.Parse(data); + return DeserializeDictionaryOfAset(document.RootElement, options, data); + } + default: + throw new FormatException($"The model {nameof(DictionaryOfAset)} does not support reading '{options.Format}' format."); + } + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; + + public static DictionaryOfAset DeserializeDictionaryOfAset(JsonElement element, ModelReaderWriterOptions options, BinaryData data) + { + if (element.ValueKind == JsonValueKind.Null) + { + return null!; + } + + if (element.ValueKind != JsonValueKind.Object) + { + throw new FormatException($"Expected object for {nameof(DictionaryOfAset)}, got {element.ValueKind}"); + } + + Dictionary items = new Dictionary(); +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + JsonPatch additionalProperties = new(data is null ? ReadOnlyMemory.Empty : data.ToMemory()); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + foreach (var property in element.EnumerateObject()) + { + if (property.Value.ValueKind == JsonValueKind.Null) + { + items.Add(property.Name, null!); + } + else if (property.Value.ValueKind == JsonValueKind.Object) + { + items.Add(property.Name, AvailabilitySetData.DeserializeAvailabilitySetData(property.Value, options, property.Value.GetUtf8Bytes())); + } + } + + return new DictionaryOfAset(items, additionalProperties); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + + switch (format) + { + case "J": + return ModelReaderWriter.Write(this, options); + default: + throw new FormatException($"The model {nameof(DictionaryOfAset)} does not support writing '{options.Format}' format."); + } + } + +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + private bool PropagateGet(ReadOnlySpan jsonPath, out JsonPatch.EncodedValue value) + { + string key = jsonPath.GetFirstPropertyName(out int i); + value = default; + + if (!Items.TryGetValue(key, out var aset)) + return false; + + return aset.Patch.TryGetEncodedValue([.. "$"u8, .. SerializationHelpers.GetRemainder(jsonPath, i)], out value); + } + + private bool PropagateSet(ReadOnlySpan jsonPath, JsonPatch.EncodedValue value) + { + string key = jsonPath.GetFirstPropertyName(out int i); + + if (!Items.TryGetValue(key, out var aset)) + return false; + + aset.Patch.Set([.. "$"u8, .. SerializationHelpers.GetRemainder(jsonPath, i)], value); + return true; + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } +} diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs index 739b339afff6..0ba14fb96ffc 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/DiscriminatorSet/BaseModel.cs @@ -15,7 +15,7 @@ namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models [PersistableModelProxy(typeof(UnknownBaseModel))] public abstract class BaseModel : IJsonModel { - private Dictionary _rawData; + private Dictionary _serializedAdditionalRawData; public static implicit operator BinaryContent?(BaseModel? baseModel) { @@ -37,7 +37,7 @@ public static explicit operator BaseModel(ClientResult result) protected internal BaseModel(Dictionary? rawData) { - _rawData = rawData ?? new Dictionary(); + _serializedAdditionalRawData = rawData ?? new Dictionary(); } public string? Kind { get; internal set; } @@ -46,7 +46,7 @@ protected internal BaseModel(Dictionary? rawData) protected internal void SerializeRawData(Utf8JsonWriter writer) { //write out the raw data - foreach (var property in _rawData) + foreach (var property in _serializedAdditionalRawData) { writer.WritePropertyName(property.Key); #if NET6_0_OR_GREATER diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ListOfAset.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ListOfAset.cs new file mode 100644 index 000000000000..df0606ec7603 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ListOfAset.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.ClientModel.Tests.ModelReaderWriterTests; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using ClientModel.Tests.ClientShared; + +namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models +{ + public partial class ListOfAset : IJsonModel + { + [Experimental("SCME0001")] + private JsonPatch _patch; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Experimental("SCME0001")] + public ref JsonPatch Patch => ref _patch; + + public List Items { get; private set; } + + /// + /// Initializes a new instance of ListOfAset. + /// + public ListOfAset() + { + Items = new List(); + } + + /// + /// Initializes a new instance of ListOfAset. + /// + /// The list of availability set data items. + /// Additional properties for patching. +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + internal ListOfAset(IList items, JsonPatch patch) + { + Items = items?.ToList() ?? new(); + _patch = patch; +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(ListOfAset)} does not support writing '{format}' format."); + } + + Serialize(writer, options); + } + + private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + if (Patch.Contains("$"u8)) + { + writer.WriteRawValue(Patch.GetJson("$"u8)); + } + else if (OptionalProperty.IsCollectionDefined(Items)) + { + writer.WriteStartArray(); + foreach (var item in Items) + { + ((IJsonModel)item).Write(writer, options); + } + Patch.WriteTo(writer, "$"u8); + writer.WriteEndArray(); + } + + Patch.WriteTo(writer); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + ListOfAset IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(ListOfAset)} does not support reading '{format}' format."); + } + + using JsonDocument document = JsonDocument.ParseValue(ref reader); + return DeserializeListOfAset(document.RootElement, options, null!); + } + + ListOfAset IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + + switch (format) + { + case "J": + { + using JsonDocument document = JsonDocument.Parse(data); + return DeserializeListOfAset(document.RootElement, options, data); + } + default: + throw new FormatException($"The model {nameof(ListOfAset)} does not support reading '{options.Format}' format."); + } + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; + + public static ListOfAset DeserializeListOfAset(JsonElement element, ModelReaderWriterOptions options, BinaryData data) + { + if (element.ValueKind == JsonValueKind.Null) + { + return null!; + } + + if (element.ValueKind != JsonValueKind.Array) + { + throw new FormatException($"Expected array for {nameof(ListOfAset)}, got {element.ValueKind}"); + } + + List items = new List(); +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + JsonPatch additionalProperties = new(data is null ? ReadOnlyMemory.Empty : data.ToMemory()); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + foreach (var arrayItem in element.EnumerateArray()) + { + if (arrayItem.ValueKind == JsonValueKind.Null) + { + items.Add(null!); + } + else if (arrayItem.ValueKind == JsonValueKind.Object) + { + items.Add(AvailabilitySetData.DeserializeAvailabilitySetData(arrayItem, options, arrayItem.GetUtf8Bytes())); + } + } + + return new ListOfAset(items, additionalProperties); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + + switch (format) + { + case "J": + return ModelReaderWriter.Write(this, options); + default: + throw new FormatException($"The model {nameof(ListOfAset)} does not support writing '{options.Format}' format."); + } + } + } +} diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelAsStruct.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelAsStruct.cs index 937c0747f6f4..52b92ff9c704 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelAsStruct.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelAsStruct.cs @@ -3,6 +3,7 @@ using ClientModel.Tests.ClientShared; using System.ClientModel.Primitives; +using System.ClientModel.Tests.ModelReaderWriterTests; using System.Collections.Generic; using System.Text.Json; @@ -11,7 +12,7 @@ namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models /// The InputAdditionalPropertiesModelStruct. public readonly partial struct ModelAsStruct : IJsonModel, IJsonModel { - private readonly Dictionary _rawData; + private readonly Dictionary _serializedAdditionalRawData; /// Initializes a new instance of InputAdditionalPropertiesModelStruct. /// @@ -21,7 +22,7 @@ namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models public ModelAsStruct(int id, Dictionary rawData) { Id = id; - _rawData = rawData; + _serializedAdditionalRawData = rawData; } /// Gets the id. @@ -36,9 +37,9 @@ private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) writer.WriteStartObject(); writer.WritePropertyName("id"u8); writer.WriteNumberValue(Id); - if (_rawData is not null && options.Format == "J") + if (_serializedAdditionalRawData is not null && options.Format == "J") { - foreach (var property in _rawData) + foreach (var property in _serializedAdditionalRawData) { writer.WritePropertyName(property.Key); #if NET6_0_OR_GREATER @@ -86,7 +87,7 @@ internal static ModelAsStruct DeserializeInputAdditionalPropertiesModelStruct(Js } if (options.Format == "J") { - rawData.Add(property.Name, BinaryData.FromString(property.Value.GetRawText())); + rawData.Add(property.Name, property.Value.GetUtf8Bytes()); continue; } } diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithPersistableOnly.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithPersistableOnly.cs index 87855a35865f..0f89df25db95 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithPersistableOnly.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithPersistableOnly.cs @@ -3,6 +3,7 @@ using ClientModel.Tests.ClientShared; using System.ClientModel.Primitives; +using System.ClientModel.Tests.ModelReaderWriterTests; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,11 +13,11 @@ namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models { public class ModelWithPersistableOnly : IPersistableModel { - private readonly Dictionary _rawData; + private readonly Dictionary _serializedAdditionalRawData; public ModelWithPersistableOnly() { - _rawData = new Dictionary(); + _serializedAdditionalRawData = new Dictionary(); Fields = new List(); KeyValuePairs = new Dictionary(); } @@ -28,7 +29,7 @@ internal ModelWithPersistableOnly(string? name, int xProperty, int? nullProperty NullProperty = nullProperty; Fields = fields; KeyValuePairs = keyValuePairs; - _rawData = rawData; + _serializedAdditionalRawData = rawData; } private void AssertHasValue(T? value, string name) @@ -109,7 +110,7 @@ private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) private void SerializeRawData(Utf8JsonWriter writer) { //write out the raw data - foreach (var property in _rawData) + foreach (var property in _serializedAdditionalRawData) { writer.WritePropertyName(property.Key); #if NET6_0_OR_GREATER @@ -175,7 +176,7 @@ internal static ModelWithPersistableOnly DeserializeModelX(JsonElement element, if (options.Format == "J") { //this means it's an unknown property we got - rawData.Add(property.Name, BinaryData.FromString(property.Value.GetRawText())); + rawData.Add(property.Name, property.Value.GetUtf8Bytes()); } } diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/SerializationHelpers.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/SerializationHelpers.cs new file mode 100644 index 000000000000..2b57dec93aa9 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/SerializationHelpers.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Buffers.Text; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; + +namespace System.ClientModel.Tests.ModelReaderWriterTests +{ + internal static class SerializationHelpers + { +#if NET8_0_OR_GREATER + private static readonly int s_maxPropertyNameLength = 256; +#endif + + public static ReadOnlySpan SliceToStartOfPropertyName(this ReadOnlySpan jsonPath) + { + ReadOnlySpan local = jsonPath; + if (local.IsEmpty) + return ReadOnlySpan.Empty; + + if (local[0] != (byte)'$') + return ReadOnlySpan.Empty; + + if (local.Length < 3) + return ReadOnlySpan.Empty; + + if (local[1] == (byte)'.') + { + local = local.Slice(2); + } + else + { + if (local[1] != (byte)'[' || local.Length < 4 || (local[2] != (byte)'\'' && local[2] != (byte)'\"')) + return ReadOnlySpan.Empty; + local = local.Slice(3); + } + return local; + } + + public static string GetFirstPropertyName(this ReadOnlySpan jsonPath, out int bytesConsumed) + { + ReadOnlySpan local = SliceToStartOfPropertyName(jsonPath); + + for (bytesConsumed = 0; bytesConsumed < local.Length; bytesConsumed++) + { + byte current = local[bytesConsumed]; + if (current == (byte)'.') + { + break; + } + else if (current == (byte)'\'' || current == (byte)'"') + { + if (bytesConsumed + 1 < local.Length && local[bytesConsumed + 1] == ']') + { + break; + } + } + } + +#if NET6_0_OR_GREATER + string key = Encoding.UTF8.GetString(local.Slice(0, bytesConsumed)); +#else + string key = Encoding.UTF8.GetString(local.Slice(0, bytesConsumed).ToArray()); +#endif + + bytesConsumed += jsonPath.Length - local.Length; + return key; + } + + [Experimental("SCM0001")] + public static void WriteDictionaryWithPatch( + this Utf8JsonWriter writer, + ModelReaderWriterOptions options, + ref JsonPatch patch, + ReadOnlySpan propertyName, + ReadOnlySpan prefix, + IDictionary dictionary, + Action write, + Func? getPatchFromItem) + { + if (!propertyName.IsEmpty) + writer.WritePropertyName(propertyName); + + writer.WriteStartObject(); +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[s_maxPropertyNameLength]; +#endif + foreach (var item in dictionary) + { + if (getPatchFromItem is not null && getPatchFromItem(item.Value).TryGetJson("$"u8, out ReadOnlyMemory patchedJson)) + { + if (!patchedJson.IsEmpty) + { + writer.WritePropertyName(item.Key); + writer.WriteRawValue(patchedJson.Span); + } + continue; + } + +#if NET8_0_OR_GREATER + int bytesWritten = Encoding.UTF8.GetBytes(item.Key.AsSpan(), buffer); + bool patchContains = bytesWritten == s_maxPropertyNameLength + ? patch.Contains(prefix, Encoding.UTF8.GetBytes(item.Key)) + : patch.Contains(prefix, buffer.Slice(0, bytesWritten)); +#else + bool patchContains = patch.Contains(prefix, Encoding.UTF8.GetBytes(item.Key)); +#endif + if (!patchContains) + { + writer.WritePropertyName(item.Key); + write(writer, item.Value, options); + } + } + + patch.WriteTo(writer, prefix); + writer.WriteEndObject(); + } + + public static bool TryGetIndex(ReadOnlySpan indexSlice, out int index, out int bytesConsumed) + { + index = -1; + bytesConsumed = 0; + + if (indexSlice.IsEmpty || indexSlice[0] != (byte)'[') + return false; + + indexSlice = indexSlice.Slice(1); + if (indexSlice.IsEmpty || indexSlice[0] == (byte)'-') + return false; + + int indexEnd = indexSlice.Slice(1).IndexOf((byte)']'); + if (indexEnd < 0) + return false; + + return Utf8Parser.TryParse(indexSlice.Slice(0, indexEnd + 1), out index, out bytesConsumed); + } + + public static ReadOnlySpan GetRemainder(ReadOnlySpan jsonPath, int i) + { + return i >= jsonPath.Length + ? ReadOnlySpan.Empty + : jsonPath[i] == (byte)'.' + ? jsonPath.Slice(i) + : jsonPath.Slice(i + 2); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.Serialization.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.Serialization.cs index 529b31159dd6..e844c91dcf3b 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.Serialization.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.Serialization.cs @@ -3,11 +3,14 @@ // -using ClientModel.Tests.ClientShared; +using System.Buffers.Text; using System.ClientModel.Primitives; using System.ClientModel.Tests.Client.Models.ResourceManager.Resources; +using System.ClientModel.Tests.ModelReaderWriterTests; using System.Collections.Generic; +using System.Text; using System.Text.Json; +using ClientModel.Tests.ClientShared; namespace System.ClientModel.Tests.Client.Models.ResourceManager.Compute { @@ -15,82 +18,128 @@ public partial class AvailabilitySetData : IJsonModel { void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) { - ModelReaderWriterHelper.ValidateFormat(this, options.Format); - + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(AvailabilitySetData)} does not support writing '{format}' format."); + } Serialize(writer, options); } private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. writer.WriteStartObject(); - if (options.Format == "J") + if (options.Format == "J" && !Patch.Contains("$.name"u8)) { writer.WritePropertyName("name"u8); writer.WriteStringValue(Name); } - if (options.Format == "J") + if (options.Format == "J" && !Patch.Contains("$.id"u8)) { writer.WritePropertyName("id"u8); writer.WriteStringValue(Id.ToString()); } - if (options.Format == "J") + if (options.Format == "J" && !Patch.Contains("$.type"u8)) { writer.WritePropertyName("type"u8); writer.WriteStringValue(ResourceType.ToString()); } - if (OptionalProperty.IsDefined(Sku)) + if (OptionalProperty.IsDefined(Sku) && !Patch.Contains("$.sku"u8)) { writer.WritePropertyName("sku"u8); writer.WriteObjectValue(Sku); } - if (OptionalProperty.IsCollectionDefined(Tags)) + if (OptionalProperty.IsCollectionDefined(Tags) && !Patch.Contains("$.tags"u8)) { + //TODO: 12% perf hit to call this helper + //writer.WriteDictionaryWithPatch(options, ref Patch, "tags"u8, "$.tags"u8, Tags, static (writer, value, _) => writer.WriteStringValue(value), null); writer.WritePropertyName("tags"u8); + writer.WriteStartObject(); +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[256]; +#endif foreach (var item in Tags) { - writer.WritePropertyName(item.Key); - writer.WriteStringValue(item.Value); +#if NET8_0_OR_GREATER + int bytesWritten = Encoding.UTF8.GetBytes(item.Key.AsSpan(), buffer); + bool patchContains = bytesWritten == 256 + ? Patch.Contains("$.tags"u8, Encoding.UTF8.GetBytes(item.Key)) + : Patch.Contains("$.tags"u8, buffer.Slice(0, bytesWritten)); +#else + bool patchContains = Patch.Contains("$.tags"u8, Encoding.UTF8.GetBytes(item.Key)); +#endif + if (!patchContains) + { + writer.WritePropertyName(item.Key); + writer.WriteStringValue(item.Value); + } } + + Patch.WriteTo(writer, "$.tags"u8); writer.WriteEndObject(); } - writer.WritePropertyName("location"u8); - writer.WriteStringValue(Location); - writer.WritePropertyName("properties"u8); - writer.WriteStartObject(); - if (OptionalProperty.IsDefined(PlatformUpdateDomainCount)) - { - writer.WritePropertyName("platformUpdateDomainCount"u8); - writer.WriteNumberValue(PlatformUpdateDomainCount.Value); - } - if (OptionalProperty.IsDefined(PlatformFaultDomainCount)) + if (!Patch.Contains("$.location"u8)) { - writer.WritePropertyName("platformFaultDomainCount"u8); - writer.WriteNumberValue(PlatformFaultDomainCount.Value); + writer.WritePropertyName("location"u8); + writer.WriteStringValue(Location); } - if (OptionalProperty.IsCollectionDefined(VirtualMachines)) + if (!Patch.Contains("$.properties"u8)) { - writer.WritePropertyName("virtualMachines"u8); - writer.WriteStartArray(); - foreach (var item in VirtualMachines) + writer.WritePropertyName("properties"u8); + writer.WriteStartObject(); + if (OptionalProperty.IsDefined(PlatformUpdateDomainCount) && !Patch.Contains("$.properties.platformUpdateDomainCount"u8)) { - JsonSerializer.Serialize(writer, item); + writer.WritePropertyName("platformUpdateDomainCount"u8); + writer.WriteNumberValue(PlatformUpdateDomainCount.Value); } - writer.WriteEndArray(); - } - if (OptionalProperty.IsDefined(ProximityPlacementGroup)) - { - writer.WritePropertyName("proximityPlacementGroup"u8); - JsonSerializer.Serialize(writer, ProximityPlacementGroup); + if (OptionalProperty.IsDefined(PlatformFaultDomainCount) && !Patch.Contains("$.properties.platformFaultDomainCount"u8)) + { + writer.WritePropertyName("platformFaultDomainCount"u8); + writer.WriteNumberValue(PlatformFaultDomainCount.Value); + } + if (Patch.Contains("$.properties.virtualMachines"u8)) + { + if (!Patch.IsRemoved("$.properties.virtualMachines"u8)) + { + writer.WritePropertyName("virtualMachines"u8); + writer.WriteRawValue(Patch.GetJson("$.properties.virtualMachines"u8)); + } + } + else if (OptionalProperty.IsCollectionDefined(VirtualMachines)) + { + writer.WritePropertyName("virtualMachines"u8); + writer.WriteStartArray(); + for (int i = 0; i < VirtualMachines.Count; i++) + { + if (VirtualMachines[i].Patch.IsRemoved("$"u8)) + continue; + + ((IJsonModel)VirtualMachines[i]).Write(writer, options); + } + Patch.WriteTo(writer, "$.properties.virtualMachines"u8); + writer.WriteEndArray(); + } + if (OptionalProperty.IsDefined(ProximityPlacementGroup) && !Patch.Contains("$.properties.proximityPlacementGroup"u8)) + { + writer.WritePropertyName("proximityPlacementGroup"u8); + ((IJsonModel)ProximityPlacementGroup).Write(writer, options); + } + + Patch.WriteTo(writer, "$.properties"u8); + + writer.WriteEndObject(); } + + Patch.WriteTo(writer); + writer.WriteEndObject(); - writer.WriteEndObject(); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } - public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement element, ModelReaderWriterOptions options = default) + public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement element, ModelReaderWriterOptions options, BinaryData data) { - options ??= ModelReaderWriterHelper.WireOptions; - if (element.ValueKind == JsonValueKind.Null) { return null; @@ -107,6 +156,9 @@ public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement ele OptionalProperty> virtualMachines = default; OptionalProperty proximityPlacementGroup = default; OptionalProperty> statuses = default; +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + JsonPatch jsonPatch = new(data is null ? ReadOnlyMemory.Empty : data.ToMemory()); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. foreach (var property in element.EnumerateObject()) { if (property.NameEquals("sku"u8)) @@ -115,7 +167,7 @@ public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement ele { continue; } - sku = ComputeSku.DeserializeComputeSku(property.Value, options); + sku = ComputeSku.DeserializeComputeSku(property.Value, options, property.Value.GetUtf8Bytes()); continue; } if (property.NameEquals("tags"u8)) @@ -158,7 +210,7 @@ public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement ele { continue; } - systemData = JsonSerializer.Deserialize(property.Value.GetRawText()); + systemData = ModelReaderWriter.Read(property.Value.GetUtf8Bytes(), options); continue; } if (property.NameEquals("properties"u8)) @@ -197,7 +249,7 @@ public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement ele List array = new List(); foreach (var item in property0.Value.EnumerateArray()) { - array.Add(JsonSerializer.Deserialize(item.GetRawText())); + array.Add(ModelReaderWriter.Read(item.GetUtf8Bytes(), options)); } virtualMachines = array; continue; @@ -208,7 +260,7 @@ public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement ele { continue; } - proximityPlacementGroup = JsonSerializer.Deserialize(property0.Value.GetRawText()); + proximityPlacementGroup = ModelReaderWriter.Read(property0.Value.GetUtf8Bytes(), options); continue; } if (property0.NameEquals("statuses"u8)) @@ -225,19 +277,44 @@ public static AvailabilitySetData DeserializeAvailabilitySetData(JsonElement ele statuses = array; continue; } + jsonPatch.Set([.. "$.properties."u8, .. Encoding.UTF8.GetBytes(property0.Name)], property0.Value.GetUtf8Bytes()); + } continue; } + jsonPatch.Set([.. "$."u8, .. Encoding.UTF8.GetBytes(property.Name)], property.Value.GetUtf8Bytes()); } - return new AvailabilitySetData(id, name, type, systemData.Value, OptionalProperty.ToDictionary(tags), location, sku.Value, OptionalProperty.ToNullable(platformUpdateDomainCount), OptionalProperty.ToNullable(platformFaultDomainCount), OptionalProperty.ToList(virtualMachines), proximityPlacementGroup, OptionalProperty.ToList(statuses)); + + return new AvailabilitySetData( + id, + name, + type, + systemData.Value, + OptionalProperty.ToDictionary(tags), + location, + sku.Value, + OptionalProperty.ToNullable(platformUpdateDomainCount), + OptionalProperty.ToNullable(platformFaultDomainCount), + OptionalProperty.ToList(virtualMachines), + proximityPlacementGroup, + OptionalProperty.ToList(statuses), + jsonPatch); } AvailabilitySetData IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) { - ModelReaderWriterHelper.ValidateFormat(this, options.Format); + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - using var doc = JsonDocument.Parse(data); - return DeserializeAvailabilitySetData(doc.RootElement, options); + switch (format) + { + case "J": + { + using JsonDocument document = JsonDocument.Parse(data); + return DeserializeAvailabilitySetData(document.RootElement, options, data); + } + default: + throw new FormatException($"The model {nameof(AvailabilitySetData)} does not support reading '{options.Format}' format."); + } } private struct AvailabilitySetDataProperties @@ -258,19 +335,83 @@ private struct AvailabilitySetDataProperties AvailabilitySetData IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { - ModelReaderWriterHelper.ValidateFormat(this, options.Format); + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(AvailabilitySetData)} does not support reading '{format}' format."); + } - using var doc = JsonDocument.ParseValue(ref reader); - return DeserializeAvailabilitySetData(doc.RootElement, options); + using JsonDocument document = JsonDocument.ParseValue(ref reader); + return DeserializeAvailabilitySetData(document.RootElement, options, null); } BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) { - ModelReaderWriterHelper.ValidateFormat(this, options.Format); + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - return ModelReaderWriter.Write(this, options); + switch (format) + { + case "J": + return ModelReaderWriter.Write(this, options, TestClientModelReaderWriterContext.Default); + default: + throw new FormatException($"The model {nameof(AvailabilitySetData)} does not support writing '{options.Format}' format."); + } } string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; + +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + private bool PropagateSet(ReadOnlySpan jsonPath, JsonPatch.EncodedValue value) + { + ReadOnlySpan local = jsonPath.SliceToStartOfPropertyName(); + + if (local.StartsWith("sku"u8)) + { + Sku.Patch.Set([.. "$"u8, .. local.Slice("sku"u8.Length)], value); + return true; + } + else if (local.StartsWith("properties.virtualMachines"u8)) + { + int propertyLength = "properties.virtualMachines"u8.Length; + ReadOnlySpan indexSlice = local.Slice(propertyLength); + if (!SerializationHelpers.TryGetIndex(indexSlice, out int index, out int bytesConsumed)) + return false; + + if (VirtualMachines.Count > index) + { + VirtualMachines[index].Patch.Set([.. "$"u8, .. indexSlice.Slice(bytesConsumed + 2)], value); + return true; + } + } + + return false; + } + + private bool PropagateGet(ReadOnlySpan jsonPath, out JsonPatch.EncodedValue value) + { + ReadOnlySpan local = jsonPath.SliceToStartOfPropertyName(); + value = default; + + if (local.StartsWith("sku"u8)) + { + return Sku.Patch.TryGetEncodedValue([.. "$"u8, .. local.Slice("sku"u8.Length)], out value); + } + else if (local.StartsWith("properties.virtualMachines"u8)) + { + int propertyLength = "properties.virtualMachines"u8.Length; + ReadOnlySpan indexSlice = local.Slice(propertyLength); + if (!SerializationHelpers.TryGetIndex(indexSlice, out int index, out int bytesConsumed)) + return false; + + if (VirtualMachines.Count > index) + { + return VirtualMachines[index].Patch.TryGetEncodedValue([.. "$"u8, .. indexSlice.Slice(bytesConsumed + 2)], out value); + } + } + + return false; + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } } diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.cs index fb71b03c936b..96bf89f7c5b8 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/AvailabilitySetData.cs @@ -3,10 +3,11 @@ // -using ClientModel.Tests.ClientShared; +using System.ClientModel.Primitives; using System.ClientModel.Tests.Client.Models.ResourceManager.Resources; using System.Collections.Generic; using System.Text.Json; +using ClientModel.Tests.ClientShared; namespace System.ClientModel.Tests.Client.Models.ResourceManager.Compute { @@ -33,7 +34,7 @@ public static explicit operator AvailabilitySetData(ClientResult result) if (result is null) throw new ArgumentNullException(nameof(result)); using JsonDocument jsonDocument = JsonDocument.Parse(result.GetRawResponse().Content); - return DeserializeAvailabilitySetData(jsonDocument.RootElement, ModelReaderWriterHelper.WireOptions); + return DeserializeAvailabilitySetData(jsonDocument.RootElement, ModelReaderWriterHelper.WireOptions, result.GetRawResponse().Content); } /// Initializes a new instance of AvailabilitySetData. @@ -57,7 +58,8 @@ public AvailabilitySetData(string location) : base(location) /// A list of references to all virtual machines in the availability set. /// Specifies information about the proximity placement group that the availability set should be assigned to. Minimum api-version: 2018-04-01. /// The resource status information. - internal AvailabilitySetData(string id, string name, string resourceType, SystemData systemData, IDictionary tags, string location, ComputeSku sku, int? platformUpdateDomainCount, int? platformFaultDomainCount, IList virtualMachines, WritableSubResource proximityPlacementGroup, IReadOnlyList statuses) : base(id, name, resourceType, systemData, tags, location) +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + internal AvailabilitySetData(string id, string name, string resourceType, SystemData systemData, IDictionary tags, string location, ComputeSku sku, int? platformUpdateDomainCount, int? platformFaultDomainCount, IList virtualMachines, WritableSubResource proximityPlacementGroup, IReadOnlyList statuses, in JsonPatch jsonPatch) : base(id, name, resourceType, systemData, tags, location, jsonPatch) { Sku = sku; PlatformUpdateDomainCount = platformUpdateDomainCount; @@ -65,6 +67,8 @@ internal AvailabilitySetData(string id, string name, string resourceType, System VirtualMachines = virtualMachines; ProximityPlacementGroup = proximityPlacementGroup; Statuses = statuses; + Patch.SetPropagators(PropagateSet, PropagateGet); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } /// Sku of the availability set, only name is required to be set. See AvailabilitySetSkuTypes for possible set of values. Use 'Aligned' for virtual machines with managed disks and 'Classic' for virtual machines with unmanaged disks. Default value is 'Classic'. diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.Serialization.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.Serialization.cs index 2d8dcd8a6ea2..b6fe85151ad7 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.Serialization.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.Serialization.cs @@ -3,38 +3,60 @@ // -using ClientModel.Tests.ClientShared; using System.ClientModel.Primitives; +using System.ClientModel.Tests.ModelReaderWriterTests; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text; using System.Text.Json; +using ClientModel.Tests.ClientShared; namespace System.ClientModel.Tests.Client.Models.ResourceManager.Compute { public partial class ComputeSku : IJsonModel { + [Experimental("SCME0001")] + private JsonPatch _patch = new(); + [EditorBrowsable(EditorBrowsableState.Never)] + [Experimental("SCME0001")] + public ref JsonPatch Patch => ref _patch; + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) => Serialize(writer, options); private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + if (Patch.Contains("$"u8)) + { + writer.WriteRawValue(Patch.GetJson("$"u8)); + return; + } + writer.WriteStartObject(); - if (OptionalProperty.IsDefined(Name)) + if (OptionalProperty.IsDefined(Name) && !Patch.Contains("$.name"u8)) { writer.WritePropertyName("name"u8); writer.WriteStringValue(Name); } - if (OptionalProperty.IsDefined(Tier)) + if (OptionalProperty.IsDefined(Tier) && !Patch.Contains("$.tier"u8)) { writer.WritePropertyName("tier"u8); writer.WriteStringValue(Tier); } - if (OptionalProperty.IsDefined(Capacity)) + if (OptionalProperty.IsDefined(Capacity) && !Patch.Contains("$.capacity"u8)) { writer.WritePropertyName("capacity"u8); writer.WriteNumberValue(Capacity.Value); } + + Patch.WriteTo(writer); + writer.WriteEndObject(); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } - internal static ComputeSku DeserializeComputeSku(JsonElement element, ModelReaderWriterOptions options = default) + internal static ComputeSku DeserializeComputeSku(JsonElement element, ModelReaderWriterOptions options, BinaryData data) { options ??= ModelReaderWriterHelper.WireOptions; @@ -45,6 +67,9 @@ internal static ComputeSku DeserializeComputeSku(JsonElement element, ModelReade OptionalProperty name = default; OptionalProperty tier = default; OptionalProperty capacity = default; +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + JsonPatch jsonPatch = new(data is null ? ReadOnlyMemory.Empty : data.ToMemory()); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. foreach (var property in element.EnumerateObject()) { if (property.NameEquals("name"u8)) @@ -66,8 +91,11 @@ internal static ComputeSku DeserializeComputeSku(JsonElement element, ModelReade capacity = property.Value.GetInt64(); continue; } + jsonPatch.Set([.. "$."u8, .. Encoding.UTF8.GetBytes(property.Name)], property.Value.GetUtf8Bytes()); } - return new ComputeSku(name.Value, tier.Value, OptionalProperty.ToNullable(capacity)); + var model = new ComputeSku(name.Value, tier.Value, OptionalProperty.ToNullable(capacity), jsonPatch); + + return model; } private struct ComputeSkuProperties @@ -80,7 +108,7 @@ private struct ComputeSkuProperties ComputeSku IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { using var doc = JsonDocument.ParseValue(ref reader); - return DeserializeComputeSku(doc.RootElement, options); + return DeserializeComputeSku(doc.RootElement, options, null); } private static void SetProperty(ReadOnlySpan propertyName, ref ComputeSkuProperties properties, ref Utf8JsonReader reader, ModelReaderWriterOptions options) @@ -112,7 +140,7 @@ private static void SetProperty(ReadOnlySpan propertyName, ref ComputeSkuP ComputeSku IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) { using var doc = JsonDocument.Parse(data); - return DeserializeComputeSku(doc.RootElement, options); + return DeserializeComputeSku(doc.RootElement, options, data); } BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.cs index 65749933edf0..05add9db808e 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ComputeSku.cs @@ -3,6 +3,8 @@ // +using System.ClientModel.Primitives; + namespace System.ClientModel.Tests.Client.Models.ResourceManager.Compute { /// Describes a virtual machine scale set sku. NOTE: If the new VM SKU is not supported on the hardware the scale set is currently on, you need to deallocate the VMs in the scale set before you modify the SKU name. @@ -17,11 +19,14 @@ public ComputeSku() /// The sku name. /// Specifies the tier of virtual machines in a scale set.<br /><br /> Possible Values:<br /><br /> **Standard**<br /><br /> **Basic**. /// Specifies the number of virtual machines in the scale set. - internal ComputeSku(string name, string tier, long? capacity) +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + internal ComputeSku(string name, string tier, long? capacity, in JsonPatch jsonPatch) { Name = name; Tier = tier; Capacity = capacity; + _patch = jsonPatch; +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } /// The sku name. diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ResourceData.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ResourceData.cs index d35623324934..8f70ea439b23 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ResourceData.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/ResourceData.cs @@ -1,11 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + namespace System.ClientModel.Tests.Client.Models.ResourceManager { /// Common fields that are returned in the response for all Azure Resource Manager resources. public abstract partial class ResourceData { + [Experimental("SCME0001")] + private JsonPatch _patch; + [EditorBrowsable(EditorBrowsableState.Never)] + [Experimental("SCME0001")] + public ref JsonPatch Patch => ref _patch; + /// Initializes a new instance of Resource. protected ResourceData() { @@ -16,12 +26,15 @@ protected ResourceData() /// The name of the resource. /// The type of the resource. E.g. "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts". /// Azure Resource Manager metadata containing createdBy and modifiedBy information. - protected ResourceData(string id, string name, string resourceType, SystemData systemData) +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + protected ResourceData(string id, string name, string resourceType, SystemData systemData, in JsonPatch jsonPatch) { Id = id; Name = name; ResourceType = resourceType; SystemData = systemData; + _patch = jsonPatch; +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } /// Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}. diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/SourceGenerationContext.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/SourceGenerationContext.cs index a3bfeaba673b..0f5ad32cbfb9 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/SourceGenerationContext.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/SourceGenerationContext.cs @@ -3,6 +3,7 @@ using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; using System.ClientModel.Tests.Client.Models.ResourceManager.Resources; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace System.ClientModel.Tests.Client.Models @@ -10,6 +11,7 @@ namespace System.ClientModel.Tests.Client.Models #if NET6_0_OR_GREATER [JsonSerializable(typeof(ResourceProviderData))] [JsonSerializable(typeof(AvailabilitySetData))] + [Experimental("SCM0001")] public partial class SourceGenerationContext : JsonSerializerContext { } diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/TrackedResourceData.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/TrackedResourceData.cs index 365ff0f5a1bf..2865323b8eeb 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/TrackedResourceData.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/TrackedResourceData.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.ClientModel.Primitives; using System.Collections.Generic; using ClientModel.Tests.ClientShared; @@ -29,7 +30,9 @@ protected TrackedResourceData(string location) /// Azure Resource Manager metadata containing createdBy and modifiedBy information. /// Resource tags. /// The geo-location where the resource lives. - protected TrackedResourceData(string id, string name, string resourceType, SystemData systemData, IDictionary tags, string location) : base(id, name, resourceType, systemData) +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + protected TrackedResourceData(string id, string name, string resourceType, SystemData systemData, IDictionary tags, string location, in JsonPatch jsonPatch) : base(id, name, resourceType, systemData, jsonPatch) +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. { Tags = tags; Location = location; diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.Serialization.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.Serialization.cs index edea5bb6144d..96399a171cac 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.Serialization.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.Serialization.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using ClientModel.Tests.ClientShared; using System.ClientModel.Primitives; +using System.ClientModel.Tests.ModelReaderWriterTests; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using ClientModel.Tests.ClientShared; namespace System.ClientModel.Tests.Client.Models.ResourceManager.Resources { @@ -22,18 +24,29 @@ public partial class WritableSubResource : IJsonModel /// Input Json writer. private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. if (writer is null) { throw new ArgumentNullException(nameof(writer)); } + if (Patch.Contains("$"u8)) + { + writer.WriteRawValue(Patch.GetJson("$"u8)); + return; + } + writer.WriteStartObject(); - if (OptionalProperty.IsDefined(Id)) + if (OptionalProperty.IsDefined(Id) && !Patch.Contains("$.id"u8)) { writer.WritePropertyName("id"); writer.WriteStringValue(Id); } + + Patch.WriteTo(writer); + writer.WriteEndObject(); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } /// @@ -41,11 +54,14 @@ private void Serialize(Utf8JsonWriter writer, ModelReaderWriterOptions options) /// /// The JSON element to be deserialized. /// Deserialized WritableSubResource object. - internal static WritableSubResource DeserializeWritableSubResource(JsonElement element, ModelReaderWriterOptions? options = default) + internal static WritableSubResource DeserializeWritableSubResource(JsonElement element, ModelReaderWriterOptions? options, BinaryData? data) { options ??= ModelReaderWriterHelper.WireOptions; string? id = default; +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + JsonPatch jsonPatch = new(data is null ? ReadOnlyMemory.Empty : data.ToMemory()); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. foreach (var property in element.EnumerateObject()) { if (property.NameEquals("id")) @@ -57,8 +73,12 @@ internal static WritableSubResource DeserializeWritableSubResource(JsonElement e id = property.Value.GetString(); continue; } + + jsonPatch.Set([.. "$."u8, .. Encoding.UTF8.GetBytes(property.Name)], property.Value.GetUtf8Bytes()); } - return new WritableSubResource(id); + var model = new WritableSubResource(id, jsonPatch); + + return model; } private struct WritableSubResourceProperties @@ -69,7 +89,7 @@ private struct WritableSubResourceProperties WritableSubResource IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { using var doc = JsonDocument.ParseValue(ref reader); - return DeserializeWritableSubResource(doc.RootElement, options); + return DeserializeWritableSubResource(doc.RootElement, options, null); } private static void SetProperty(ReadOnlySpan propertyName, ref WritableSubResourceProperties properties, ref Utf8JsonReader reader, ModelReaderWriterOptions options) @@ -89,7 +109,7 @@ private static void SetProperty(ReadOnlySpan propertyName, ref WritableSub WritableSubResource IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) { using var doc = JsonDocument.Parse(data); - return DeserializeWritableSubResource(doc.RootElement, options); + return DeserializeWritableSubResource(doc.RootElement, options, data); } internal partial class WritableSubResourceConverter : JsonConverter @@ -101,7 +121,7 @@ public override void Write(Utf8JsonWriter writer, WritableSubResource model, Jso public override WritableSubResource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var document = JsonDocument.ParseValue(ref reader); - return DeserializeWritableSubResource(document.RootElement, ModelReaderWriterHelper.WireOptions); + return DeserializeWritableSubResource(document.RootElement, ModelReaderWriterHelper.WireOptions, null); } } diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.cs index 1c9d37256537..dc14904d32bd 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/ServiceModels/WritableSubResource.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + namespace System.ClientModel.Tests.Client.Models.ResourceManager.Resources { /// @@ -8,6 +12,12 @@ namespace System.ClientModel.Tests.Client.Models.ResourceManager.Resources /// public partial class WritableSubResource { + [Experimental("SCME0001")] + private JsonPatch _patch = new(); + [EditorBrowsable(EditorBrowsableState.Never)] + [Experimental("SCME0001")] + public ref JsonPatch Patch => ref _patch; + /// /// Initializes an empty instance of for mocking. /// @@ -17,9 +27,12 @@ public WritableSubResource() /// Initializes a new instance of . /// ARM resource Id. - protected internal WritableSubResource(string? id) +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + protected internal WritableSubResource(string? id, in JsonPatch jsonPatch) { Id = id; + _patch = jsonPatch; +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } /// diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs index c08a4173701f..206646c9108a 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs @@ -25,6 +25,8 @@ public class TestClientModelReaderWriterContext : ModelReaderWriterContext private UnknownBaseModel_Builder? _unknownBaseModel_Builder; private ModelY_Builder? _modelY_Builder; private ExperimentalModel_Builder? _experimentalModel_Builder; + private ListOfAset_Builder? _listOfAset_Builder; + private DictionaryOfAset_Builder? _dictionaryOfAset_Builder; protected override bool TryGetTypeBuilderCore(Type type, out ModelReaderWriterTypeBuilder? builder) { @@ -41,11 +43,27 @@ protected override bool TryGetTypeBuilderCore(Type type, out ModelReaderWriterTy #pragma warning disable TEST001 Type t when t == typeof(ExperimentalModel) => _experimentalModel_Builder ??= new ExperimentalModel_Builder(), #pragma warning restore TEST001 + Type t when t == typeof(ListOfAset) => _listOfAset_Builder ??= new(), + Type t when t == typeof(DictionaryOfAset) => _dictionaryOfAset_Builder ??= new(), _ => null }; return builder is not null; } + private class DictionaryOfAset_Builder : ModelReaderWriterTypeBuilder + { + protected override Type BuilderType => typeof(DictionaryOfAset); + + protected override object CreateInstance() => new DictionaryOfAset(); + } + + private class ListOfAset_Builder : ModelReaderWriterTypeBuilder + { + protected override Type BuilderType => typeof(ListOfAset); + + protected override object CreateInstance() => new ListOfAset(); + } + private class ModelY_Builder : ModelReaderWriterTypeBuilder { protected override Type BuilderType => typeof(ModelY); diff --git a/sdk/core/System.ClientModel/tests/client/NetStandardPolyfil/ExperimentalAttribute.cs b/sdk/core/System.ClientModel/tests/client/NetStandardPolyfil/ExperimentalAttribute.cs new file mode 100644 index 000000000000..5d7d2786f08f --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/NetStandardPolyfil/ExperimentalAttribute.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if !NET8_0_OR_GREATER +#nullable enable + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that an API is experimental and it may change in the future. +/// +/// +/// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental +/// feature is used. Authors can use this attribute to ship preview features in their assemblies. +/// +[AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, Inherited = false)] +internal sealed class ExperimentalAttribute : Attribute +{ + /// + /// Initializes a new instance of the class, specifying the ID that the compiler will use + /// when reporting a use of the API the attribute applies to. + /// + /// The ID that the compiler will use when reporting a use of the API the attribute applies to. + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + /// + /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. + /// + /// The unique diagnostic ID. + /// + /// The diagnostic ID is shown in build output for warnings and errors. + /// This property represents the unique ID that can be used to suppress the warnings or errors, if needed. + /// + public string DiagnosticId { get; } + + /// + /// Gets or sets the URL for corresponding documentation. + /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. + /// + /// The format string that represents a URL to corresponding documentation. + /// An example format string is https://contoso.com/obsoletion-warnings/{0}. + public string? UrlFormat { get; set; } +} + +#endif // !NET8_0_OR_LATER diff --git a/sdk/core/System.ClientModel/tests/internal.perf/InsertItemsBenchmarks.cs b/sdk/core/System.ClientModel/tests/internal.perf/InsertItemsBenchmarks.cs new file mode 100644 index 000000000000..acd92ead3f8c --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal.perf/InsertItemsBenchmarks.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace System.ClientModel.Tests.Internal.Perf +{ +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + public class InsertItemsBenchmarks + { + [Params(1, 5, 20)] + public int Inserts; + + private readonly Dictionary _jsonPaths = new() + { + { "$.items"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.items.foo"u8.ToArray(), "value"u8.ToArray() }, + { "$.bars[0].x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.bars[0].x.y"u8.ToArray(), "value"u8.ToArray() }, + { "$.bars[1].x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.bars[1].x.y"u8.ToArray(), "value"u8.ToArray() }, + { "$.bar.foo.x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.bar.foo.x.y"u8.ToArray(), "value"u8.ToArray() }, + { "$.properties.virtualMachine"u8.ToArray(), "value"u8.ToArray() }, + { "$.sku.name"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.sku.name.something"u8.ToArray(), "value"u8.ToArray() }, + { "$.foo.bar.baz"u8.ToArray(), "value"u8.ToArray() }, + { "$.foo.baz.bar"u8.ToArray(), "value"u8.ToArray() }, + { "$.item.something.x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z"u8.ToArray(), "[{\"a\":\"value\"}]"u8.ToArray() }, + { "$.item.something.x.y.z[0]"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z[0].foo.bar.baz.qux"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z[0].foo.bar.baz.qux.quux"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z[0].foo.bar.baz.qux.quux.corge"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray()}, + }; + + private readonly Dictionary _jsonPathsNoNesting = new() + { + { "$.items"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.items1.foo"u8.ToArray(), "value"u8.ToArray() }, + { "$.bars[0].x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.bars[0].x1.y"u8.ToArray(), "value"u8.ToArray() }, + { "$.bars[1].x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.bars[1].x2.y"u8.ToArray(), "value"u8.ToArray() }, + { "$.bar.foo.x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.bar.foo.x1.y"u8.ToArray(), "value"u8.ToArray() }, + { "$.properties.virtualMachine"u8.ToArray(), "value"u8.ToArray() }, + { "$.sku.name"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.sku.name1.something"u8.ToArray(), "value"u8.ToArray() }, + { "$.foo.bar.baz"u8.ToArray(), "value"u8.ToArray() }, + { "$.foo1.baz.bar"u8.ToArray(), "value"u8.ToArray() }, + { "$.item.something.x"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x1.y"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y1.z"u8.ToArray(), "[{\"a\":\"value\"}]"u8.ToArray() }, + { "$.item.something.x.y.z1[0]"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z2[0].foo.bar.baz.qux"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z3[0].foo.bar.baz.qux.quux"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray() }, + { "$.item.something.x.y.z4[0].foo.bar.baz.qux.quux.corge"u8.ToArray(), "{\"a\":\"value\"}"u8.ToArray()}, + }; + + [Benchmark] + public void InsertItems() + { + JsonPatch jp = new(); + int count = 0; + foreach (var kvp in _jsonPaths) + { + jp.Set(kvp.Key.AsSpan(), kvp.Value); + count++; + if (count >= Inserts) + { + break; + } + } + } + + [Benchmark] + public void InsertItemsNoNesting() + { + JsonPatch jp = new(); + int count = 0; + foreach (var kvp in _jsonPathsNoNesting) + { + jp.Set(kvp.Key.AsSpan(), kvp.Value); + count++; + if (count >= Inserts) + { + break; + } + } + } + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +} diff --git a/sdk/core/System.ClientModel/tests/internal.perf/JsonPatchBenchmarks.cs b/sdk/core/System.ClientModel/tests/internal.perf/JsonPatchBenchmarks.cs new file mode 100644 index 000000000000..ea74e1f78ef1 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal.perf/JsonPatchBenchmarks.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace System.ClientModel.Tests.Internal.Perf +{ +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + public class JsonPatchBenchmarks + { + private JsonPatch _jpMatch; + private JsonPatch _jpMiss; +#if NET8_0_OR_GREATER + private string _property = "child"; +#endif + + [Params(1, 5, 20, 100)] + public int ItemsInDictionary; + + public JsonPatchBenchmarks() + { + _jpMatch = new JsonPatch(); + _jpMatch.Set("$.tags.child"u8, 0); + _jpMiss = new JsonPatch(); + _jpMiss.Set("$.tags.diffChild"u8, 0); + } + + [Benchmark] + public void ContainsChild_Stack_Optimized_Match() + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[256]; + for (int i=0; i< ItemsInDictionary; i++) + { + int length = Encoding.UTF8.GetBytes(_property.AsSpan(), buffer); + _jpMatch.Contains("$.tags"u8, buffer.Slice(0, length)); + } +#endif + } + + [Benchmark] + public void ContainsChild_Stack_FullPath_Match() + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[256]; + ReadOnlySpan span = "$.tags."u8; + span.CopyTo(buffer); + for (int i = 0; i < ItemsInDictionary; i++) + { + int length = span.Length; + length += Encoding.UTF8.GetBytes(_property.AsSpan(), buffer.Slice(length)); + _jpMatch.Contains(buffer.Slice(0, length)); + } +#endif + } + } +} +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/sdk/core/System.ClientModel/tests/internal.perf/JsonPatchUsageBenchmarks.cs b/sdk/core/System.ClientModel/tests/internal.perf/JsonPatchUsageBenchmarks.cs new file mode 100644 index 000000000000..e9ebd609ef42 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal.perf/JsonPatchUsageBenchmarks.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.ClientModel.Tests.ModelReaderWriterTests; +using System.IO; +using System.Reflection; +using BenchmarkDotNet.Attributes; + +namespace System.ClientModel.Tests.Internal.Perf +{ +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + public class JsonPatchUsageBenchmarks + { + private AvailabilitySetData _model; + private AvailabilitySetData _modelWithPatches; + private BinaryData _data; + private BinaryData _dataWithPatches; + + public JsonPatchUsageBenchmarks() + { + var json = File.ReadAllText(Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName, "TestData", "AvailabilitySetData/AvailabilitySetData.json")); + var data = BinaryData.FromString(json); + _model = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + _modelWithPatches = ModelReaderWriter.Read(data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + _modelWithPatches.Patch.Set("$.sku.name"u8, "newSkuName"); + _modelWithPatches.Patch.Append("$.properties.virtualMachines"u8, "{\"id\":\"vmId\"}"u8); + _modelWithPatches.Patch.Set("$.foobar"u8, 5); + _data = ModelReaderWriter.Write(_model, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + _dataWithPatches = ModelReaderWriter.Write(_modelWithPatches, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + } + + [Benchmark] + public BinaryData ModelOnly_Write() + { + return ModelReaderWriter.Write(_model, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + } + + [Benchmark] + public BinaryData ModelWithPatches_Write() + { + return ModelReaderWriter.Write(_modelWithPatches, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + } + + [Benchmark] + public AvailabilitySetData ModelOnly_Read() + { + return ModelReaderWriter.Read(_data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + } + + [Benchmark] + public AvailabilitySetData ModelWithPatches_Read() + { + return ModelReaderWriter.Read(_dataWithPatches, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default); + } + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +} diff --git a/sdk/core/System.ClientModel/tests/internal.perf/JsonPathComparerBenchmarks.cs b/sdk/core/System.ClientModel/tests/internal.perf/JsonPathComparerBenchmarks.cs new file mode 100644 index 000000000000..454f32c8f963 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal.perf/JsonPathComparerBenchmarks.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using BenchmarkDotNet.Attributes; + +namespace System.ClientModel.Tests.Internal.Perf +{ + public class JsonPathComparerBenchmarks + { + private byte[] _path1a = "$.properties.virtualMachines[0].name"u8.ToArray(); + private byte[] _path1b = "$.properties.virtualMachines[0].name"u8.ToArray(); + private byte[] _path1aPrime = "$.properties['virtualMachines'][0].name"u8.ToArray(); + private byte[] _path2 = "$.props.virtualMachines[0].name"u8.ToArray(); + private byte[] _path3 = "$.properties.virtualMachines[0].id"u8.ToArray(); + + [Benchmark] + public void Normalize_Comparer() + { + Span buffer = stackalloc byte[_path1a.Length]; + JsonPathComparer.Default.Normalize(_path1a, buffer, out int bytesWritten); + } + + [Benchmark] + public void Normalize_AlternateForm_Comparer() + { + Span buffer = stackalloc byte[_path1aPrime.Length]; + JsonPathComparer.Default.Normalize(_path1aPrime, buffer, out int bytesWritten); + } + + [Benchmark] + public int Normalize_GetHashCode_Comparer() + { + return JsonPathComparer.Default.GetNormalizedHashCode(_path1a); + } + + [Benchmark] + public int Normalize_GetHashCode_AlternateForm_Comparer() + { + return JsonPathComparer.Default.GetNormalizedHashCode(_path1aPrime); + } + + [Benchmark] + public bool Normalize_Equals_Comparer() + { + return JsonPathComparer.Default.NormalizedEquals(_path1a, _path1b); + } + + [Benchmark] + public bool Normalize_Equals_AlternateForm_Comparer() + { + return JsonPathComparer.Default.NormalizedEquals(_path1a, _path1aPrime); + } + + [Benchmark] + public bool Normalize_EarlyNotEqual_Comparer() + { + return JsonPathComparer.Default.NormalizedEquals(_path1a, _path2); + } + + [Benchmark] + public bool Normalize_LateNotEqual_Comparer() + { + return JsonPathComparer.Default.NormalizedEquals(_path1a, _path3); + } + + [Benchmark] + public int GetHashCode_Comparer() + { + return JsonPathComparer.Default.GetHashCode(_path1a); + } + + [Benchmark] + public int GetHashCode_AlternateForm_Comparer() + { + return JsonPathComparer.Default.GetHashCode(_path1aPrime); + } + + [Benchmark] + public bool Equal_Comparer() + { + return JsonPathComparer.Default.Equals(_path1a, _path1b); + } + + [Benchmark] + public bool Equal_AlternateForm_Comparer() + { + return JsonPathComparer.Default.Equals(_path1a, _path1aPrime); + } + + [Benchmark] + public bool EarlyNotEqual_Comparer() + { + return JsonPathComparer.Default.Equals(_path1a, _path2); + } + + [Benchmark] + public bool LateNotEqual_Comparer() + { + return JsonPathComparer.Default.Equals(_path1a, _path3); + } + + [Benchmark] + public int GetHashCode_Span() + { +#if NET8_0_OR_GREATER + var hash = new HashCode(); + hash.AddBytes(_path1a); + return hash.ToHashCode(); +#else + unchecked + { + int hash = 17; + for (int i = 0; i < _path1a.Length; i++) + { + hash = hash * 31 + _path1a[i]; + } + return hash; + } +#endif + } + + [Benchmark] + public bool Equal_Span() + { + return _path1a.AsSpan().SequenceEqual(_path1b.AsSpan()); + } + + [Benchmark] + public bool EarlyNotEqual_Span() + { + return _path1a.AsSpan().SequenceEqual(_path2.AsSpan()); + } + + [Benchmark] + public bool LateNotEqual_Span() + { + return _path1a.AsSpan().SequenceEqual(_path3.AsSpan()); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/internal.perf/JsonPathReaderBenchmarks.cs b/sdk/core/System.ClientModel/tests/internal.perf/JsonPathReaderBenchmarks.cs new file mode 100644 index 000000000000..f69eafa456a0 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal.perf/JsonPathReaderBenchmarks.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using BenchmarkDotNet.Attributes; +using System.ClientModel.Primitives; + +namespace System.ClientModel.Tests.Internal.Perf +{ + public class JsonPathReaderBenchmarks + { + [Benchmark] + public void Read_DotNotation() + { + var reader = new JsonPathReader("$.foo.bar[2].baz"u8); + while (reader.Read()) { } + } + + [Benchmark] + public void Read_SingleQuote() + { + var reader = new JsonPathReader("$['foo']['bar'][2]['baz']"u8); + while (reader.Read()) { } + } + + [Benchmark] + public void Read_DoubleQuote() + { + var reader = new JsonPathReader("$[\"foo\"].bar[2].baz"u8); + while (reader.Read()) { } + } + + [Benchmark] + public void GetFirstProperty_DotNotation() + { + var reader = new JsonPathReader("$.foo.bar[2].baz"u8); + var prop = reader.GetFirstProperty(); + } + + [Benchmark] + public void GetFirstProperty_SingleQuote() + { + var reader = new JsonPathReader("$['foo']['bar'][2]['baz']"u8); + var prop = reader.GetFirstProperty(); + } + + [Benchmark] + public void GetFirstProperty_DoubleQuote() + { + var reader = new JsonPathReader("$[\"foo\"].bar[2].baz"u8); + var prop = reader.GetFirstProperty(); + } + + [Benchmark] + public void Advance_DotNotation() + { + var reader = new JsonPathReader("$.foo.bar[2].baz"u8); + reader.Advance("$.foo.bar"u8); + } + + [Benchmark] + public void Advance_SingleQuote() + { + var reader = new JsonPathReader("$['foo']['bar'][2]['baz']"u8); + reader.Advance("$['foo']['bar']"u8); + } + + [Benchmark] + public void Advance_DoubleQuote() + { + var reader = new JsonPathReader("$[\"foo\"].bar[2].baz"u8); + reader.Advance("$[\"foo\"].bar"u8); + } + + [Benchmark] + public void Equals_DifferentVariants() + { + var reader1 = new JsonPathReader("$.foo.bar[2].baz"u8); + var reader2 = new JsonPathReader("$['foo']['bar'][2]['baz']"u8); + reader1.Equals(reader2); + } + + [Benchmark] + public void GetHashCode_DotNotation() + { + var reader = new JsonPathReader("$.foo.bar[2].baz"u8); + reader.GetHashCode(); + } + + [Benchmark] + public void GetHashCode_SingleQuote() + { + var reader = new JsonPathReader("$['foo']['bar'][2]['baz']"u8); + reader.GetHashCode(); + } + + [Benchmark] + public void GetHashCode_DoubleQuote() + { + var reader = new JsonPathReader("$[\"foo\"].bar[2].baz"u8); + reader.GetHashCode(); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/AvailabilitySetDataModel.cs b/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/AvailabilitySetDataModel.cs index b1a618f836ce..16e11076ff3c 100644 --- a/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/AvailabilitySetDataModel.cs +++ b/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/AvailabilitySetDataModel.cs @@ -9,7 +9,7 @@ namespace System.ClientModel.Tests.Internal.Perf public class AvailabilitySetDataModel : JsonModelBenchmark { protected override AvailabilitySetData Read(JsonElement jsonElement) - => AvailabilitySetData.DeserializeAvailabilitySetData(jsonElement, _options); + => AvailabilitySetData.DeserializeAvailabilitySetData(jsonElement, _options, null); protected override string JsonFileName => "AvailabilitySetData/AvailabilitySetData.json"; } diff --git a/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/JsonModelBenchmark.cs b/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/JsonModelBenchmark.cs index 4e97a8f243b3..7ccfa7ec9335 100644 --- a/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/JsonModelBenchmark.cs +++ b/sdk/core/System.ClientModel/tests/internal.perf/ModelReaderWriter/JsonModelBenchmark.cs @@ -51,7 +51,9 @@ public string Write_JsonSerializer() public string Write_JsonSerializer_SourceGenerated() { #if NET6_0_OR_GREATER +#pragma warning disable SCM0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return JsonSerializer.Serialize(_model, _model.GetType(), SourceGenerationContext.Default); +#pragma warning restore SCM0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #else return Write_JsonSerializer(); #endif @@ -125,7 +127,9 @@ public object Read_JsonSerializer_SourceGeneration() { #if NET6_0_OR_GREATER using var stream = new MemoryStream(); +#pragma warning disable SCM0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return JsonSerializer.Deserialize(_jsonSerializerResult, _model.GetType(), SourceGenerationContext.Default); +#pragma warning restore SCM0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #else return Read_JsonSerializer(); #endif diff --git a/sdk/core/System.ClientModel/tests/internal.perf/PatchSetBenchmarks.cs b/sdk/core/System.ClientModel/tests/internal.perf/PatchSetBenchmarks.cs new file mode 100644 index 000000000000..d206eb64897b --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal.perf/PatchSetBenchmarks.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; +using System.ClientModel.Tests.Client.Models.ResourceManager.Resources; +using System.ClientModel.Tests.ModelReaderWriterTests; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using BenchmarkDotNet.Attributes; + +namespace System.ClientModel.Tests.Internal.Perf +{ +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + public class PatchSetBenchmarks + { + private BinaryData _data; + + private const int operationsPerInvoke = 100000; + private List _models = new(operationsPerInvoke); + + public PatchSetBenchmarks() + { + var json = File.ReadAllText(Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName, "TestData", "AvailabilitySetData/AvailabilitySetData.json")); + _data = BinaryData.FromString(json); + } + + [IterationSetup] + public void IterationSetup() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models.Add(ModelReaderWriter.Read(_data, ModelReaderWriterOptions.Json, TestClientModelReaderWriterContext.Default)); + _models[i].Patch.Set("$.x"u8, 0); // force initialize the patch + _models[i].Sku.Patch.Set("$.x"u8, 0); // force initialize the patch in nested model + for (int j = 0; j < 20; j++) + { + _models[i].VirtualMachines.Add(new WritableSubResource { Id = $"vmId{j}" }); + } + _models[i].VirtualMachines[5].Patch.Set("$.x"u8, 0); // force initialize the patch in array item + } + } + + [IterationCleanup] + public void IterationCleanup() + { + _models.Clear(); + } + + [Benchmark(OperationsPerInvoke = operationsPerInvoke)] + public void Patch_Set_Propagate_ExistingProperty() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models[i].Patch.Set("$.sku.name"u8, "newSkuName"); + } + } + + [Benchmark(OperationsPerInvoke = operationsPerInvoke)] + public void Patch_Set_Propagate_NewProperty() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models[i].Patch.Set("$.sku.foobar"u8, "value"); + } + } + + [Benchmark(OperationsPerInvoke = operationsPerInvoke)] + public void Patch_Set_Propagate_ArrayItem_ExistingProperty() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models[i].Patch.Set("$.properties.virtualMachines[5].id"u8, "newId"); + } + } + + [Benchmark(OperationsPerInvoke = operationsPerInvoke)] + public void Patch_Set_Propagate_ArrayItem_NewProperty() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models[i].Patch.Set("$.properties.virtualMachines[5].foobar"u8, "value"); + } + } + + [Benchmark(OperationsPerInvoke = operationsPerInvoke)] + public void Patch_Set_Propagate_DictionaryItem_ExistingProperty() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models[i].Patch.Set("$.tags.key"u8, "newValue"); + } + } + + [Benchmark(OperationsPerInvoke = operationsPerInvoke)] + public void Patch_Set_Propagate_DictionaryItem_NewProperty() + { + for (int i = 0; i < operationsPerInvoke; i++) + { + _models[i].Patch.Set("$.tags.foobar"u8, "value"); + } + } + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +} diff --git a/sdk/core/System.ClientModel/tests/internal.perf/Program.cs b/sdk/core/System.ClientModel/tests/internal.perf/Program.cs index 88f99575e16b..9425f80c3744 100644 --- a/sdk/core/System.ClientModel/tests/internal.perf/Program.cs +++ b/sdk/core/System.ClientModel/tests/internal.perf/Program.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Azure.Test.Perf; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Jobs; @@ -15,27 +11,21 @@ namespace System.ClientModel.Tests.Internal.Perf; public class Program { - public static async Task Main(string[] args) + public static void Main(string[] args) { - if (!args.Contains("--bm")) - // Allow the framework to execute the test scenarios. - await PerfProgram.Main(Assembly.GetExecutingAssembly(), args); - - // To run Benchmark.NET benchmarks, use the --bm flag. - // To see the list of benchmarks that can be run - // dotnet run -c Release --framework net6.0 --bm --list flat + // dotnet run -c Release --framework net6.0 --list flat // To run a specific benchmark class - // dotnet run -c Release --framework net6.0 --bm --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* + // dotnet run -c Release --framework net6.0 --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* // To run a specific benchmark method - // dotnet run -c Release --framework net6.0 --bm --filter *ResourceProviderDataModel.Read_PublicInterface + // dotnet run -c Release --framework net6.0 --filter *ResourceProviderDataModel.Read_PublicInterface // or - // dotnet run -c Release --framework net6.0 --bm --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel.Read_PublicInterface + // dotnet run -c Release --framework net6.0 --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel.Read_PublicInterface // To run a specific benchmark class and category - // dotnet run -c Release --framework net6.0 --bm --anyCategories PublicInterface --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* + // dotnet run -c Release --framework net6.0 --anyCategories PublicInterface --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* var config = ManualConfig.Create(DefaultConfig.Instance); config.Options = ConfigOptions.JoinSummary | ConfigOptions.StopOnFirstError; @@ -46,6 +36,6 @@ public static async Task Main(string[] args) .WithMinIterationCount(15) .WithMaxIterationCount(20) .AsDefault()); - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args.Where(a => !a.Equals("--bm")).ToArray(), config); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); } -} \ No newline at end of file +} diff --git a/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonModelConverterTests.cs b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonModelConverterTests.cs index e8c4f50cbf35..90a75c055f94 100644 --- a/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonModelConverterTests.cs +++ b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonModelConverterTests.cs @@ -205,7 +205,7 @@ private static Dictionary GetRawData(object model) { modelType = modelType.BaseType!; } - var propertyInfo = modelType.GetField("_rawData", BindingFlags.Instance | BindingFlags.NonPublic); + var propertyInfo = modelType.GetField("_serializedAdditionalRawData", BindingFlags.Instance | BindingFlags.NonPublic); Assert.IsNotNull(propertyInfo); return (Dictionary)propertyInfo!.GetValue(model)!; } diff --git a/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathEqualityComparerTests.cs b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathEqualityComparerTests.cs new file mode 100644 index 000000000000..37a71552d965 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathEqualityComparerTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.ClientModel.Primitives; +using System.Text; +using NUnit.Framework; + +namespace System.ClientModel.Tests.Internal.ModelReaderWriterTests +{ + public class JsonPathEqualityComparerTests + { + [TestCase("$['foo']", "$[\"foo\"]", true)] + [TestCase("$.a['b.c']", "$.a.b.c", false)] + [TestCase("$.x.y", "$['x'].y", true)] + [TestCase("$.x.y", "$[\"x\"].y", true)] + [TestCase("$.x.y", "$['x']['y']", true)] + [TestCase("$.x.y", "$.x.y.z", false)] + [TestCase("$.x.y", "$.x.z", false)] + [TestCase("$.foo.bar", "$['foo']['bar']", true)] + [TestCase("$.foo.bar", "$['foo\'']['bar']", false)] + [TestCase("$.foo.bar", "$['fo\'o']['bar']", false)] + [TestCase("$.foo.bar", "$[\"foo\"].bar", true)] + [TestCase("$.foo.bar", "$[\"\"foo\"].bar", false)] + [TestCase("$.foo.bar", "$[\"f\"oo\"].bar", false)] + [TestCase("$.foo.bar", "$.foo.bar.baz", false)] + [TestCase("$.foo.bar", "$.foo.baz", false)] + [TestCase("$.a.b[0].c", "$['a'].b[0]['c']", true)] + [TestCase("$.a.b[0].c", "$.a.b[1].c", false)] + [TestCase("$.a.b[0].c", "$.a.b[0].d", false)] + [TestCase("$.foo.bar[2].baz", "$['foo']['bar'][2]['baz']", true)] + [TestCase("$.foo.bar[2].baz", "$.foo.bar[3].baz", false)] + [TestCase("$.foo.bar[2].baz", "$.foo.bar[2].qux", false)] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$['a'].b[0]['c'].d[1][\"e\"].f", true)] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$.a.b[0].c.d[2]['e'].f", false)] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$.a.b[0].c.d[1]['e'].g", false)] + [TestCase("$[\"f.oo\"]['bar']", "$['f.oo'].bar", true)] + [TestCase("$['f.oo']['bar']", "$['f.oo'].bar", true)] + [TestCase("$['']", "$.", true)] + [TestCase("$['f.o.o']", "$['f.o.o']", true)] + [TestCase("$['f.o.o']", "$.f.o.o", false)] + [TestCase("$['mix\"quote']", "$['mix\"quote']", true)] + public void Equals_ByteArray(string left, string right, bool expected) + { + var a = Encoding.UTF8.GetBytes(left); + var b = Encoding.UTF8.GetBytes(right); + Assert.AreEqual(expected, JsonPathComparer.Default.NormalizedEquals(a, b)); + + Span buffer = stackalloc byte[Math.Max(a.Length, b.Length)]; + JsonPathComparer.Default.Normalize(a.AsSpan(), buffer, out int bytesWrittenA); + var normalizedA = buffer.Slice(0, bytesWrittenA).ToArray(); + JsonPathComparer.Default.Normalize(b.AsSpan(), buffer, out int bytesWrittenB); + var normalizedB = buffer.Slice(0, bytesWrittenB).ToArray(); + + Assert.AreEqual(expected, JsonPathComparer.Default.Equals(normalizedA, normalizedB)); + } + + [Test] + public void Equals_ByteArray_Same() + { + var a = "$.x"u8.ToArray(); + Assert.AreEqual(true, JsonPathComparer.Default.Equals(a, a)); + } + + [Test] + public void Equals_ByteArray_Null() + { + var a = "$.x"u8.ToArray(); + Assert.AreEqual(false, JsonPathComparer.Default.Equals(a, null)); + Assert.AreEqual(false, JsonPathComparer.Default.Equals(null, a)); + } + + [TestCase("$.x.y", "$['x'].y", true)] + [TestCase("$.x.y", "$[\"x\"].y", true)] + [TestCase("$.x.y", "$['x']['y']", true)] + [TestCase("$.x.y", "$.x.y.z", false)] + [TestCase("$.x.y", "$.x.z", false)] + [TestCase("$.foo.bar", "$['foo']['bar']", true)] + [TestCase("$.foo.bar", "$['foo\'']['bar']", false)] + [TestCase("$.foo.bar", "$['fo\'o']['bar']", false)] + [TestCase("$.foo.bar", "$[\"foo\"].bar", true)] + [TestCase("$.foo.bar", "$[\"\"foo\"].bar", false)] + [TestCase("$.foo.bar", "$[\"f\"oo\"].bar", false)] + [TestCase("$.foo.bar", "$.foo.bar.baz", false)] + [TestCase("$.foo.bar", "$.foo.baz", false)] + [TestCase("$.a.b[0].c", "$['a'].b[0]['c']", true)] + [TestCase("$.a.b[0].c", "$.a.b[1].c", false)] + [TestCase("$.a.b[0].c", "$.a.b[0].d", false)] + [TestCase("$.foo.bar[2].baz", "$['foo']['bar'][2]['baz']", true)] + [TestCase("$.foo.bar[2].baz", "$.foo.bar[3].baz", false)] + [TestCase("$.foo.bar[2].baz", "$.foo.bar[2].qux", false)] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$['a'].b[0]['c'].d[1][\"e\"].f", true)] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$.a.b[0].c.d[2]['e'].f", false)] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$.a.b[0].c.d[1]['e'].g", false)] + public void Equals_SpanToByteArray(string left, string right, bool expected) + { + var a = Encoding.UTF8.GetBytes(left); + var b = Encoding.UTF8.GetBytes(right); + Assert.AreEqual(expected, JsonPathComparer.Default.NormalizedEquals(a.AsSpan(), b)); + } + + [Test] + public void Equals_SpanToNullByteArray() + { + var a = "$.x"u8.ToArray(); + Assert.AreEqual(false, JsonPathComparer.Default.Equals(a.AsSpan(), null!)); + } + + [TestCase("$['x'].y", "$.x.y")] + [TestCase("$[\"x\"].y", "$.x.y")] + [TestCase("$['x']['y']", "$.x.y")] + [TestCase("$['foo']['bar']", "$.foo.bar")] + [TestCase("$['foo\'']['bar']", "$.foo'.bar")] + [TestCase("$['fo\'o']['bar']", "$.fo'o.bar")] + [TestCase("$[\"foo\"].bar", "$.foo.bar")] + [TestCase("$[\"\"foo\"].bar", "$.\"foo.bar")] + [TestCase("$[\"f\"oo\"].bar", "$.f\"oo.bar")] + [TestCase("$['a'].b[0]['c']", "$.a.b[0].c")] + [TestCase("$[\"a\"].b[0].c", "$.a.b[0].c")] + [TestCase("$['foo']['bar'][2]['baz']", "$.foo.bar[2].baz")] + [TestCase("$['a'].b[0]['c'].d[1][\"e\"].f", "$.a.b[0].c.d[1].e.f")] + [TestCase("$['a'].b[0][\"c\"].d[1].e.f", "$.a.b[0].c.d[1].e.f")] + [TestCase("$['f.oo']['bar']", "$['f.oo'].bar")] + [TestCase("$[\"f.oo\"]['bar']", "$['f.oo'].bar")] + [TestCase("$['a'][\"b.c\"]['d']", "$.a['b.c'].d")] + public void Normalize(string input, string expected) + { + var a = Encoding.UTF8.GetBytes(input); + Span buffer = stackalloc byte[a.Length]; + JsonPathComparer.Default.Normalize(a.AsSpan(), buffer, out int bytesWritten); + Assert.AreEqual(expected, Encoding.UTF8.GetString(buffer.Slice(0, bytesWritten).ToArray())); + } + + [TestCase("$.x.y", "$['x'].y")] + [TestCase("$.x.y", "$[\"x\"].y")] + [TestCase("$.x.y", "$['x']['y']")] + [TestCase("$.foo.bar", "$['foo']['bar']")] + [TestCase("$.foo.bar", "$.foo.bar")] + [TestCase("$.foo.bar", "$[\"foo\"].bar")] + [TestCase("$.a.b[0].c", "$['a'].b[0]['c']")] + [TestCase("$.foo.bar[2].baz", "$['foo']['bar'][2]['baz']")] + [TestCase("$.a.b[0].c.d[1]['e'].f", "$['a'].b[0]['c'].d[1][\"e\"].f")] + public void NormalizedHashCode(string left, string right) + { + var a = Encoding.UTF8.GetBytes(left); + var b = Encoding.UTF8.GetBytes(right); + Assert.AreEqual(JsonPathComparer.Default.GetNormalizedHashCode(a), JsonPathComparer.Default.GetNormalizedHashCode(b)); + + Span buffer = stackalloc byte[Math.Max(a.Length, b.Length)]; + JsonPathComparer.Default.Normalize(a.AsSpan(), buffer, out int bytesWrittenA); + var normalizedA = buffer.Slice(0, bytesWrittenA).ToArray(); + JsonPathComparer.Default.Normalize(b.AsSpan(), buffer, out int bytesWrittenB); + var normalizedB = buffer.Slice(0, bytesWrittenB).ToArray(); + + Assert.AreEqual(JsonPathComparer.Default.GetHashCode(normalizedA), JsonPathComparer.Default.GetHashCode(normalizedB)); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathExtensionsTests.cs b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathExtensionsTests.cs new file mode 100644 index 000000000000..913ebb3be222 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathExtensionsTests.cs @@ -0,0 +1,510 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Text; +using System.Text.Json; +using NUnit.Framework; + +namespace System.ClientModel.Tests.Internal.ModelReaderWriterTests +{ + public class JsonPathExtensionsTests + { + [TestCase("{\"obj\":{\"a\":1}}", "$.obj", "{\"a\":1}")] + [TestCase("{\"ob.j\":{\"a\":1}}", "$['ob.j']", "{\"a\":1}")] + [TestCase("{\"ob\\\"j\":{\"a\":1}}", "$['ob\\\"j']", "{\"a\":1}")] + [TestCase("{\"obj\":{\"arr\":[1,2,3]}}", "$.obj.arr[1]", "2")] + [TestCase("{\"obj\":{\"arr\":[{\"x\":{\"y\":1}},{\"x\":{\"y\":2}},{\"x\":{\"y\":3}}]}}", "$.obj.arr[1]", "{\"x\":{\"y\":2}}")] + [TestCase("{\"obj\":{\"arr\":[{\"x\":{\"y\":1}},{\"x\":{\"y\":2}},{\"x\":{\"y\":3}}]}}", "$.obj.arr", "[{\"x\":{\"y\":1}},{\"x\":{\"y\":2}},{\"x\":{\"y\":3}}]")] + [TestCase("{\"obj\":{\"arr\":[{\"x\":{\"y\":1}},{\"x\":{\"y\":2}},{\"x\":{\"y\":3}}]}}", "$.obj.arr[2].x", "{\"y\":3}")] + public void TryGetJson_Found(string jsonStr, string jsonPathStr, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + Assert.IsTrue(json.TryGetJson(jsonPath, out var slice)); + Assert.AreEqual(expected, Encoding.UTF8.GetString(slice.ToArray())); + } + + [TestCase("{\"a\":1}", "$.missing")] + [TestCase("{\"a\":1}", "$.a[0]")] + [TestCase("{\"a\":[1,2]}", "$.a[2]")] + [TestCase("{\"a\":[1,2]}", "$.a[10]")] + [TestCase("{\"a\":[1,2]}", "$.a[0].x")] + public void GetJson_NotFound_Throws(string jsonStr, string jsonPathStr) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + var ex = Assert.Throws(() => json.GetJson(Encoding.UTF8.GetBytes(jsonPathStr))); + Assert.AreEqual($"{jsonPathStr} was not found in the JSON structure.", ex!.Message); + } + + [TestCase("{\"a\":1,\"b\":2}", "$.b", "100", "{\"a\":1,\"b\":100}")] + [TestCase("{\"a\":1,\"b\":2}", "$.a", "100", "{\"a\":100,\"b\":2}")] + [TestCase("{\"a\":1,\"b\":\"value\"}", "$.b", "\"newValue\"", "{\"a\":1,\"b\":\"newValue\"}")] + [TestCase("{\"a\":1,\"b\":\"value\",\"c\":[1,2,3]}", "$.c[0]", "10", "{\"a\":1,\"b\":\"value\",\"c\":[10,2,3]}")] + [TestCase("{\"a\":1,\"b\":\"value\",\"c\":[1,2,3]}", "$.c[1]", "10", "{\"a\":1,\"b\":\"value\",\"c\":[1,10,3]}")] + [TestCase("{\"a\":1,\"b\":\"value\",\"c\":[1,2,3]}", "$.c[2]", "10", "{\"a\":1,\"b\":\"value\",\"c\":[1,2,10]}")] + [TestCase("{\"a\":1}", "$.b", "2", "{\"a\":1,\"b\":2}")] + public void Json_Set(string jsonStr, string jsonPathStr, string newValue, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + ReadOnlyMemory value = Encoding.UTF8.GetBytes(newValue); + var replaced = json.Set(jsonPath, value); + Assert.AreEqual(expected, Encoding.UTF8.GetString(replaced)); + } + + [TestCase("{\"a\":1,\"b\":2}", "$.b", "{\"a\":1}")] + [TestCase("{\"a\":1}", "$.a", "{}")] + [TestCase("{\"a\":1,\"b\":2,\"c\":3}", "$.b", "{\"a\":1,\"c\":3}")] + public void Json_Remove(string jsonStr, string jsonPathStr, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + var updated = json.Remove(jsonPath); + Assert.AreEqual(expected, Encoding.UTF8.GetString(updated)); + } + + [TestCase("{\"arr\":[1]}", "$.arr", "2", "{\"arr\":[1,2]}")] + [TestCase("{\"arr\":[]}", "$.arr", "2", "{\"arr\":[2]}")] + [TestCase("{\"arr\":[{\"x\":1}]}", "$.arr", "{\"x\":2}", "{\"arr\":[{\"x\":1},{\"x\":2}]}")] + [TestCase("{\"arr\":[]}", "$.arr", "{\"x\":2}", "{\"arr\":[{\"x\":2}]}")] + public void Append_Array(string jsonStr, string jsonPathStr, string newValue, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + ReadOnlyMemory value = Encoding.UTF8.GetBytes(newValue); + var appended = json.Append(jsonPath, value); + Assert.AreEqual(expected, Encoding.UTF8.GetString(appended)); + } + + [TestCase("{\"arr\":[10,20,30]}", "$.arr", 3)] + [TestCase("{\"arr\":[]}", "$.arr", 0)] + public void GetArrayLength(string jsonStr, string jsonPathStr, int expectedLength) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + var reader = new Utf8JsonReader(json.Span); + int length = reader.GetArrayLength(jsonPath); + Assert.AreEqual(expectedLength, length); + } + + [TestCase("$[0]", true)] + [TestCase("$.x[0]", true)] + [TestCase("$[0].x", false)] + [TestCase("$[]", false)] + [TestCase("abc", false)] + [TestCase("$[1]", true)] + [TestCase("$[12]", true)] + [TestCase("$.a[10]", true)] + [TestCase("$.a[10].b", false)] + [TestCase("$.a[1][2]", true)] + [TestCase("$[0005]", true)] + [TestCase("$[2147483647]", true)] + [TestCase("$[18446744073709551615]", true)] + [TestCase("$[1a]", false)] + [TestCase("$[-1]", false)] + [TestCase("$[+1]", false)] + [TestCase("$[1.2]", false)] + [TestCase("$[1 ]", false)] + [TestCase("$[ 1]", false)] + [TestCase("$['0']", false)] + [TestCase("$[\"0\"]", false)] + [TestCase("$[[1]]", false)] + [TestCase("$.a[1", false)] + [TestCase("$.a1]", false)] + [TestCase("abc[3]", true)] + [TestCase("$[1", false)] + [TestCase("$[1]]", false)] + [TestCase("1111]", false)] + public void IsArrayIndex(string jsonPathStr, bool expected) + { + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + Assert.AreEqual(expected, jsonPath.IsArrayIndex()); + + byte[] jsonPathArray = jsonPath.ToArray(); + Assert.AreEqual(expected, jsonPathArray.IsArrayIndex()); + } + + [TestCase("$[0][1].a", "$[0][1].a")] + [TestCase("$[0][1]", "$")] + [TestCase("$.x[0][1].a", "$.x[0][1].a")] + [TestCase("$.x[0][1]", "$.x")] + [TestCase("$.x.y[0][1].a", "$.x.y[0][1].a")] + [TestCase("$.x.y[0][1]", "$.x.y")] + [TestCase("$[0].x[1].a", "$[0].x[1].a")] + [TestCase("$[0].x[1]", "$[0].x")] + [TestCase("$.a[0][1][2]", "$.a")] + public void GetFirstNonArrayy(string jsonPathStr, string expected) + { + byte[] jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + var first = jsonPath.GetFirstNonIndexParent(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(first.ToArray())); + } + + [TestCase(".foo.bar[0]", "foo")] + [TestCase("foo.bar", "foo")] + [TestCase("['prop'].x", "prop")] + [TestCase("$['prop'].x", "prop")] + [TestCase("prop'].x", "prop")] + [TestCase("pr'op'].x", "pr'op")] + [TestCase("pr\"op'].x", "pr\"op")] + [TestCase("pr.op'].x", "pr")] + [TestCase("prop.x", "prop")] + [TestCase("pr'op.x", "pr'op")] + [TestCase("pr\"op.x", "pr\"op")] + [TestCase("pr.op.x", "pr")] + [TestCase("prop\"].x", "prop")] + [TestCase("pr'op\"].x", "pr'op")] + [TestCase("pr\"op\"].x", "pr\"op")] + [TestCase("pr.op\"].x", "pr")] + [TestCase("prop[0].x", "prop")] + [TestCase("['pr\"op'][0].x", "pr\"op")] + [TestCase("['pr.op'][0].x", "pr.op")] + [TestCase("", "")] + [TestCase("prop", "prop")] + public void GetPropertyNameFromSlice(string jsonSliceStr, string expected) + { + ReadOnlySpan jsonSlice = Encoding.UTF8.GetBytes(jsonSliceStr); + var name = jsonSlice.GetPropertyNameFromSlice(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(name.ToArray())); + } + + [TestCase("$.a.b.c", "c")] + [TestCase("$.a.b['c']", "c")] + [TestCase("$.a.b[\"c\"]", "c")] + [TestCase("$.a.b.c[0]", "c")] + [TestCase("$", "")] + public void GetPropertyName(string jsonPathStr, string expected) + { + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + var last = jsonPath.GetPropertyName(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(last.ToArray())); + } + + [TestCase("$.foo.bar", "$.foo")] + [TestCase("$['foo'].bar", "$['foo']")] + [TestCase("$[0].bar", "$[0]")] + [TestCase("$.x[0].bar", "$.x[0]")] + [TestCase("$.x[0]", "$.x")] + [TestCase("$[0]", "$")] + [TestCase("$['f.oo'].bar", "$['f.oo']")] + [TestCase("$.bar['f.oo']", "$.bar")] + [TestCase("$", "$")] + [TestCase("$prop", "")] + [TestCase("$prop']", "")] + [TestCase("$prop\"]", "")] + public void GetParent(string jsonPathStr, string expected) + { + var result = Encoding.UTF8.GetBytes(jsonPathStr).GetParent(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(result.ToArray())); + } + + [TestCase("$[123]", "123")] + [TestCase("$[0]", "0")] + [TestCase("$['0']", "")] + [TestCase("$.a[12]", "12")] + [TestCase("$['a'][\"b\"]", "")] + [TestCase("$.a", "")] + [TestCase("$1]", "")] + public void GetIndexSpan_Value(string jsonPathStr, string expected) + { + var index = Encoding.UTF8.GetBytes(jsonPathStr).GetIndexSpan(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(index.ToArray())); + } + + [TestCase("$", true)] + [TestCase(".$", false)] + [TestCase("$.x", false)] + public void IsRoot(string jsonPathStr, bool expected) + { + var root = Encoding.UTF8.GetBytes(jsonPathStr); + Assert.AreEqual(expected, root.IsRoot()); + } + + [TestCase("[10,20,30]", "10", 0, 0)] + [TestCase("[10,20,30]", "20", 1, 1)] + [TestCase("[10,20,30]", "30", 2, 2)] + public void SkipToIndex(string jsonStr, string expectedValue, int index, int expectedMaxIndex) + { + var json = Encoding.UTF8.GetBytes(jsonStr); + var reader = new Utf8JsonReader(json); + Assert.IsTrue(reader.Read()); + int maxIndex; + Assert.IsTrue(reader.SkipToIndex(index, out maxIndex)); + Assert.AreEqual(expectedValue, Encoding.UTF8.GetString(reader.ValueSpan.ToArray())); + Assert.AreEqual(expectedMaxIndex, maxIndex); + } + + [TestCase("[10,20,30]", -1, 3)] + [TestCase("[10,20,30]", 3, 3)] + [TestCase("[10,20,30]", 10, 3)] + public void SkipToIndex_NotFound_Throws(string jsonStr, int index, int expectedMaxIndex) + { + var json = Encoding.UTF8.GetBytes(jsonStr); + var reader = new Utf8JsonReader(json); + Assert.IsTrue(reader.Read()); + int maxIndex; + Assert.IsFalse(reader.SkipToIndex(index, out maxIndex)); + Assert.AreEqual(expectedMaxIndex, maxIndex); + } + + [TestCase("$.a.b.c", "$.a")] + [TestCase("$.foo.bar.baz", "$.foo")] + [TestCase("$['prop'].x.y", "$['prop']")] + [TestCase("$[\"prop\"].x.y", "$[\"prop\"]")] + [TestCase("$", "$")] + public void GetFirstProperty(string jsonPathStr, string expected) + { + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + var first = jsonPath.GetFirstProperty(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(first.ToArray())); + } + + [TestCase("{\"a\":1,\"b\":2}", "$.c", "3", false, "{\"a\":1,\"b\":2,\"c\":3}")] + [TestCase("{\"a\":1}", "$.a", "2", true, "{\"a\":2}")] + [TestCase("{\"arr\":[1,2]}", "$.arr[1]", "10", true, "{\"arr\":[1,10]}")] + [TestCase("{}", "$.new", "42", false, "{\"new\":42}")] + public void SetCurrentValue(string jsonStr, string jsonPathStr, string newValue, bool shouldBeFound, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + ReadOnlyMemory value = Encoding.UTF8.GetBytes(newValue); + + var reader = new Utf8JsonReader(json.Span); + Assert.AreEqual(shouldBeFound, reader.Advance(jsonPath)); + + var result = reader.SetCurrentValue(shouldBeFound, jsonPath.GetPropertyName(), json, value); + Assert.AreEqual(expected, Encoding.UTF8.GetString(result)); + } + + [TestCase("{\"arr\":[1,2,3]}", "$.arr[1]", "10", "{\"arr\":[1,10,2,3]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[0]", "10", "{\"arr\":[10,1,2,3]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[3]", "4", "{\"arr\":[1,2,3,4]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[4]", "4", "{\"arr\":[1,2,3,null,4]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[5]", "6", "{\"arr\":[1,2,3,null,null,6]}")] + [TestCase("{\"arr\":null}", "$.arr[0]", "1", "{\"arr\":[1]}")] + [TestCase("{\"arr\":[[1,2],[3,4]]}", "$.arr[0][1]", "6", "{\"arr\":[[1,6,2],[3,4]]}")] + [TestCase("{\"arr\":[[1,2],[3,4]]}", "$.arr[3][1]", "6", "{\"arr\":[[1,2],[3,4],null,[null,6]]}")] + [TestCase("{\"arr\":[[[1],[]],[[3,4]]]}", "$.arr[0][1][1]", "6", "{\"arr\":[[[1],[null,6]],[[3,4]]]}")] + [TestCase("{\"arr\":[[[1],[]],[[3,4]]]}", "$.arr[3][3][0]", "6", "{\"arr\":[[[1],[]],[[3,4]],null,[null,null,null,[6]]]}")] + [TestCase("{\"arr\":[[[[1],[]],[[3,4]]]]}", "$.arr[0][0][1][1]", "6", "{\"arr\":[[[[1],[null,6]],[[3,4]]]]}")] + [TestCase("{\"arr\":[[[[1],[]],[[3,4]]]]}", "$.arr[3][3][3][1]", "6", "{\"arr\":[[[[1],[]],[[3,4]]],null,null,[null,null,null,[null,null,null,[null,6]]]]}")] + [TestCase("{\"arr\":[]}", "$.arr[3]", "5", "{\"arr\":[null,null,null,5]}")] + [TestCase("{\"arr\":[]}", "$.arr[3][2]", "7", "{\"arr\":[null,null,null,[null,null,7]]}")] + public void InsertAt(string jsonStr, string arrayPathStr, string newValue, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan arrayPath = Encoding.UTF8.GetBytes(arrayPathStr); + ReadOnlyMemory value = Encoding.UTF8.GetBytes(newValue); + + var result = json.InsertAt(arrayPath, value); + Assert.AreEqual(expected, Encoding.UTF8.GetString(result)); + } + + [TestCase("{\"arr\":1}", "$.arr[1]")] + [TestCase("{\"arr\":{}}", "$.arr[1]")] + [TestCase("{\"arr\":\"x\"}", "$.arr[1]")] + public void InsertAt_InvalidPath_Throws(string jsonStr, string arrayPathStr) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + var ex = Assert.Throws(() => json.InsertAt(Encoding.UTF8.GetBytes(arrayPathStr), Encoding.UTF8.GetBytes("1"))); + Assert.AreEqual($"$.arr is not an array.", ex!.Message); + } + + [TestCase("{\"a\":1,\"b\":2}", "$.a", true)] + [TestCase("{\"a\":1,\"b\":2}", "$.c", false)] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[1]", true)] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[5]", false)] + [TestCase("{\"arr\":[]}", "$.arr[0]", false)] + [TestCase("{\"arr\":[]}", "$.arr", true)] + [TestCase("{}", "$", true)] + [TestCase("{}", "$.x", false)] + [TestCase("{\"x\":{\"y\":{\"z\":1}}}", "$.x", true)] + [TestCase("{\"x\":{\"y\":{\"z\":1}}}", "$.x.y", true)] + [TestCase("{\"x\":{\"y\":{\"z\":1}}}", "$.x.y.z", true)] + [TestCase("{\"x\":{\"y\":{\"z\":1}}}", "$.x.y.z.a", false)] + public void Advance(string jsonStr, string jsonPathStr, bool expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + + var reader = new Utf8JsonReader(json.Span); + bool result = reader.Advance(jsonPath); + Assert.AreEqual(expected, result); + + var jsonReader = new Utf8JsonReader(json.Span); + var pathReader = new JsonPathReader(jsonPath); + result = jsonReader.Advance(ref pathReader); + Assert.AreEqual(expected, result); + } + + [TestCase("{\"arr\":[1]}", "$.arr", "", "2", "{\"arr\":[1,2]}")] + [TestCase("{\"arr\":[]}", "$.arr", "", "1", "{\"arr\":[1]}")] + [TestCase("{\"obj\":{}}", "$.obj", "prop", "value", "{\"obj\":{\"prop\":value}}")] + [TestCase("{\"arr\":[1,2]}", "$.arr", "", "3", "{\"arr\":[1,2,3]}")] + [TestCase("{\"obj\":{\"x\":1}}", "$.obj", "prop", "value", "{\"obj\":{\"x\":1,\"prop\":value}}")] + public void Insert(string jsonStr, string jsonPathStr, string propertyNameStr, string newValue, string expected) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + ReadOnlyMemory value = Encoding.UTF8.GetBytes(newValue); + ReadOnlySpan propertyName = string.IsNullOrEmpty(propertyNameStr) ? ReadOnlySpan.Empty : Encoding.UTF8.GetBytes(propertyNameStr); + + var reader = new Utf8JsonReader(json.Span); + reader.Advance(jsonPath); + + var result = reader.Insert(json, propertyName, value); + Assert.AreEqual(expected, Encoding.UTF8.GetString(result)); + } + + [TestCase("invalid")] + [TestCase("")] + public void GetPropertyName_InvalidPath_Throws(string jsonPathStr) + { + var ex = Assert.Throws(() => Encoding.UTF8.GetBytes(jsonPathStr).GetPropertyName()); +#if NET8_0_OR_GREATER + Assert.AreEqual("JsonPath must start with '$' (Parameter 'jsonPath')", ex!.Message.Split('\n')[0]); +#else + Assert.AreEqual("JsonPath must start with '$'\r\nParameter name: jsonPath", ex!.Message); +#endif + } + + [TestCase("invalid")] + [TestCase("")] + public void GetParent_InvalidPath_Throws(string jsonPathStr) + { + var ex = Assert.Throws(() => Encoding.UTF8.GetBytes(jsonPathStr).GetParent()); +#if NET8_0_OR_GREATER + Assert.AreEqual("JsonPath must start with '$' (Parameter 'jsonPath')", ex!.Message.Split('\n')[0]); +#else + Assert.AreEqual("JsonPath must start with '$'\r\nParameter name: jsonPath", ex!.Message); +#endif + } + + [TestCase("invalid")] + [TestCase("")] + public void GetIndexSpan_InvalidPath_Throws(string jsonPathStr) + { + var ex = Assert.Throws(() => Encoding.UTF8.GetBytes(jsonPathStr).GetIndexSpan()); +#if NET8_0_OR_GREATER + Assert.AreEqual("JsonPath must start with '$' (Parameter 'jsonPath')", ex!.Message.Split('\n')[0]); +#else + Assert.AreEqual("JsonPath must start with '$'\r\nParameter name: jsonPath", ex!.Message); +#endif + } + + [TestCase("invalid")] + [TestCase("")] + public void Advance_InvalidPath_Throws(string jsonPathStr) + { + var json = Encoding.UTF8.GetBytes("{}"); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + + var reader = new Utf8JsonReader(json); + bool exceptionThrown = false; + try + { + reader.Advance(jsonPath); + } + catch (ArgumentException ex) + { +#if NET8_0_OR_GREATER + Assert.AreEqual("JsonPath must start with '$' (Parameter 'jsonPath')", ex!.Message); +#else + Assert.AreEqual("JsonPath must start with '$'\r\nParameter name: jsonPath", ex!.Message); +#endif + exceptionThrown = true; + } + Assert.IsTrue(exceptionThrown); + } + + [TestCase("{\"arr\":[1,2,3],\"notArray\":1}", "$.notArray", "$.notArray is not an array.")] + [TestCase("{\"obj\":{}}", "$.missing", "$.missing was not found in the JSON structure.")] + public void GetArrayLength_InvalidCases_Throws(string jsonStr, string jsonPathStr, string expectedMessage) + { + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + ReadOnlySpan jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + + var reader = new Utf8JsonReader(json.Span); + bool exceptionThrown = false; + try + { + reader.GetArrayLength(jsonPath); + } + catch (Exception ex) + { + Assert.AreEqual(expectedMessage, ex!.Message); + exceptionThrown = true; + } + Assert.IsTrue(exceptionThrown); + } + + [TestCase("$", "/")] + [TestCase("$[0]", "/0")] + [TestCase("$.x", "/x")] + [TestCase("$['x']", "/x")] + [TestCase("$[\"x\"]", "/x")] + [TestCase("$.x.['y'][4].[\"z\"][12].a", "/x/y/4/z/12/a")] + [TestCase("$.x.['y'][4].[\"z~z/\"][12].a", "/x/y/4/z~0z~1/12/a")] + [TestCase("$.~", "/~0")] + [TestCase("$.~x", "/~0x")] + [TestCase("$.x~", "/x~0")] + [TestCase("$['a~b/c'].x", "/a~0b~1c/x")] + public void ConvertToJsonPointer(string jsonPathStr, string expectedJsonPointer) + { + byte[] jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + Span jsonPointerSpan = stackalloc byte[jsonPath.Length << 1]; + int bytesWritten = jsonPath.ConvertToJsonPointer(jsonPointerSpan); + Assert.AreEqual(expectedJsonPointer, Encoding.UTF8.GetString(jsonPointerSpan.Slice(0, bytesWritten).ToArray())); + } + + [TestCase("$", "/-")] + [TestCase("$[0]", "/0/-")] + [TestCase("$.x", "/x/-")] + [TestCase("$['x']", "/x/-")] + [TestCase("$[\"x\"]", "/x/-")] + [TestCase("$.x.['y'][4].[\"z\"][12].a", "/x/y/4/z/12/a/-")] + [TestCase("$.x.['y'][4].[\"z~z/\"][12].a", "/x/y/4/z~0z~1/12/a/-")] + [TestCase("$.~", "/~0/-")] + [TestCase("$.~x", "/~0x/-")] + [TestCase("$.x~", "/x~0/-")] + [TestCase("$['a~b/c'].x", "/a~0b~1c/x/-")] + public void ConvertToJsonPointerAppend(string jsonPathStr, string expectedJsonPointer) + { + byte[] jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + Span jsonPointerSpan = stackalloc byte[jsonPath.Length << 1]; + int bytesWritten = jsonPath.ConvertToJsonPointer(jsonPointerSpan, true); + Assert.AreEqual(expectedJsonPointer, Encoding.UTF8.GetString(jsonPointerSpan.Slice(0, bytesWritten).ToArray())); + } + + [TestCase("{\"x\":1,\"y\":2,\"z\":3}", "$.x", "{\"y\":2,\"z\":3}")] + [TestCase("{\"x\":1,\"y\":2,\"z\":3}", "$.y", "{\"x\":1,\"z\":3}")] + [TestCase("{\"x\":1,\"y\":2,\"z\":3}", "$.z", "{\"x\":1,\"y\":2}")] + [TestCase("{\"x\":1,\"y\":2}", "$.x", "{\"y\":2}")] + [TestCase("{\"x\":1,\"y\":2}", "$.y", "{\"x\":1}")] + [TestCase("{\"x\":1, \"y\":2 \r\n}", "$.y", "{\"x\":1}")] + [TestCase("{\"x\":{\"y\":1},\"a\":{\"b\":2}}", "$.x", "{\"a\":{\"b\":2}}")] + [TestCase("{\"x\":{\"y\":1},\"a\":{\"b\":2}}", "$.x.y", "{\"x\":{},\"a\":{\"b\":2}}")] + [TestCase("{\"x\":{\"y\":1},\"a\":{\"b\":2}}", "$.a", "{\"x\":{\"y\":1}}")] + [TestCase("{\"x\":{\"y\":1},\"a\":{\"b\":2}}", "$.a.b", "{\"x\":{\"y\":1},\"a\":{}}")] + [TestCase("{\"arr\":[1,2, \r\n3]}", "$.arr[2]", "{\"arr\":[1,2]}")] + [TestCase("{\"arr\":[1,2, \r\n3 ]}", "$.arr[2]", "{\"arr\":[1,2]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[1]", "{\"arr\":[1,3]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[0]", "{\"arr\":[2,3]}")] + [TestCase("{\"arr\":[1,2,3]}", "$.arr[2]", "{\"arr\":[1,2]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$.arr[0]", "{\"arr\":[[3,4],[5,6]]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$.arr[1]", "{\"arr\":[[1,2],[5,6]]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$.arr[2]", "{\"arr\":[[1,2],[3,4]]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$.arr[0][0]", "{\"arr\":[[2],[3,4],[5,6]]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$.arr[1][1]", "{\"arr\":[[1,2],[3],[5,6]]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$.arr[2][0]", "{\"arr\":[[1,2],[3,4],[6]]}")] + [TestCase("{\"arr\":[[1,2],[3,4],[5,6]]}", "$", "")] + public void RemoveAt(string jsonStr, string jsonPathStr, string expectedJsonPointer) + { + byte[] jsonPath = Encoding.UTF8.GetBytes(jsonPathStr); + ReadOnlyMemory json = Encoding.UTF8.GetBytes(jsonStr); + var newJson = json.Remove(jsonPath.AsSpan()); + Assert.AreEqual(expectedJsonPointer, Encoding.UTF8.GetString(newJson)); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathReaderTests.cs b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathReaderTests.cs new file mode 100644 index 000000000000..83a38d63894e --- /dev/null +++ b/sdk/core/System.ClientModel/tests/internal/ModelReaderWriter/JsonPathReaderTests.cs @@ -0,0 +1,551 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Text; +using NUnit.Framework; + +namespace System.ClientModel.Tests.Internal.ModelReaderWriterTests +{ + public class JsonPathReaderTests + { + [TestCase("$[\"\"foo\"]", "\"foo")] + [TestCase("$[\"f\"oo\"]", "f\"oo")] + [TestCase("$[\"foo\"\"]", "foo\"")] + [TestCase("$['\'foo']", "'foo")] + [TestCase("$['f\'oo']", "f'oo")] + [TestCase("$['foo\'']", "foo'")] + [TestCase("$.\"foo", "\"foo")] + [TestCase("$.f\"oo", "f\"oo")] + [TestCase("$.foo\"", "foo\"")] + [TestCase("$.\'foo", "'foo")] + [TestCase("$.f\'oo", "f'oo")] + [TestCase("$.foo\'", "foo'")] + [TestCase("$['foo bar']", "foo bar")] + [TestCase("$['foo-bar']", "foo-bar")] + [TestCase("$['foo_bar']", "foo_bar")] + [TestCase("$['foo.bar']", "foo.bar")] + [TestCase("$['foo,bar']", "foo,bar")] + [TestCase("$['foo:bar']", "foo:bar")] + [TestCase("$['foo;bar']", "foo;bar")] + [TestCase("$['foo/bar']", "foo/bar")] + [TestCase("$['foo\\\\bar']", "foo\\\\bar")] + [TestCase("$['foo/bar\\\\baz']", "foo/bar\\\\baz")] + [TestCase("$['foo[bar]']", "foo[bar]")] + [TestCase("$['foo{bar}']", "foo{bar}")] + [TestCase("$['foo(bar)']", "foo(bar)")] + [TestCase("$['foo@bar']", "foo@bar")] + [TestCase("$['foo#bar']", "foo#bar")] + [TestCase("$['foo$bar']", "foo$bar")] + [TestCase("$['foo%bar']", "foo%bar")] + [TestCase("$['foo^bar']", "foo^bar")] + [TestCase("$['foo&bar']", "foo&bar")] + [TestCase("$['foo*bar']", "foo*bar")] + [TestCase("$['foo!bar']", "foo!bar")] + [TestCase("$['foo?bar']", "foo?bar")] + [TestCase("$['foo=bar']", "foo=bar")] + [TestCase("$['foo']", "foo")] + [TestCase("$['foo|bar']", "foo|bar")] + [TestCase("$['foo~bar']", "foo~bar")] + [TestCase("$['foo`bar']", "foo`bar")] + [TestCase("$['foo\u263Abar']", "foo\u263Abar")] + [TestCase("$['']", "")] + [TestCase("$.''", "''")] + [TestCase("$['foo''bar']", "foo''bar")] + [TestCase("$['foo\"bar']", "foo\"bar")] + [TestCase("$['foo\nbar']", "foo\nbar")] + [TestCase("$['foo\tbar']", "foo\tbar")] + [TestCase("$['foo\rbar']", "foo\rbar")] + public void Read_SpecialCharacter(string jsonPath, string propertyName) + { + JsonPathReader reader = new(jsonPath); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property + Assert.IsTrue(reader.Read(), "Expected to read Property token"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual(propertyName, Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + } + + [Test] + public void Read_RootToken_ReturnsCorrectly() + { + var reader = new JsonPathReader("$"u8); + + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + Assert.AreEqual(0, reader.Current.TokenStartIndex); + Assert.AreEqual("$", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [Test] + public void Read_SimpleProperty_AllTokensCorrect() + { + var reader = new JsonPathReader("$.property"u8); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // Property separator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property + Assert.IsTrue(reader.Read(), "Expected to read Property token"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("property", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // End + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [Test] + public void Read_QuotedString_ReturnsCorrectly() + { + var reader = new JsonPathReader("$['quoted-property']"u8); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // Property separator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property + Assert.IsTrue(reader.Read(), "Expected to read Property token"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("quoted-property", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [Test] + public void Read_IntegerPropertyName() + { + var reader = new JsonPathReader("$['123']"u8); + + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + Assert.IsTrue(reader.Read(), "Expected to read Property token"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("123", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + } + + [Test] + public void Read_UnterminatedQuote_ThrowsException() + { + var reader = new JsonPathReader("$['unterminated"u8); + + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // Property separator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + bool exceptionThrown = false; + try + { + reader.Read(); + } + catch (FormatException ex) + { + exceptionThrown = true; + Assert.AreEqual("Unterminated quoted string in JsonPath", ex.Message); + } + + Assert.IsTrue(exceptionThrown, "Expected ArgumentException for unterminated quote"); + } + + [Test] + public void Read_ComplexPath_AllTokensCorrect() + { + var reader = new JsonPathReader("$.items[0].name"u8); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property items + Assert.IsTrue(reader.Read(), "Expected to read Property token (items)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("items", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // ArrayIndex 0 + Assert.IsTrue(reader.Read(), "Expected to read Number token (0)"); + Assert.AreEqual(JsonPathTokenType.ArrayIndex, reader.Current.TokenType); + Assert.AreEqual("0", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property name + Assert.IsTrue(reader.Read(), "Expected to read Property token (name)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("name", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // End + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [Test] + public void Read_UnsupportedFilterExpression_ParsedAsProperty() + { + var reader = new JsonPathReader("$[?(test"u8); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + bool exceptionThrown = false; + try + { + reader.Read(); + } + catch (FormatException ex) + { + exceptionThrown = true; + Assert.AreEqual("Invalid JsonPath syntax at position 2: expected a property or positive array index after '['", ex.Message); + } + Assert.IsTrue(exceptionThrown, "Expected FormatException for unsupported filter expression"); + } + + [Test] + public void Read_WildcardOperator_ParsedAsProperty() + { + var reader = new JsonPathReader("$.*"u8); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property * + Assert.IsTrue(reader.Read(), "Expected to read Property token (*)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("*", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [Test] + public void Current_InitialState_IsDefault() + { + var reader = new JsonPathReader("$.test"u8); + + // Compare properties individually since JsonPathToken doesn't have custom equality + var defaultToken = default(JsonPathToken); + Assert.AreEqual(defaultToken.TokenType, reader.Current.TokenType); + Assert.AreEqual(defaultToken.TokenStartIndex, reader.Current.TokenStartIndex); + Assert.IsTrue(reader.Current.ValueSpan.SequenceEqual(defaultToken.ValueSpan), + $"Expected empty span but got '{Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())}'"); + } + + [Test] + public void Read_PropertyWithNumbers_ParsedCorrectly() + { + var reader = new JsonPathReader("$.x.2.y"u8); + + // Root $ + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + Assert.AreEqual(0, reader.Current.TokenStartIndex); + Assert.AreEqual("$", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // PropertySeparator . + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property x + Assert.IsTrue(reader.Read(), "Expected to read Property token (x)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("x", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // PropertySeparator . + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property 2 + Assert.IsTrue(reader.Read(), "Expected to read Property token (2)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("2", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // PropertySeparator . + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property y + Assert.IsTrue(reader.Read(), "Expected to read Property token (y)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("y", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // End + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [TestCase("$.x", "$.x")] + [TestCase("$[0]", "$[0]")] + [TestCase("$.x[0]", "$.x")] + [TestCase("$[0].x", "$[0]")] + [TestCase("$['complex path']", "$['complex path']")] + [TestCase("$.x['complex path']", "$.x")] + [TestCase("$.complex.path.here", "$.complex")] + [TestCase("$", "$")] + [TestCase("$.property", "$.property")] + [TestCase("$['property.with.dot'].x", "$['property.with.dot']")] + public void GetFirstProperty(string jsonPath, string expected) + { + var reader = new JsonPathReader(jsonPath); + + var result = reader.GetFirstProperty(); + Assert.AreEqual(expected, Encoding.UTF8.GetString(result.ToArray())); + } + + [Test] + public void PropertyNameWithDot() + { + var reader = new JsonPathReader("$.foo['property.with.dot'].x"u8); + + // Root + Assert.IsTrue(reader.Read(), "Expected to read Root token"); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property foo + Assert.IsTrue(reader.Read(), "Expected to read Property token (foo)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("foo", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property property.with.dot + Assert.IsTrue(reader.Read(), "Expected to read Property token (property.with.dot)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("property.with.dot", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // PropertySeparator + Assert.IsTrue(reader.Read(), "Expected to read PropertySeparator token"); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Property x + Assert.IsTrue(reader.Read(), "Expected to read Property token (x)"); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("x", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + + // End + Assert.IsTrue(reader.Read(), "Expected to read End token"); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + + Assert.IsFalse(reader.Read(), "Should not be able to read past End token"); + } + + [Test] + public void Peek_DoesNotAdvance() + { + var reader = new JsonPathReader("$.x.y"u8); + + // First token + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // Peek property separator + var peek1 = reader.Peek(); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, peek1.TokenType); + // After peek, current still root + Assert.AreEqual(JsonPathTokenType.Root, reader.Current.TokenType); + + // Read now moves to separator + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + + // Peek property 'x' + var peek2 = reader.Peek(); + Assert.AreEqual(JsonPathTokenType.Property, peek2.TokenType); + Assert.AreEqual("x", Encoding.UTF8.GetString(peek2.ValueSpan.ToArray())); + + // Read property 'x' + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonPathTokenType.Property, reader.Current.TokenType); + Assert.AreEqual("x", Encoding.UTF8.GetString(reader.Current.ValueSpan.ToArray())); + } + + [Test] + public void Advance_ByPrefix() + { + var full = new JsonPathReader("$.a.b.c"u8); + var prefixReader = new JsonPathReader("$.a.b"u8); + + // Consume prefix in prefixReader fully (simulate external usage) + while (prefixReader.Read() && prefixReader.Current.TokenType != JsonPathTokenType.End) + { } + + // Now advance 'full' reader using parsed prefix slice + var slice = Encoding.UTF8.GetBytes("$.a.b"); + Assert.IsTrue(full.Advance(slice)); + + // After advance, next token should continue at '.c' + Assert.IsTrue(full.Read()); // Expect PropertySeparator + Assert.AreEqual(JsonPathTokenType.PropertySeparator, full.Current.TokenType); + Assert.IsTrue(full.Read()); // Property c + Assert.AreEqual(JsonPathTokenType.Property, full.Current.TokenType); + Assert.AreEqual("c", Encoding.UTF8.GetString(full.Current.ValueSpan.ToArray())); + } + + [Test] + public void Advance_PrefixMismatch_ReturnsFalse() + { + var full = new JsonPathReader("$.a.b.c"u8); + Assert.IsFalse(full.Advance("$.x"u8)); + } + + [Test] + public void Read_Invalid_UnclosedBracket_Throws() + { + var reader = new JsonPathReader("$['foo'"u8); + Assert.IsTrue(reader.Read()); // $ + Assert.IsTrue(reader.Read()); // separator + bool exceptionThrown = false; + try + { + reader.Read(); + } + catch (FormatException ex) + { + exceptionThrown = true; + Assert.AreEqual("Unterminated quoted string in JsonPath", ex.Message); + } + Assert.IsTrue(exceptionThrown, "Expected FormatException for unterminated quoted string"); + } + + [Test] + public void Read_Invalid_OpenBracket_NoFollowup_Throws() + { + var reader = new JsonPathReader("$["u8); + Assert.IsTrue(reader.Read()); // root + bool exceptionThrown = false; + try + { + reader.Read(); + } + catch (FormatException ex) + { + exceptionThrown = true; + Assert.AreEqual("Invalid JsonPath syntax at position 2: expected a property or positive array index after '['", ex.Message); + } + Assert.IsTrue(exceptionThrown, "Expected FormatException for open bracket with no follow-up"); + } + + [Test] + public void Read_Invalid_UnclosedArrayIndex_Throws() + { + var reader = new JsonPathReader("$[123"u8); + Assert.IsTrue(reader.Read()); // root + bool exceptionThrown = false; + try + { + reader.Read(); + } + catch (FormatException ex) + { + exceptionThrown = true; + Assert.AreEqual("Invalid JsonPath syntax at position 5: expected ']' after number", ex.Message); + } + Assert.IsTrue(exceptionThrown, "Expected FormatException for unclosed array index"); + } + + [Test] + public void Read_Invalid_NegativeIndex_Throws() + { + var reader = new JsonPathReader("$.arr[-1]"u8); + Assert.IsTrue(reader.Read()); // root + Assert.IsTrue(reader.Read()); // separator + Assert.IsTrue(reader.Read()); // property arr + bool exceptionThrown = false; + try + { + reader.Read(); + } + catch (FormatException ex) + { + exceptionThrown = true; + Assert.AreEqual("Invalid JsonPath syntax at position 6: expected a property or positive array index after '['", ex.Message); + } + Assert.IsTrue(exceptionThrown, "Expected FormatException for negative array index"); + } + + [Test] + public void Read_TrailingDot_ParsesSeparatorThenEnd() + { + var reader = new JsonPathReader("$.foo."u8); + Assert.IsTrue(reader.Read()); // $ + Assert.IsTrue(reader.Read()); // . + Assert.IsTrue(reader.Read()); // foo + Assert.IsTrue(reader.Read()); // trailing . + Assert.AreEqual(JsonPathTokenType.PropertySeparator, reader.Current.TokenType); + Assert.IsTrue(reader.Read()); + Assert.AreEqual(JsonPathTokenType.End, reader.Current.TokenType); + } + + [Test] + public void Peek_AfterEnd_ReturnsEnd() + { + var r = new JsonPathReader("$"u8); + Assert.IsTrue(r.Read()); + Assert.IsTrue(r.Read()); // End + Assert.AreEqual(JsonPathTokenType.End, r.Current.TokenType); + var pk = r.Peek(); + Assert.AreEqual(JsonPathTokenType.End, pk.TokenType); + var pk2 = r.Peek(); + Assert.AreEqual(JsonPathTokenType.End, pk2.TokenType); + } + } +}