From c34f0d490b09408bdb93a3f6f86b9c842c498a25 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Thu, 27 Aug 2020 14:37:16 -0700 Subject: [PATCH] Add JsonPatchDocument (#14618) --- sdk/core/Azure.Core.Experimental/CHANGELOG.md | 3 + .../Azure.Core.Experimental.netstandard2.0.cs | 11 ++ .../src/JsonPatchDocument.cs | 123 ++++++++++++++++++ .../src/JsonPatchOperation.cs | 21 +++ .../src/JsonPatchOperationKind.cs | 27 ++++ .../tests/JsonPatchDocumentTests.cs | 81 ++++++++++++ 6 files changed, 266 insertions(+) create mode 100644 sdk/core/Azure.Core.Experimental/src/JsonPatchDocument.cs create mode 100644 sdk/core/Azure.Core.Experimental/src/JsonPatchOperation.cs create mode 100644 sdk/core/Azure.Core.Experimental/src/JsonPatchOperationKind.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/JsonPatchDocumentTests.cs diff --git a/sdk/core/Azure.Core.Experimental/CHANGELOG.md b/sdk/core/Azure.Core.Experimental/CHANGELOG.md index e5fc71c994b4..d7bc9e890f56 100644 --- a/sdk/core/Azure.Core.Experimental/CHANGELOG.md +++ b/sdk/core/Azure.Core.Experimental/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.1.0-preview.5 (Unreleased) +### Added +- `JsonPatchDocument` type to represent JSON Path document. + ## 0.1.0-preview.4 (2020-08-18) ### Fixed diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 8558d0adffcf..07f7bc5d3501 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -93,6 +93,17 @@ public DynamicJson(System.Text.Json.JsonElement element) { } public override string ToString() { throw null; } public void WriteTo(System.Text.Json.Utf8JsonWriter writer) { } } + public partial class JsonPatchDocument + { + public JsonPatchDocument() { } + public void AppendAdd(string path, string rawJsonValue) { } + public void AppendCopy(string from, string path) { } + public void AppendMove(string from, string path) { } + public void AppendRemove(string path) { } + public void AppendReplace(string path, string rawJsonValue) { } + public void AppendTest(string path, string rawJsonValue) { } + public override string ToString() { throw null; } + } } namespace Azure.Core.GeoJson { diff --git a/sdk/core/Azure.Core.Experimental/src/JsonPatchDocument.cs b/sdk/core/Azure.Core.Experimental/src/JsonPatchDocument.cs new file mode 100644 index 000000000000..ad317617f449 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/JsonPatchDocument.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using System.IO; +using System.Text; +using System.Text.Json; +using Azure.Core.JsonPatch; + +namespace Azure.Core +{ + /// + /// Represents a JSON Patch document. + /// + public class JsonPatchDocument + { + internal Collection Operations { get; } + + /// + /// Initializes a new instance of + /// + public JsonPatchDocument() + { + Operations = new Collection(); + } + + /// + /// Appends an "add" operation to this . + /// + /// The path to apply the addition to. + /// The raw JSON value to add to the path. + public void AppendAdd(string path, string rawJsonValue) + { + Operations.Add(new JsonPatchOperation(JsonPatchOperationKind.Add, path, null, rawJsonValue)); + } + + /// + /// Appends a "replace" operation to this . + /// + /// The path to replace. + /// The raw JSON value to replace with. + public void AppendReplace(string path, string rawJsonValue) + { + Operations.Add(new JsonPatchOperation(JsonPatchOperationKind.Replace, path, null, rawJsonValue)); + } + + /// + /// Appends a "copy" operation to this . + /// + /// The path to copy from. + /// The path to copy to. + public void AppendCopy(string from, string path) + { + Operations.Add(new JsonPatchOperation(JsonPatchOperationKind.Copy, path, from, null)); + } + + /// + /// Appends a "move" operation to this . + /// + /// The path to move from. + /// The path to move to. + public void AppendMove(string from, string path) + { + Operations.Add(new JsonPatchOperation(JsonPatchOperationKind.Move, path, from, null)); + } + + /// + /// Appends a "remove" operation to this . + /// + /// The path to remove. + public void AppendRemove(string path) + { + Operations.Add(new JsonPatchOperation(JsonPatchOperationKind.Remove, path, null, null)); + } + + /// + /// Appends a "test" operation to this . + /// + /// The path to test. + /// The raw JSON value to test against. + public void AppendTest(string path, string rawJsonValue) + { + Operations.Add(new JsonPatchOperation(JsonPatchOperationKind.Test, path, null, rawJsonValue)); + } + + /// + /// Returns a formatted JSON string representation of this . + /// + /// A formatted JSON string representation of this . + public override string ToString() + { + using var memoryStream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(memoryStream)) + { + WriteTo(writer); + } + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + + private void WriteTo(Utf8JsonWriter writer) + { + writer.WriteStartArray(); + foreach (var operation in Operations) + { + writer.WriteStartObject(); + writer.WriteString("op", operation.Kind.ToString()); + if (operation.From != null) + { + writer.WriteString("from", operation.From); + } + writer.WriteString("path", operation.Path); + if (operation.RawJsonValue != null) + { + using var parsedValue = JsonDocument.Parse(operation.RawJsonValue); + writer.WritePropertyName("value"); + parsedValue.WriteTo(writer); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core.Experimental/src/JsonPatchOperation.cs b/sdk/core/Azure.Core.Experimental/src/JsonPatchOperation.cs new file mode 100644 index 000000000000..8e1b545aa65a --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/JsonPatchOperation.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core.JsonPatch +{ + internal class JsonPatchOperation + { + public JsonPatchOperation(JsonPatchOperationKind kind, string path, string? from, string? rawJsonValue) + { + Kind = kind; + Path = path; + From = from; + RawJsonValue = rawJsonValue; + } + + public JsonPatchOperationKind Kind { get; } + public string Path { get; } + public string? From { get; } + public string? RawJsonValue { get; } + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core.Experimental/src/JsonPatchOperationKind.cs b/sdk/core/Azure.Core.Experimental/src/JsonPatchOperationKind.cs new file mode 100644 index 000000000000..d2a94874d435 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/JsonPatchOperationKind.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core.JsonPatch +{ + internal readonly struct JsonPatchOperationKind + { + private readonly string _operation; + + public JsonPatchOperationKind(string operation) + { + _operation = operation; + } + + public static JsonPatchOperationKind Add { get; } = new JsonPatchOperationKind("add"); + public static JsonPatchOperationKind Remove { get; } = new JsonPatchOperationKind("remove"); + public static JsonPatchOperationKind Replace { get; } = new JsonPatchOperationKind("replace"); + public static JsonPatchOperationKind Move { get; } = new JsonPatchOperationKind("move"); + public static JsonPatchOperationKind Copy { get; } = new JsonPatchOperationKind("copy"); + public static JsonPatchOperationKind Test { get; } = new JsonPatchOperationKind("test"); + + public override string ToString() + { + return _operation; + } + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core.Experimental/tests/JsonPatchDocumentTests.cs b/sdk/core/Azure.Core.Experimental/tests/JsonPatchDocumentTests.cs new file mode 100644 index 000000000000..9578472e7e1d --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/JsonPatchDocumentTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.JsonPatch; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class JsonPatchDocumentTests + { + [Test] + public void AddIsSerializedCorrectly() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendAdd("/a/b/c","[ \"foo\", \"bar\" ]"); + Assert.AreEqual(document.ToString(), "[{\"op\":\"add\",\"path\":\"/a/b/c\",\"value\":[\"foo\",\"bar\"]}]"); + } + + [Test] + public void ReplaceIsSerializedCorrectly() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendReplace("/a/b/c","[ \"foo\", \"bar\" ]"); + Assert.AreEqual(document.ToString(), "[{\"op\":\"replace\",\"path\":\"/a/b/c\",\"value\":[\"foo\",\"bar\"]}]"); + } + + [Test] + public void TestIsSerializedCorrectly() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendTest("/a/b/c","[ \"foo\", \"bar\" ]"); + Assert.AreEqual(document.ToString(), "[{\"op\":\"test\",\"path\":\"/a/b/c\",\"value\":[\"foo\",\"bar\"]}]"); + } + + [Test] + public void RemoveIsSerializedCorrectly() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendRemove("/a/b/c"); + Assert.AreEqual(document.ToString(), "[{\"op\":\"remove\",\"path\":\"/a/b/c\"}]"); + } + + [Test] + public void MoveIsSerializedCorrectly() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendMove("/a/b/c", "/a/b/d"); + Assert.AreEqual(document.ToString(), "[{\"op\":\"move\",\"from\":\"/a/b/c\",\"path\":\"/a/b/d\"}]"); + } + + [Test] + public void CopyIsSerializedCorrectly() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendCopy("/a/b/c", "/a/b/d"); + Assert.AreEqual(document.ToString(), "[{\"op\":\"copy\",\"from\":\"/a/b/c\",\"path\":\"/a/b/d\"}]"); + } + + [Test] + public void MultipleOperationsSerializedInOrder() + { + JsonPatchDocument document = new JsonPatchDocument(); + document.AppendTest("/a/b/c","\"foo\""); + document.AppendAdd("/a/b/c","42"); + document.AppendReplace("/a/b/c","[ \"foo\", \"bar\" ]"); + document.AppendRemove("/a/b/c"); + document.AppendMove("/a/b/c", "/a/b/d"); + document.AppendCopy("/a/b/c", "/a/b/d"); + + Assert.AreEqual(document.ToString(), + "[" + + "{\"op\":\"test\",\"path\":\"/a/b/c\",\"value\":\"foo\"}," + + "{\"op\":\"add\",\"path\":\"/a/b/c\",\"value\":42}," + + "{\"op\":\"replace\",\"path\":\"/a/b/c\",\"value\":[\"foo\",\"bar\"]}," + + "{\"op\":\"remove\",\"path\":\"/a/b/c\"}," + + "{\"op\":\"move\",\"from\":\"/a/b/c\",\"path\":\"/a/b/d\"}," + + "{\"op\":\"copy\",\"from\":\"/a/b/c\",\"path\":\"/a/b/d\"}" + + "]"); + } + } +} \ No newline at end of file