Skip to content

Commit 7d2b006

Browse files
shacharPashslorello89chayim
authored
Support JSON.MERGE Command (#132)
Co-authored-by: slorello89 <steve.lorello@redis.com> Co-authored-by: Chayim <chayim@users.noreply.github.com>
1 parent 524dfb0 commit 7d2b006

File tree

8 files changed

+142
-41
lines changed

8 files changed

+142
-41
lines changed

src/NRedisStack/Json/DataTypes/KeyValuePath.cs

+4-9
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,17 @@
22

33
namespace NRedisStack.Json.DataTypes;
44

5-
public struct KeyValuePath
5+
public struct KeyPathValue
66
{
77
public string Key { get; set; }
8-
public object Value { get; set; }
98
public string Path { get; set; }
9+
public object Value { get; set; }
1010

11-
public KeyValuePath(string key, object value, string path = "$")
11+
public KeyPathValue(string key, string path, object value)
1212
{
13-
if (key == null || value == null)
14-
{
15-
throw new ArgumentNullException("Key and value cannot be null.");
16-
}
17-
1813
Key = key;
19-
Value = value;
2014
Path = path;
15+
Value = value;
2116
}
2217
public string[] ToArray()
2318
{

src/NRedisStack/Json/IJsonCommands.cs

+26-4
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,31 @@ public interface IJsonCommands
209209
/// <summary>
210210
/// Sets or updates the JSON value of one or more keys.
211211
/// </summary>
212-
/// <param name="keyValuePathList">The key, The value to set and
212+
/// <param name="KeyPathValueList">The key, The value to set and
213213
/// The path to set within the key, must be > 1 </param>
214214
/// <returns>The disposition of the command</returns>
215215
/// <remarks><seealso href="https://redis.io/commands/json.mset"/></remarks>
216-
bool MSet(KeyValuePath[] keyValuePathList);
216+
bool MSet(KeyPathValue[] KeyPathValueList);
217+
218+
/// <summary>
219+
/// Sets or updates the JSON value at a path.
220+
/// </summary>
221+
/// <param name="key">The key.</param>
222+
/// <param name="path">The path to set within the key.</param>
223+
/// <param name="json">The value to set.</param>
224+
/// <returns>The disposition of the command</returns>
225+
/// <remarks><seealso href="https://redis.io/commands/json.merge"/></remarks>
226+
bool Merge(RedisKey key, RedisValue path, RedisValue json);
227+
228+
/// <summary>
229+
/// Sets or updates the JSON value at a path.
230+
/// </summary>
231+
/// <param name="key">The key.</param>
232+
/// <param name="path">The path to set within the key.</param>
233+
/// <param name="obj">The value to set.</param>
234+
/// <returns>The disposition of the command</returns>
235+
/// <remarks><seealso href="https://redis.io/commands/json.merge"/></remarks>
236+
bool Merge(RedisKey key, RedisValue path, object obj);
217237

218238
/// <summary>
219239
/// Sets or updates the JSON value of one or more keys.
@@ -242,7 +262,8 @@ public interface IJsonCommands
242262
/// <param name="key">The key to append to.</param>
243263
/// <param name="path">The path of the string(s) to append to.</param>
244264
/// <param name="value">The value to append.</param>
245-
/// <returns>The new length of the string(s) appended to, those lengths will be null if the path did not resolve ot a string.</returns>
265+
/// <returns>The new length of the string(s) appended to, those lengths
266+
/// will be null if the path did not resolve ot a string.</returns>
246267
/// <remarks><seealso href="https://redis.io/commands/json.strappend"/></remarks>
247268
long?[] StrAppend(RedisKey key, string value, string? path = null);
248269

@@ -251,7 +272,8 @@ public interface IJsonCommands
251272
/// </summary>
252273
/// <param name="key">The key of the json object.</param>
253274
/// <param name="path">The path of the string(s) within the json object.</param>
254-
/// <returns>The length of the string(s) appended to, those lengths will be null if the path did not resolve ot a string.</returns>
275+
/// <returns>The length of the string(s) appended to, those lengths
276+
/// will be null if the path did not resolve ot a string.</returns>
255277
/// <remarks><seealso href="https://redis.io/commands/json.strlen"/></remarks>
256278
public long?[] StrLen(RedisKey key, string? path = null);
257279

src/NRedisStack/Json/IJsonCommandsAsync.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,31 @@ public interface IJsonCommandsAsync
209209
/// <summary>
210210
/// Sets or updates the JSON value of one or more keys.
211211
/// </summary>
212-
/// <param name="keyValuePathList">The key, The value to set and
212+
/// <param name="KeyPathValueList">The key, The value to set and
213213
/// The path to set within the key, must be > 1 </param>
214214
/// <returns>The disposition of the command</returns>
215215
/// <remarks><seealso href="https://redis.io/commands/json.mset"/></remarks>
216-
Task<bool> MSetAsync(KeyValuePath[] keyValuePathList);
216+
Task<bool> MSetAsync(KeyPathValue[] KeyPathValueList);
217+
218+
/// <summary>
219+
/// Sets or updates the JSON value at a path.
220+
/// </summary>
221+
/// <param name="key">The key.</param>
222+
/// <param name="path">The path to set within the key.</param>
223+
/// <param name="json">The value to set.</param>
224+
/// <returns>The disposition of the command</returns>
225+
/// <remarks><seealso href="https://redis.io/commands/json.merge"/></remarks>
226+
Task<bool> MergeAsync(RedisKey key, RedisValue path, RedisValue json);
227+
228+
/// <summary>
229+
/// Sets or updates the JSON value at a path.
230+
/// </summary>
231+
/// <param name="key">The key.</param>
232+
/// <param name="path">The path to set within the key.</param>
233+
/// <param name="obj">The value to set.</param>
234+
/// <returns>The disposition of the command</returns>
235+
/// <remarks><seealso href="https://redis.io/commands/json.merge"/></remarks>
236+
Task<bool> MergeAsync(RedisKey key, RedisValue path, object obj);
217237

218238
/// <summary>
219239
/// Set json file from the provided file Path.

src/NRedisStack/Json/JsonCommandBuilder.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,20 @@ public static SerializedCommand Set(RedisKey key, RedisValue path, RedisValue js
2929
};
3030
}
3131

32-
public static SerializedCommand MSet(KeyValuePath[] keyValuePathList)
32+
public static SerializedCommand MSet(KeyPathValue[] KeyPathValueList)
3333
{
34-
if (keyValuePathList.Length < 1)
35-
throw new ArgumentOutOfRangeException(nameof(keyValuePathList));
34+
if (KeyPathValueList.Length < 1)
35+
throw new ArgumentOutOfRangeException(nameof(KeyPathValueList));
3636

37-
var args = keyValuePathList.SelectMany(x => x.ToArray()).ToArray();
37+
var args = KeyPathValueList.SelectMany(x => x.ToArray()).ToArray();
3838
return new SerializedCommand(JSON.MSET, args);
3939
}
4040

41+
public static SerializedCommand Merge(RedisKey key, RedisValue path, RedisValue json)
42+
{
43+
return new SerializedCommand(JSON.MERGE, key, path, json);
44+
}
45+
4146
public static SerializedCommand StrAppend(RedisKey key, string value, string? path = null)
4247
{
4348
if (path == null)

src/NRedisStack/Json/JsonCommands.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,22 @@ public bool Set(RedisKey key, RedisValue path, RedisValue json, When when = When
4141
}
4242

4343
/// <inheritdoc/>
44-
public bool MSet(KeyValuePath[] keyValuePathList)
44+
public bool MSet(KeyPathValue[] KeyPathValueList)
4545
{
46-
return _db.Execute(JsonCommandBuilder.MSet(keyValuePathList)).OKtoBoolean();
46+
return _db.Execute(JsonCommandBuilder.MSet(KeyPathValueList)).OKtoBoolean();
47+
}
48+
49+
/// <inheritdoc/>
50+
public bool Merge(RedisKey key, RedisValue path, RedisValue json)
51+
{
52+
return _db.Execute(JsonCommandBuilder.Merge(key, path, json)).OKtoBoolean();
53+
}
54+
55+
/// <inheritdoc/>
56+
public bool Merge(RedisKey key, RedisValue path, object obj)
57+
{
58+
string json = JsonSerializer.Serialize(obj);
59+
return _db.Execute(JsonCommandBuilder.Merge(key, path, json)).OKtoBoolean();
4760
}
4861

4962
/// <inheritdoc/>

src/NRedisStack/Json/JsonCommandsAsync.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,22 @@ public async Task<bool> SetAsync(RedisKey key, RedisValue path, RedisValue json,
144144
return (await _db.ExecuteAsync(JsonCommandBuilder.Set(key, path, json, when))).OKtoBoolean();
145145
}
146146

147-
public async Task<bool> MSetAsync(KeyValuePath[] keyValuePathList)
147+
public async Task<bool> MSetAsync(KeyPathValue[] KeyPathValueList)
148148
{
149-
return (await _db.ExecuteAsync(JsonCommandBuilder.MSet(keyValuePathList))).OKtoBoolean();
149+
return (await _db.ExecuteAsync(JsonCommandBuilder.MSet(KeyPathValueList))).OKtoBoolean();
150+
}
151+
152+
/// <inheritdoc/>
153+
public async Task<bool> MergeAsync(RedisKey key, RedisValue path, RedisValue json)
154+
{
155+
return (await _db.ExecuteAsync(JsonCommandBuilder.Merge(key, path, json))).OKtoBoolean();
156+
}
157+
158+
/// <inheritdoc/>
159+
public async Task<bool> MergeAsync(RedisKey key, RedisValue path, object obj)
160+
{
161+
string json = JsonSerializer.Serialize(obj);
162+
return (await _db.ExecuteAsync(JsonCommandBuilder.Merge(key, path, json))).OKtoBoolean();
150163
}
151164

152165
public async Task<bool> SetFromFileAsync(RedisKey key, RedisValue path, string filePath, When when = When.Always)

src/NRedisStack/Json/Literals/Commands.cs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal class JSON
1515
public const string FORGET = "JSON.FORGET";
1616
public const string GET = "JSON.GET";
1717
public const string MEMORY = "MEMORY";
18+
public const string MERGE = "JSON.MERGE";
1819
public const string MSET = "JSON.MSET";
1920
public const string MGET = "JSON.MGET";
2021
public const string NUMINCRBY = "JSON.NUMINCRBY";

tests/NRedisStack.Tests/Json/JsonTests.cs

+50-18
Original file line numberDiff line numberDiff line change
@@ -726,18 +726,18 @@ public async Task GetAsync()
726726
}
727727

728728
[Fact]
729-
[Trait("Category","edge")]
729+
[Trait("Category", "edge")]
730730
public void MSet()
731731
{
732732
IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase());
733733
var keys = CreateKeyNames(2);
734734
var key1 = keys[0];
735735
var key2 = keys[1];
736736

737-
KeyValuePath[] values = new[]
737+
KeyPathValue[] values = new[]
738738
{
739-
new KeyValuePath(key1, new { a = "hello" }),
740-
new KeyValuePath(key2, new { a = "world" })
739+
new KeyPathValue(key1, "$", new { a = "hello" }),
740+
new KeyPathValue(key2, "$", new { a = "world" })
741741
};
742742
commands.MSet(values)
743743
;
@@ -747,22 +747,21 @@ public void MSet()
747747
Assert.Equal("[\"world\"]", result[1].ToString());
748748

749749
// test errors:
750-
Assert.Throws<ArgumentOutOfRangeException>(() => commands.MSet(new KeyValuePath[0]));
751-
750+
Assert.Throws<ArgumentOutOfRangeException>(() => commands.MSet(new KeyPathValue[0]));
752751
}
753752

754753
[Fact]
755-
[Trait("Category","edge")]
754+
[Trait("Category", "edge")]
756755
public async Task MSetAsync()
757756
{
758757
IJsonCommandsAsync commands = new JsonCommands(redisFixture.Redis.GetDatabase());
759758
var keys = CreateKeyNames(2);
760759
var key1 = keys[0];
761760
var key2 = keys[1];
762-
KeyValuePath[] values = new[]
761+
KeyPathValue[] values = new[]
763762
{
764-
new KeyValuePath(key1, new { a = "hello" }),
765-
new KeyValuePath(key2, new { a = "world" })
763+
new KeyPathValue(key1, "$", new { a = "hello" }),
764+
new KeyPathValue(key2, "$", new { a = "world" })
766765
};
767766
await commands.MSetAsync(values)
768767
;
@@ -772,14 +771,47 @@ await commands.MSetAsync(values)
772771
Assert.Equal("[\"world\"]", result[1].ToString());
773772

774773
// test errors:
775-
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await commands.MSetAsync(new KeyValuePath[0]));
774+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await commands.MSetAsync(new KeyPathValue[0]));
776775
}
777776

778777
[Fact]
779-
public void TestKeyValuePathErrors()
778+
[Trait("Category", "edge")]
779+
public void Merge()
780780
{
781-
Assert.Throws<ArgumentNullException>(() => new KeyValuePath(null!, new { a = "hello" }));
782-
Assert.Throws<ArgumentNullException>(() => new KeyValuePath("key", null!) );
781+
// Create a connection to Redis
782+
IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase());
783+
784+
Assert.True(commands.Set("test_merge", "$", new { person = new { name = "John Doe", age = 25, address = new {home = "123 Main Street"}, phone = "123-456-7890" } }));
785+
Assert.True(commands.Merge("test_merge", "$", new { person = new { age = 30 } }));
786+
Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"age\":30,\"address\":{\"home\":\"123 Main Street\"},\"phone\":\"123-456-7890\"}}", commands.Get("test_merge").ToString());
787+
788+
// Test with root path path $.a.b
789+
Assert.True(commands.Merge("test_merge", "$.person.address", new {work = "Redis office"}));
790+
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());
791+
792+
// Test with null value to delete a value
793+
Assert.True(commands.Merge("test_merge", "$.person", "{\"age\":null}"));
794+
Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"phone\":\"123-456-7890\",\"address\":{\"home\":\"123 Main Street\",\"work\":\"Redis office\"}}}", commands.Get("test_merge").ToString());
795+
}
796+
797+
[Fact]
798+
[Trait("Category", "edge")]
799+
public async Task MergeAsync()
800+
{
801+
// Create a connection to Redis
802+
IJsonCommandsAsync commands = new JsonCommands(redisFixture.Redis.GetDatabase());
803+
804+
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" } }));
805+
Assert.True(await commands.MergeAsync("test_merge", "$", new { person = new { age = 30 } }));
806+
Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"age\":30,\"address\":{\"home\":\"123 Main Street\"},\"phone\":\"123-456-7890\"}}", (await commands.GetAsync("test_merge")).ToString());
807+
808+
// Test with root path path $.a.b
809+
Assert.True(await commands.MergeAsync("test_merge", "$.person.address", new {work = "Redis office"}));
810+
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());
811+
812+
// Test with null value to delete a value
813+
Assert.True(await commands.MergeAsync("test_merge", "$.person", "{\"age\":null}"));
814+
Assert.Equal("{\"person\":{\"name\":\"John Doe\",\"phone\":\"123-456-7890\",\"address\":{\"home\":\"123 Main Street\",\"work\":\"Redis office\"}}}", (await commands.GetAsync("test_merge")).ToString());
783815
}
784816

785817
[Fact]
@@ -1067,18 +1099,18 @@ public async Task TestSetFromDirectoryAsync()
10671099
public void TestJsonCommandBuilder()
10681100
{
10691101
var getBuild1 = JsonCommandBuilder.Get("key", "indent", "newline", "space", "path");
1070-
var getBuild2 = JsonCommandBuilder.Get("key",new string[]{"path1", "path2", "path3"}, "indent", "newline", "space");
1071-
var expectedArgs1 = new object[] { "key", "INDENT", "indent", "NEWLINE","newline", "SPACE", "space", "path" };
1102+
var getBuild2 = JsonCommandBuilder.Get("key", new string[] { "path1", "path2", "path3" }, "indent", "newline", "space");
1103+
var expectedArgs1 = new object[] { "key", "INDENT", "indent", "NEWLINE", "newline", "SPACE", "space", "path" };
10721104
var expectedArgs2 = new object[] { "key", "INDENT", "indent", "NEWLINE", "newline", "SPACE", "space", "path1", "path2", "path3" };
10731105

10741106

1075-
for(int i = 0; i < expectedArgs1.Length; i++)
1107+
for (int i = 0; i < expectedArgs1.Length; i++)
10761108
{
10771109
Assert.Equal(expectedArgs1[i].ToString(), getBuild1.Args[i].ToString());
10781110
}
10791111
Assert.Equal("JSON.GET", getBuild1.Command);
10801112

1081-
for(int i = 0; i < expectedArgs2.Length; i++)
1113+
for (int i = 0; i < expectedArgs2.Length; i++)
10821114
{
10831115
Assert.Equal(expectedArgs2[i].ToString(), getBuild2.Args[i].ToString());
10841116
}

0 commit comments

Comments
 (0)