diff --git a/src/NRedisStack/Json/DataTypes/KeyValuePath.cs b/src/NRedisStack/Json/DataTypes/KeyValuePath.cs index 52d4009d..252b639e 100644 --- a/src/NRedisStack/Json/DataTypes/KeyValuePath.cs +++ b/src/NRedisStack/Json/DataTypes/KeyValuePath.cs @@ -2,22 +2,17 @@ namespace NRedisStack.Json.DataTypes; -public struct KeyValuePath +public struct KeyPathValue { public string Key { get; set; } - public object Value { get; set; } public string Path { get; set; } + public object Value { get; set; } - public KeyValuePath(string key, object value, string path = "$") + public KeyPathValue(string key, string path, object value) { - if (key == null || value == null) - { - throw new ArgumentNullException("Key and value cannot be null."); - } - Key = key; - Value = value; Path = path; + Value = value; } public string[] ToArray() { diff --git a/src/NRedisStack/Json/IJsonCommands.cs b/src/NRedisStack/Json/IJsonCommands.cs index cc6fd3ee..bed3e52b 100644 --- a/src/NRedisStack/Json/IJsonCommands.cs +++ b/src/NRedisStack/Json/IJsonCommands.cs @@ -209,11 +209,31 @@ public interface IJsonCommands /// /// Sets or updates the JSON value of one or more keys. /// - /// The key, The value to set and + /// The key, The value to set and /// The path to set within the key, must be > 1 /// The disposition of the command /// - bool MSet(KeyValuePath[] keyValuePathList); + bool MSet(KeyPathValue[] KeyPathValueList); + + /// + /// Sets or updates the JSON value at a path. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// The disposition of the command + /// + bool Merge(RedisKey key, RedisValue path, RedisValue json); + + /// + /// Sets or updates the JSON value at a path. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// The disposition of the command + /// + bool Merge(RedisKey key, RedisValue path, object obj); /// /// Sets or updates the JSON value of one or more keys. @@ -242,7 +262,8 @@ public interface IJsonCommands /// The key to append to. /// The path of the string(s) to append to. /// The value to append. - /// The new length of the string(s) appended to, those lengths will be null if the path did not resolve ot a string. + /// The new length of the string(s) appended to, those lengths + /// will be null if the path did not resolve ot a string. /// long?[] StrAppend(RedisKey key, string value, string? path = null); @@ -251,7 +272,8 @@ public interface IJsonCommands /// /// The key of the json object. /// The path of the string(s) within the json object. - /// The length of the string(s) appended to, those lengths will be null if the path did not resolve ot a string. + /// The length of the string(s) appended to, those lengths + /// will be null if the path did not resolve ot a string. /// public long?[] StrLen(RedisKey key, string? path = null); diff --git a/src/NRedisStack/Json/IJsonCommandsAsync.cs b/src/NRedisStack/Json/IJsonCommandsAsync.cs index 2ad45a49..81c1343f 100644 --- a/src/NRedisStack/Json/IJsonCommandsAsync.cs +++ b/src/NRedisStack/Json/IJsonCommandsAsync.cs @@ -209,11 +209,31 @@ public interface IJsonCommandsAsync /// /// Sets or updates the JSON value of one or more keys. /// - /// The key, The value to set and + /// The key, The value to set and /// The path to set within the key, must be > 1 /// The disposition of the command /// - Task MSetAsync(KeyValuePath[] keyValuePathList); + Task MSetAsync(KeyPathValue[] KeyPathValueList); + + /// + /// Sets or updates the JSON value at a path. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// The disposition of the command + /// + Task MergeAsync(RedisKey key, RedisValue path, RedisValue json); + + /// + /// Sets or updates the JSON value at a path. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// The disposition of the command + /// + Task MergeAsync(RedisKey key, RedisValue path, object obj); /// /// Set json file from the provided file Path. diff --git a/src/NRedisStack/Json/JsonCommandBuilder.cs b/src/NRedisStack/Json/JsonCommandBuilder.cs index 48211320..b5505100 100644 --- a/src/NRedisStack/Json/JsonCommandBuilder.cs +++ b/src/NRedisStack/Json/JsonCommandBuilder.cs @@ -29,15 +29,20 @@ public static SerializedCommand Set(RedisKey key, RedisValue path, RedisValue js }; } - public static SerializedCommand MSet(KeyValuePath[] keyValuePathList) + public static SerializedCommand MSet(KeyPathValue[] KeyPathValueList) { - if (keyValuePathList.Length < 1) - throw new ArgumentOutOfRangeException(nameof(keyValuePathList)); + if (KeyPathValueList.Length < 1) + throw new ArgumentOutOfRangeException(nameof(KeyPathValueList)); - var args = keyValuePathList.SelectMany(x => x.ToArray()).ToArray(); + var args = KeyPathValueList.SelectMany(x => x.ToArray()).ToArray(); return new SerializedCommand(JSON.MSET, args); } + public static SerializedCommand Merge(RedisKey key, RedisValue path, RedisValue json) + { + return new SerializedCommand(JSON.MERGE, key, path, json); + } + public static SerializedCommand StrAppend(RedisKey key, string value, string? path = null) { if (path == null) diff --git a/src/NRedisStack/Json/JsonCommands.cs b/src/NRedisStack/Json/JsonCommands.cs index 876db349..d1e588f7 100644 --- a/src/NRedisStack/Json/JsonCommands.cs +++ b/src/NRedisStack/Json/JsonCommands.cs @@ -41,9 +41,22 @@ public bool Set(RedisKey key, RedisValue path, RedisValue json, When when = When } /// - public bool MSet(KeyValuePath[] keyValuePathList) + public bool MSet(KeyPathValue[] KeyPathValueList) { - return _db.Execute(JsonCommandBuilder.MSet(keyValuePathList)).OKtoBoolean(); + return _db.Execute(JsonCommandBuilder.MSet(KeyPathValueList)).OKtoBoolean(); + } + + /// + public bool Merge(RedisKey key, RedisValue path, RedisValue json) + { + return _db.Execute(JsonCommandBuilder.Merge(key, path, json)).OKtoBoolean(); + } + + /// + public bool Merge(RedisKey key, RedisValue path, object obj) + { + string json = JsonSerializer.Serialize(obj); + return _db.Execute(JsonCommandBuilder.Merge(key, path, json)).OKtoBoolean(); } /// diff --git a/src/NRedisStack/Json/JsonCommandsAsync.cs b/src/NRedisStack/Json/JsonCommandsAsync.cs index 3eb32026..c42fcbd5 100644 --- a/src/NRedisStack/Json/JsonCommandsAsync.cs +++ b/src/NRedisStack/Json/JsonCommandsAsync.cs @@ -144,9 +144,22 @@ public async Task SetAsync(RedisKey key, RedisValue path, RedisValue json, return (await _db.ExecuteAsync(JsonCommandBuilder.Set(key, path, json, when))).OKtoBoolean(); } - public async Task MSetAsync(KeyValuePath[] keyValuePathList) + public async Task MSetAsync(KeyPathValue[] KeyPathValueList) { - return (await _db.ExecuteAsync(JsonCommandBuilder.MSet(keyValuePathList))).OKtoBoolean(); + return (await _db.ExecuteAsync(JsonCommandBuilder.MSet(KeyPathValueList))).OKtoBoolean(); + } + + /// + public async Task MergeAsync(RedisKey key, RedisValue path, RedisValue json) + { + return (await _db.ExecuteAsync(JsonCommandBuilder.Merge(key, path, json))).OKtoBoolean(); + } + + /// + public async Task MergeAsync(RedisKey key, RedisValue path, object obj) + { + string json = JsonSerializer.Serialize(obj); + return (await _db.ExecuteAsync(JsonCommandBuilder.Merge(key, path, json))).OKtoBoolean(); } public async Task SetFromFileAsync(RedisKey key, RedisValue path, string filePath, When when = When.Always) diff --git a/src/NRedisStack/Json/Literals/Commands.cs b/src/NRedisStack/Json/Literals/Commands.cs index f748fae0..8ddbafe0 100644 --- a/src/NRedisStack/Json/Literals/Commands.cs +++ b/src/NRedisStack/Json/Literals/Commands.cs @@ -15,6 +15,7 @@ internal class JSON public const string FORGET = "JSON.FORGET"; public const string GET = "JSON.GET"; public const string MEMORY = "MEMORY"; + public const string MERGE = "JSON.MERGE"; public const string MSET = "JSON.MSET"; public const string MGET = "JSON.MGET"; public const string NUMINCRBY = "JSON.NUMINCRBY"; diff --git a/tests/NRedisStack.Tests/Json/JsonTests.cs b/tests/NRedisStack.Tests/Json/JsonTests.cs index 59f17e11..18a66392 100644 --- a/tests/NRedisStack.Tests/Json/JsonTests.cs +++ b/tests/NRedisStack.Tests/Json/JsonTests.cs @@ -726,7 +726,7 @@ public async Task GetAsync() } [Fact] - [Trait("Category","edge")] + [Trait("Category", "edge")] public void MSet() { IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); @@ -734,10 +734,10 @@ public void MSet() var key1 = keys[0]; var key2 = keys[1]; - KeyValuePath[] values = new[] + KeyPathValue[] values = new[] { - new KeyValuePath(key1, new { a = "hello" }), - new KeyValuePath(key2, new { a = "world" }) + new KeyPathValue(key1, "$", new { a = "hello" }), + new KeyPathValue(key2, "$", new { a = "world" }) }; commands.MSet(values) ; @@ -747,22 +747,21 @@ public void MSet() Assert.Equal("[\"world\"]", result[1].ToString()); // test errors: - Assert.Throws(() => commands.MSet(new KeyValuePath[0])); - + Assert.Throws(() => commands.MSet(new KeyPathValue[0])); } [Fact] - [Trait("Category","edge")] + [Trait("Category", "edge")] public async Task MSetAsync() { IJsonCommandsAsync commands = new JsonCommands(redisFixture.Redis.GetDatabase()); var keys = CreateKeyNames(2); var key1 = keys[0]; var key2 = keys[1]; - KeyValuePath[] values = new[] + KeyPathValue[] values = new[] { - new KeyValuePath(key1, new { a = "hello" }), - new KeyValuePath(key2, new { a = "world" }) + new KeyPathValue(key1, "$", new { a = "hello" }), + new KeyPathValue(key2, "$", new { a = "world" }) }; await commands.MSetAsync(values) ; @@ -772,14 +771,47 @@ await commands.MSetAsync(values) Assert.Equal("[\"world\"]", result[1].ToString()); // test errors: - await Assert.ThrowsAsync(async () => await commands.MSetAsync(new KeyValuePath[0])); + await Assert.ThrowsAsync(async () => await commands.MSetAsync(new KeyPathValue[0])); } [Fact] - public void TestKeyValuePathErrors() + [Trait("Category", "edge")] + public void Merge() { - Assert.Throws(() => new KeyValuePath(null!, new { a = "hello" })); - Assert.Throws(() => new KeyValuePath("key", null!) ); + // Create a connection to Redis + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + + Assert.True(commands.Set("test_merge", "$", new { person = new { name = "John Doe", age = 25, address = new {home = "123 Main Street"}, phone = "123-456-7890" } })); + Assert.True(commands.Merge("test_merge", "$", new { person = new { age = 30 } })); + Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"age\":30,\"address\":{\"home\":\"123 Main Street\"},\"phone\":\"123-456-7890\"}}", commands.Get("test_merge").ToString()); + + // Test with root path path $.a.b + Assert.True(commands.Merge("test_merge", "$.person.address", new {work = "Redis office"})); + Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"age\":30,\"address\":{\"home\":\"123 Main Street\",\"work\":\"Redis office\"},\"phone\":\"123-456-7890\"}}", commands.Get("test_merge").ToString()); + + // Test with null value to delete a value + Assert.True(commands.Merge("test_merge", "$.person", "{\"age\":null}")); + Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"phone\":\"123-456-7890\",\"address\":{\"home\":\"123 Main Street\",\"work\":\"Redis office\"}}}", commands.Get("test_merge").ToString()); + } + + [Fact] + [Trait("Category", "edge")] + public async Task MergeAsync() + { + // Create a connection to Redis + IJsonCommandsAsync commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + + Assert.True(await commands.SetAsync("test_merge", "$", new { person = new { name = "John Doe", age = 25, address = new {home = "123 Main Street"}, phone = "123-456-7890" } })); + Assert.True(await commands.MergeAsync("test_merge", "$", new { person = new { age = 30 } })); + Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"age\":30,\"address\":{\"home\":\"123 Main Street\"},\"phone\":\"123-456-7890\"}}", (await commands.GetAsync("test_merge")).ToString()); + + // Test with root path path $.a.b + Assert.True(await commands.MergeAsync("test_merge", "$.person.address", new {work = "Redis office"})); + Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"age\":30,\"address\":{\"home\":\"123 Main Street\",\"work\":\"Redis office\"},\"phone\":\"123-456-7890\"}}", (await commands.GetAsync("test_merge")).ToString()); + + // Test with null value to delete a value + Assert.True(await commands.MergeAsync("test_merge", "$.person", "{\"age\":null}")); + Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"phone\":\"123-456-7890\",\"address\":{\"home\":\"123 Main Street\",\"work\":\"Redis office\"}}}", (await commands.GetAsync("test_merge")).ToString()); } [Fact] @@ -1067,18 +1099,18 @@ public async Task TestSetFromDirectoryAsync() public void TestJsonCommandBuilder() { var getBuild1 = JsonCommandBuilder.Get("key", "indent", "newline", "space", "path"); - var getBuild2 = JsonCommandBuilder.Get("key",new string[]{"path1", "path2", "path3"}, "indent", "newline", "space"); - var expectedArgs1 = new object[] { "key", "INDENT", "indent", "NEWLINE","newline", "SPACE", "space", "path" }; + var getBuild2 = JsonCommandBuilder.Get("key", new string[] { "path1", "path2", "path3" }, "indent", "newline", "space"); + var expectedArgs1 = new object[] { "key", "INDENT", "indent", "NEWLINE", "newline", "SPACE", "space", "path" }; var expectedArgs2 = new object[] { "key", "INDENT", "indent", "NEWLINE", "newline", "SPACE", "space", "path1", "path2", "path3" }; - for(int i = 0; i < expectedArgs1.Length; i++) + for (int i = 0; i < expectedArgs1.Length; i++) { Assert.Equal(expectedArgs1[i].ToString(), getBuild1.Args[i].ToString()); } Assert.Equal("JSON.GET", getBuild1.Command); - for(int i = 0; i < expectedArgs2.Length; i++) + for (int i = 0; i < expectedArgs2.Length; i++) { Assert.Equal(expectedArgs2[i].ToString(), getBuild2.Args[i].ToString()); }