diff --git a/src/NRedisStack/Auxiliary.cs b/src/NRedisStack/Auxiliary.cs index 474489ce..29b40fea 100644 --- a/src/NRedisStack/Auxiliary.cs +++ b/src/NRedisStack/Auxiliary.cs @@ -10,5 +10,19 @@ public static List MergeArgs(RedisKey key, params RedisValue[] items) foreach (var item in items) args.Add(item); return args; } + + public static object[] AssembleNonNullArguments(params object?[] arguments) + { + var args = new List(); + foreach (var arg in arguments) + { + if (arg != null) + { + args.Add(arg); + } + } + + return args.ToArray(); + } } } \ No newline at end of file diff --git a/src/NRedisStack/Bloom/BloomCommands.cs b/src/NRedisStack/Bloom/BloomCommands.cs index dfc96deb..d0765adc 100644 --- a/src/NRedisStack/Bloom/BloomCommands.cs +++ b/src/NRedisStack/Bloom/BloomCommands.cs @@ -111,7 +111,7 @@ public bool[] Insert(RedisKey key, RedisValue[] items, int? capacity = null, throw new ArgumentOutOfRangeException(nameof(items)); var args = BloomAux.BuildInsertArgs(key, items, capacity, error, expansion, nocreate, nonscaling); - + return _db.Execute(BF.INSERT, args).ToBooleanArray(); } @@ -335,7 +335,7 @@ public async Task ReserveAsync(RedisKey key, double errorRate, long capaci /// Iterator value; either 0 or the iterator from a previous invocation of this command. /// Tuple of iterator and data. /// - public Tuple ScanDump(RedisKey key, long iterator) + public Tuple ScanDump(RedisKey key, long iterator) { return _db.Execute(BF.SCANDUMP, key, iterator).ToScanDumpTuple(); } @@ -347,7 +347,7 @@ public Tuple ScanDump(RedisKey key, long iterator) /// Iterator value; either 0 or the iterator from a previous invocation of this command. /// Tuple of iterator and data. /// - public async Task> ScanDumpAsync(RedisKey key, long iterator) + public async Task> ScanDumpAsync(RedisKey key, long iterator) { var result = await _db.ExecuteAsync(BF.SCANDUMP, key, iterator); return result.ToScanDumpTuple(); diff --git a/src/NRedisStack/Json/IJsonCommands.cs b/src/NRedisStack/Json/IJsonCommands.cs new file mode 100644 index 00000000..f8d5cd70 --- /dev/null +++ b/src/NRedisStack/Json/IJsonCommands.cs @@ -0,0 +1,498 @@ +using StackExchange.Redis; + +namespace NRedisStack; + +public interface IJsonCommands +{ + /// + /// Appends the provided items to the array at the provided path. + /// + /// The key to append to + /// The path to append to + /// the values to append + /// The new array sizes for the appended paths + /// + long?[] ArrAppend(RedisKey key, string? path = null, params object[] values); + + /// + /// Finds the index of the provided item within the provided range + /// + /// The key to look up. + /// The json path. + /// The value to find the index of. + /// The starting index within the array. Inclusive. + /// The ending index within the array. Exclusive + /// The index of the value for each array the path resolved to. + /// + long?[] ArrIndex(RedisKey key, string path, object value, long? start = null, long? stop = null); + + /// + /// Inserts the provided items at the provided index within a json array. + /// + /// The key to insert into. + /// The path of the array(s) within the key to insert into. + /// The index to insert at. + /// The values to insert + /// The new size of each array the item was inserted into. + /// + long?[] ArrInsert(RedisKey key, string path, long index, params object[] values); + + /// + /// Gets the length of the arrays resolved by the provided path. + /// + /// The key of the json object. + /// The path to the array(s) + /// The length of each array resolved by the json path. + /// + long?[] ArrLen(RedisKey key, string? path = null); + + /// + /// Pops an item from the array(s) at the provided index. Or the last element if no index is provided. + /// + /// The json key to use. + /// The path of the array(s). + /// The index to pop from + /// The items popped from the array + /// + RedisResult[] ArrPop(RedisKey key, string? path = null, long? index = null); + + /// + /// Trims the array(s) at the provided path, leaving the range between the specified indexes (inclusive). + /// + /// The key to trim from. + /// The path of the array(s) within the json object to trim. + /// the starting index to retain. + /// The ending index to retain. + /// The new length of the array(s) after they're trimmed. + /// + long?[] ArrTrim(RedisKey key, string path, long start, long stop); + + /// + /// Clear's container values(arrays/objects), and sets numeric values to 0. + /// + /// The key to clear. + /// The path to clear. + /// number of values cleared + /// + long Clear(RedisKey key, string? path = null); + + /// + /// Deletes a json value. + /// + /// The key to delete from. + /// The path to delete. + /// number of path's deleted + /// + long Del(RedisKey key, string? path = null); + + /// + /// Deletes a json value. + /// + /// The key to delete from. + /// The path to delete. + /// number of path's deleted + /// + long Forget(RedisKey key, string? path = null); + + /// + /// Gets the value stored at the key and path in redis. + /// + /// The key to retrieve. + /// the indentation string for nested levels + /// sets the string that's printed at the end of each line + /// sets the string that's put between a key and a value + /// the path to get. + /// The requested Items + /// + RedisResult Get(RedisKey key, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null, RedisValue? path = null); + + /// + /// Gets the values stored at the provided paths in redis. + /// + /// The key to pull from. + /// The paths within the key to pull. + /// the indentation string for nested levels + /// sets the string that's printed at the end of each line + /// sets the string that's put between a key and a value + /// + RedisResult Get(RedisKey key, string[] paths, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null); + + /// + /// Generically gets an Item stored in Redis. + /// + /// The key to retrieve + /// The path to retrieve + /// The type retrieved + /// The object requested + /// + T? Get(RedisKey key, string path = "$"); + + /// + /// retrieves a group of items stored in redis, appropriate if the path will resolve to multiple records. + /// + /// The key to pull from. + /// The path to pull. + /// The type. + /// An enumerable of the requested tyep + /// + IEnumerable GetEnumerable(RedisKey key, string path = "$"); + + /// + /// Gets the provided path from multiple keys + /// + /// The keys to retrieve from. + /// The path to retrieve + /// An array of RedisResults with the requested data. + /// + RedisResult[] MGet(RedisKey[] keys, string path); + + /// + /// Increments the fields at the provided path by the provided number. + /// + /// The key. + /// The path to increment. + /// The value to increment by. + /// The new values after being incremented, or null if the path resolved a non-numeric. + /// + double?[] NumIncrby(RedisKey key, string path, double value); + + /// + /// Gets the keys of the object at the provided path. + /// + /// the key of the json object. + /// The path of the object(s) + /// the keys of the resolved object(s) + /// + IEnumerable> ObjKeys(RedisKey key, string? path = null); + + /// + /// returns the number of keys in the object(s) at the provided path. + /// + /// The key of the json object. + /// The path of the object(s) to resolve. + /// The length of the object(s) keyspace. + /// + long?[] ObjLen(RedisKey key, string? path = null); + + /// + /// Gets the key in RESP(Redis Serialization Protocol) form. + /// + /// The key to get. + /// Path within the key to get. + /// the resultant resp + /// + RedisResult[] Resp(RedisKey key, string? path = null); + + /// + /// Set's the key/path to the provided value. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// When to set the value. + /// The disposition of the command + /// + bool Set(RedisKey key, RedisValue path, object obj, When when = When.Always); + + /// + /// Set's the key/path to the provided value. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// When to set the value. + /// The disposition of the command + /// + bool Set(RedisKey key, RedisValue path, RedisValue json, When when = When.Always); + + /// + /// Appends the provided string to the string(s) at the provided path. + /// + /// 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. + /// + long?[] StrAppend(RedisKey key, string value, string? path = null); + + /// + /// Check's the length of the string(s) at the provided path. + /// + /// 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. + /// + public long?[] StrLen(RedisKey key, string? path = null); + + /// + /// Toggles the boolean value(s) at the provided path. + /// + /// The key of the json object. + /// The path of the value(s) to toggle. + /// the new value(s). Which will be null if the path did not resolve to a boolean. + /// + bool?[] Toggle(RedisKey key, string? path = null); + + /// + /// Gets the type(s) of the item(s) at the provided json path. + /// + /// The key of the JSON object. + /// The path to resolve. + /// An array of types. + /// + JsonType[] Type(RedisKey key, string? path = null); + + /// + /// Report a value's memory usage in bytes. path defaults to root if not provided. + /// + /// The object's key + /// The path within the object. + /// the value's size in bytes. + long DebugMemory(string key, string? path = null); + + /// + /// Appends the provided items to the array at the provided path. + /// + /// The key to append to + /// The path to append to + /// the values to append + /// The new array sizes for the appended paths + /// + Task ArrAppendAsync(RedisKey key, string? path = null, params object[] values); + + /// + /// Finds the index of the provided item within the provided range + /// + /// The key to look up. + /// The json path. + /// The value to find the index of. + /// The starting index within the array. Inclusive. + /// The ending index within the array. Exclusive + /// The index of the value for each array the path resolved to. + /// + Task ArrIndexAsync(RedisKey key, string path, object value, long? start = null, long? stop = null); + + /// + /// Inserts the provided items at the provided index within a json array. + /// + /// The key to insert into. + /// The path of the array(s) within the key to insert into. + /// The index to insert at. + /// The values to insert + /// The new size of each array the item was inserted into. + /// + Task ArrInsertAsync(RedisKey key, string path, long index, params object[] values); + + /// + /// Gets the length of the arrays resolved by the provided path. + /// + /// The key of the json object. + /// The path to the array(s) + /// The length of each array resolved by the json path. + /// + Task ArrLenAsync(RedisKey key, string? path = null); + + /// + /// Pops an item from the array(s) at the provided index. Or the last element if no index is provided. + /// + /// The json key to use. + /// The path of the array(s). + /// The index to pop from + /// The items popped from the array + /// + Task ArrPopAsync(RedisKey key, string? path = null, long? index = null); + + /// + /// Trims the array(s) at the provided path, leaving the range between the specified indexes (inclusive). + /// + /// The key to trim from. + /// The path of the array(s) within the json object to trim. + /// the starting index to retain. + /// The ending index to retain. + /// The new length of the array(s) after they're trimmed. + /// + Task ArrTrimAsync(RedisKey key, string path, long start, long stop); + + /// + /// Clear's container values(arrays/objects), and sets numeric values to 0. + /// + /// The key to clear. + /// The path to clear. + /// number of values cleared + /// + Task ClearAsync(RedisKey key, string? path = null); + + /// + /// Deletes a json value. + /// + /// The key to delete from. + /// The path to delete. + /// number of path's deleted + /// + Task DelAsync(RedisKey key, string? path = null); + + /// + /// Deletes a json value. + /// + /// The key to delete from. + /// The path to delete. + /// number of path's deleted + /// + Task ForgetAsync(RedisKey key, string? path = null); + + /// + /// Gets the value stored at the key and path in redis. + /// + /// The key to retrieve. + /// the indentation string for nested levels + /// sets the string that's printed at the end of each line + /// sets the string that's put between a key and a value + /// the path to get. + /// The requested Items + /// + Task GetAsync(RedisKey key, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null, RedisValue? path = null); + + /// + /// Gets the values stored at the provided paths in redis. + /// + /// The key to pull from. + /// The paths within the key to pull. + /// the indentation string for nested levels + /// sets the string that's printed at the end of each line + /// sets the string that's put between a key and a value + /// + Task GetAsync(RedisKey key, string[] paths, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null); + + /// + /// Generically gets an Item stored in Redis. + /// + /// The key to retrieve + /// The path to retrieve + /// The type retrieved + /// The object requested + /// + Task GetAsync(RedisKey key, string path = "$"); + + /// + /// retrieves a group of items stored in redis, appropriate if the path will resolve to multiple records. + /// + /// The key to pull from. + /// The path to pull. + /// The type. + /// An enumerable of the requested tyep + /// + Task> GetEnumerableAsync(RedisKey key, string path = "$"); + + /// + /// Gets the provided path from multiple keys + /// + /// The keys to retrieve from. + /// The path to retrieve + /// An array of RedisResults with the requested data. + /// + Task MGetAsync(RedisKey[] keys, string path); + + /// + /// Increments the fields at the provided path by the provided number. + /// + /// The key. + /// The path to increment. + /// The value to increment by. + /// The new values after being incremented, or null if the path resolved a non-numeric. + /// + Task NumIncrbyAsync(RedisKey key, string path, double value); + + /// + /// Gets the keys of the object at the provided path. + /// + /// the key of the json object. + /// The path of the object(s) + /// the keys of the resolved object(s) + /// + Task>> ObjKeysAsync(RedisKey key, string? path = null); + + /// + /// returns the number of keys in the object(s) at the provided path. + /// + /// The key of the json object. + /// The path of the object(s) to resolve. + /// The length of the object(s) keyspace. + /// + Task ObjLenAsync(RedisKey key, string? path = null); + + /// + /// Gets the key in RESP(Redis Serialization Protocol) form. + /// + /// The key to get. + /// Path within the key to get. + /// the resultant resp + /// + Task RespAsync(RedisKey key, string? path = null); + + /// + /// Set's the key/path to the provided value. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// When to set the value. + /// The disposition of the command + /// + Task SetAsync(RedisKey key, RedisValue path, object obj, When when = When.Always); + + /// + /// Set's the key/path to the provided value. + /// + /// The key. + /// The path to set within the key. + /// The value to set. + /// When to set the value. + /// The disposition of the command + /// + Task SetAsync(RedisKey key, RedisValue path, RedisValue json, When when = When.Always); + + /// + /// Appends the provided string to the string(s) at the provided path. + /// + /// 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. + /// + Task StrAppendAsync(RedisKey key, string value, string? path = null); + + /// + /// Check's the length of the string(s) at the provided path. + /// + /// 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. + /// + Task StrLenAsync(RedisKey key, string? path = null); + + /// + /// Toggles the boolean value(s) at the provided path. + /// + /// The key of the json object. + /// The path of the value(s) to toggle. + /// the new value(s). Which will be null if the path did not resolve to a boolean. + /// + Task ToggleAsync(RedisKey key, string? path = null); + + /// + /// Gets the type(s) of the item(s) at the provided json path. + /// + /// The key of the JSON object. + /// The path to resolve. + /// An array of types. + /// + Task TypeAsync(RedisKey key, string? path = null); + + /// + /// Report a value's memory usage in bytes. path defaults to root if not provided. + /// + /// The object's key + /// The path within the object. + /// the value's size in bytes. + Task DebugMemoryAsync(string key, string? path = null); +} \ No newline at end of file diff --git a/src/NRedisStack/Json/JsonCommands.cs b/src/NRedisStack/Json/JsonCommands.cs index 7782408d..78fa6627 100644 --- a/src/NRedisStack/Json/JsonCommands.cs +++ b/src/NRedisStack/Json/JsonCommands.cs @@ -1,48 +1,595 @@ using NRedisStack.Literals; using StackExchange.Redis; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Nodes; +using static NRedisStack.Auxiliary; namespace NRedisStack; -public class JsonCommands +public class JsonCommands : IJsonCommands { IDatabase _db; public JsonCommands(IDatabase db) { _db = db; } - private readonly JsonSerializerOptions Options = new() + + /// + public RedisResult[] Resp(RedisKey key, string? path = null) { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - public RedisResult Set(RedisKey key, RedisValue path, object obj, When when = When.Always) + RedisResult result; + if (string.IsNullOrEmpty(path)) + { + result = _db.Execute(JSON.RESP, key); + } + else + { + result = _db.Execute(JSON.RESP, key, path); + } + + if (result.IsNull) + { + return Array.Empty(); + } + + return (RedisResult[])result!; + } + + /// + public bool Set(RedisKey key, RedisValue path, object obj, When when = When.Always) { string json = JsonSerializer.Serialize(obj); return Set(key, path, json, when); } - public RedisResult Set(RedisKey key, RedisValue path, RedisValue json, When when = When.Always) + /// + public bool Set(RedisKey key, RedisValue path, RedisValue json, When when = When.Always) + { + var result = when switch + { + When.Exists => _db.Execute(JSON.SET, key, path, json, "XX"), + When.NotExists => _db.Execute(JSON.SET, key, path, json, "NX"), + _ => _db.Execute(JSON.SET, key, path, json) + }; + + if (result.IsNull || result.ToString() != "OK") + { + return false; + } + + return true; + } + + /// + public long?[] StrAppend(RedisKey key, string value, string? path = null) + { + RedisResult result; + if (path == null) + { + result = _db.Execute(JSON.STRAPPEND, key, JsonSerializer.Serialize(value)); + } + else + { + result = _db.Execute(JSON.STRAPPEND, key, path, JsonSerializer.Serialize(value)); + } + + return result.ToNullableLongArray(); + } + + /// + public long?[] StrLen(RedisKey key, string? path = null) + { + RedisResult result; + if (path != null) + { + result = _db.Execute(JSON.STRLEN, key, path); + } + else + { + result = _db.Execute(JSON.STRLEN, key); + } + + return result.ToNullableLongArray(); + } + + /// + public bool?[] Toggle(RedisKey key, string? path = null) + { + RedisResult result; + if (path != null) + { + result = _db.Execute(JSON.TOGGLE, key, path); + } + else + { + result = _db.Execute(JSON.TOGGLE, key, "$"); + } + + if (result.IsNull) + { + return Array.Empty(); + } + + if (result.Type == ResultType.Integer) + { + return new bool?[] { (long)result == 1 }; + } + + return ((RedisResult[])result!).Select(x => (bool?)((long)x == 1)).ToArray(); + } + + /// + public JsonType[] Type(RedisKey key, string? path = null) + { + RedisResult result; + if (path == null) + { + result = _db.Execute(JSON.TYPE, key); + } + else + { + result = _db.Execute(JSON.TYPE, key, path); + } + + if (result.Type == ResultType.MultiBulk) + { + return ((RedisResult[])result!).Select(x => Enum.Parse(x.ToString()!.ToUpper())).ToArray(); + } + + if (result.Type == ResultType.BulkString) + { + return new[] { Enum.Parse(result.ToString()!.ToUpper()) }; + } + + return Array.Empty(); + + } + + public long DebugMemory(string key, string? path = null) + { + if (path != null) + { + return (long)_db.Execute(JSON.DEBUG, JSON.MEMORY, key, path); + } + return (long)_db.Execute(JSON.DEBUG, JSON.MEMORY, key); + } + + public async Task ArrAppendAsync(RedisKey key, string? path = null, params object[] values) + { + if (values.Length < 1) + throw new ArgumentOutOfRangeException(nameof(values)); + + var args = new List { key }; + if (path != null) + { + args.Add(path); + } + + args.AddRange(values.Select(x => JsonSerializer.Serialize(x))); + + var result = await _db.ExecuteAsync(JSON.ARRAPPEND, args.ToArray()); + return result.ToNullableLongArray(); + } + + public async Task ArrIndexAsync(RedisKey key, string path, object value, long? start = null, long? stop = null) + { + if (start == null && stop != null) + throw new ArgumentException("stop cannot be defined without start"); + + var args = AssembleNonNullArguments(key, path, JsonSerializer.Serialize(value), start, stop); + var result = await _db.ExecuteAsync(JSON.ARRINDEX, args); + return result.ToNullableLongArray(); + } + + public async Task ArrInsertAsync(RedisKey key, string path, long index, params object[] values) + { + if (values.Length < 1) + throw new ArgumentOutOfRangeException(nameof(values)); + var args = new List { key, path, index }; + foreach (var val in values) + { + args.Add(JsonSerializer.Serialize(val)); + } + + var result = await _db.ExecuteAsync(JSON.ARRINSERT, args); + return result.ToNullableLongArray(); + } + + public async Task ArrLenAsync(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + var result = await _db.ExecuteAsync(JSON.ARRLEN, args); + return result.ToNullableLongArray(); + } + + public async Task ArrPopAsync(RedisKey key, string? path = null, long? index = null) + { + if (path == null && index != null) + throw new ArgumentException("index cannot be defined without path"); + + var args = AssembleNonNullArguments(key, path, index); + var res = await _db.ExecuteAsync(JSON.ARRPOP, args)!; + + if (res.Type == ResultType.MultiBulk) + { + return (RedisResult[])res!; + } + + if (res.Type == ResultType.BulkString) + { + return new[] { res }; + } + + return Array.Empty(); + } + + public async Task ArrTrimAsync(RedisKey key, string path, long start, long stop) => + (await _db.ExecuteAsync(JSON.ARRTRIM, key, path, start, stop)).ToNullableLongArray(); + + public async Task ClearAsync(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return (long)await _db.ExecuteAsync(JSON.CLEAR, args); + } + + public async Task DelAsync(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return (long)await _db.ExecuteAsync(JSON.DEL, args); + } + + public Task ForgetAsync(RedisKey key, string? path = null) => DelAsync(key, path); + + public Task GetAsync(RedisKey key, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null, + RedisValue? path = null) + { + List args = new List() { key }; + + if (indent != null) + { + args.Add(JsonArgs.INDENT); + args.Add(indent); + } + + if (newLine != null) + { + args.Add(JsonArgs.NEWLINE); + args.Add(newLine); + } + + if (space != null) + { + args.Add(JsonArgs.SPACE); + args.Add(space); + } + + if (path != null) + { + args.Add(path); + } + + return _db.ExecuteAsync(JSON.GET, args); + } + + public Task GetAsync(RedisKey key, string[] paths, RedisValue? indent = null, RedisValue? newLine = null, + RedisValue? space = null) + { + List args = new List() { key }; + + foreach (var path in paths) + { + args.Add(path); + } + + if (indent != null) + { + args.Add(JsonArgs.INDENT); + args.Add(indent); + } + + if (newLine != null) + { + args.Add(JsonArgs.NEWLINE); + args.Add(newLine); + } + + if (space != null) + { + args.Add(JsonArgs.SPACE); + args.Add(space); + } + + return _db.ExecuteAsync(JSON.GET, args); + } + + public async Task GetAsync(RedisKey key, string path = "$") + { + var res = await _db.ExecuteAsync(JSON.GET, key, path); + if (res.Type == ResultType.BulkString) + { + var arr = JsonSerializer.Deserialize(res.ToString()!); + if (arr?.Count > 0) + { + return JsonSerializer.Deserialize(JsonSerializer.Serialize(arr[0])); + } + } + + return default; + } + + public Task> GetEnumerableAsync(RedisKey key, string path = "$") + { + throw new NotImplementedException(); + } + + public async Task MGetAsync(RedisKey[] keys, string path) + { + var args = new List(); + foreach (var key in keys) + { + args.Add(key); + } + + args.Add(path); + var res = await _db.ExecuteAsync(JSON.MGET, args); + if (res.IsNull) + { + return Array.Empty(); + } + return (RedisResult[])res!; + } + + public async Task NumIncrbyAsync(RedisKey key, string path, double value) + { + var res = await _db.ExecuteAsync(JSON.NUMINCRBY, key, path, value); + return JsonSerializer.Deserialize(res.ToString()); + } + + public async Task>> ObjKeysAsync(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return (await _db.ExecuteAsync(JSON.OBJKEYS, args)).ToHashSets(); + } + + public async Task ObjLenAsync(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return (await _db.ExecuteAsync(JSON.OBJLEN, args)).ToNullableLongArray(); + } + + public async Task RespAsync(RedisKey key, string? path = null) + { + RedisResult result; + if (string.IsNullOrEmpty(path)) + { + result = await _db.ExecuteAsync(JSON.RESP, key); + } + else + { + result = await _db.ExecuteAsync(JSON.RESP, key, path); + } + + if (result.IsNull) + { + return Array.Empty(); + } + + return (RedisResult[])result!; + } + + public Task SetAsync(RedisKey key, RedisValue path, object obj, When when = When.Always) + { + string json = JsonSerializer.Serialize(obj); + return SetAsync(key, path, json, when); + } + + public async Task SetAsync(RedisKey key, RedisValue path, RedisValue json, When when = When.Always) + { + var t = when switch + { + When.Exists => _db.ExecuteAsync(JSON.SET, key, path, json, "XX"), + When.NotExists => _db.ExecuteAsync(JSON.SET, key, path, json, "NX"), + _ => _db.ExecuteAsync(JSON.SET, key, path, json) + }; + + var result = await t; + + if (result.IsNull || result.ToString() != "OK") + { + return false; + } + + return true; + } + + public async Task StrAppendAsync(RedisKey key, string value, string? path = null) + { + RedisResult result; + if (path == null) + { + result = await _db.ExecuteAsync(JSON.STRAPPEND, key, JsonSerializer.Serialize(value)); + } + else + { + result = await _db.ExecuteAsync(JSON.STRAPPEND, key, path, JsonSerializer.Serialize(value)); + } + + return result.ToNullableLongArray(); + } + + public async Task StrLenAsync(RedisKey key, string? path = null) + { + RedisResult result; + if (path != null) + { + result = await _db.ExecuteAsync(JSON.STRLEN, key, path); + } + else + { + result = await _db.ExecuteAsync(JSON.STRLEN, key); + } + + return result.ToNullableLongArray(); + } + + public async Task ToggleAsync(RedisKey key, string? path = null) + { + RedisResult result; + if (path != null) + { + result = await _db.ExecuteAsync(JSON.TOGGLE, key, path); + } + else + { + result = await _db.ExecuteAsync(JSON.TOGGLE, key, "$"); + } + + if (result.IsNull) + { + return Array.Empty(); + } + + if (result.Type == ResultType.Integer) + { + return new bool?[] { (long)result == 1 }; + } + + return ((RedisResult[])result!).Select(x => (bool?)((long)x == 1)).ToArray(); + } + + public async Task TypeAsync(RedisKey key, string? path = null) + { + RedisResult result; + if (path == null) + { + result = await _db.ExecuteAsync(JSON.TYPE, key); + } + else + { + result = await _db.ExecuteAsync(JSON.TYPE, key, path); + } + + if (result.Type == ResultType.MultiBulk) + { + return ((RedisResult[])result!).Select(x => Enum.Parse(x.ToString()!.ToUpper())).ToArray(); + } + + if (result.Type == ResultType.BulkString) + { + return new[] { Enum.Parse(result.ToString()!.ToUpper()) }; + } + + return Array.Empty(); + } + + public async Task DebugMemoryAsync(string key, string? path = null) + { + if (path != null) + { + return (long)await _db.ExecuteAsync(JSON.DEBUG, JSON.MEMORY, key, path); + } + return (long)await _db.ExecuteAsync(JSON.DEBUG, JSON.MEMORY, key); + } + + /// + public long?[] ArrAppend(RedisKey key, string? path = null, params object[] values) { - switch (when) + if (values.Length < 1) + throw new ArgumentOutOfRangeException(nameof(values)); + + var args = new List { key }; + if (path != null) { - case When.Exists: - return _db.Execute(JSON.SET, key, path, json, "XX"); - case When.NotExists: - return _db.Execute(JSON.SET, key, path, json, "NX"); - default: - return _db.Execute(JSON.SET, key, path, json); + args.Add(path); } + + args.AddRange(values.Select(x => JsonSerializer.Serialize(x))); + + var result = _db.Execute(JSON.ARRAPPEND, args.ToArray()); + return result.ToNullableLongArray(); } - public RedisResult Get(RedisKey key, - RedisValue? indent = null, - RedisValue? newLine = null, - RedisValue? space = null, - RedisValue? path = null) + /// + public long?[] ArrIndex(RedisKey key, string path, object value, long? start = null, long? stop = null) { + if (start == null && stop != null) + throw new ArgumentException("stop cannot be defined without start"); + + var args = AssembleNonNullArguments(key, path, JsonSerializer.Serialize(value), start, stop); + var result = _db.Execute(JSON.ARRINDEX, args); + return result.ToNullableLongArray(); + } - List args = new List(){key}; + /// + public long?[] ArrInsert(RedisKey key, string path, long index, params object[] values) + { + if (values.Length < 1) + throw new ArgumentOutOfRangeException(nameof(values)); + var args = new List { key, path, index }; + foreach (var val in values) + { + args.Add(JsonSerializer.Serialize(val)); + } + + var result = _db.Execute(JSON.ARRINSERT, args); + return result.ToNullableLongArray(); + } + + /// + public long?[] ArrLen(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + var result = _db.Execute(JSON.ARRLEN, args); + return result.ToNullableLongArray(); + } + + /// + public RedisResult[] ArrPop(RedisKey key, string? path = null, long? index = null) + { + if (path == null && index != null) + throw new ArgumentException("index cannot be defined without path"); + + var args = AssembleNonNullArguments(key, path, index); + var res = _db.Execute(JSON.ARRPOP, args)!; + + if (res.Type == ResultType.MultiBulk) + { + return (RedisResult[])res!; + } + + if (res.Type == ResultType.BulkString) + { + return new[] { res }; + } + + return Array.Empty(); + } + + /// + public long?[] ArrTrim(RedisKey key, string path, long start, long stop) => + _db.Execute(JSON.ARRTRIM, key, path, start, stop).ToNullableLongArray(); + + /// + public long Clear(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return (long)_db.Execute(JSON.CLEAR, args); + } + + /// + public long Del(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return (long)_db.Execute(JSON.DEL, args); + } + + /// + public long Forget(RedisKey key, string? path = null) => Del(key, path); + + /// + public RedisResult Get(RedisKey key, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null, RedisValue? path = null) + { + List args = new List() { key }; if (indent != null) { @@ -69,4 +616,92 @@ public RedisResult Get(RedisKey key, return _db.Execute(JSON.GET, args); } + + /// + public RedisResult Get(RedisKey key, string[] paths, RedisValue? indent = null, RedisValue? newLine = null, RedisValue? space = null) + { + List args = new List() { key }; + + foreach (var path in paths) + { + args.Add(path); + } + + if (indent != null) + { + args.Add(JsonArgs.INDENT); + args.Add(indent); + } + + if (newLine != null) + { + args.Add(JsonArgs.NEWLINE); + args.Add(newLine); + } + + if (space != null) + { + args.Add(JsonArgs.SPACE); + args.Add(space); + } + + return _db.Execute(JSON.GET, args); + } + + /// + public T? Get(RedisKey key, string path = "$") + { + var res = _db.Execute(JSON.GET, key, path); + if (res.Type == ResultType.BulkString) + { + var arr = JsonSerializer.Deserialize(res.ToString()!); + if (arr?.Count > 0) + { + return JsonSerializer.Deserialize(JsonSerializer.Serialize(arr[0])); + } + } + + return default; + } + + /// + public IEnumerable GetEnumerable(RedisKey key, string path = "$") + { + var res = _db.Execute(JSON.GET, key, path); + return JsonSerializer.Deserialize>(res.ToString()); + } + + /// + public RedisResult[] MGet(RedisKey[] keys, string path) + { + var args = new List(); + foreach (var key in keys) + { + args.Add(key); + } + + args.Add(path); + return (RedisResult[])_db.Execute(JSON.MGET, args)!; + } + + /// + public double?[] NumIncrby(RedisKey key, string path, double value) + { + var res = _db.Execute(JSON.NUMINCRBY, key, path, value); + return JsonSerializer.Deserialize(res.ToString()); + } + + /// + public IEnumerable> ObjKeys(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return _db.Execute(JSON.OBJKEYS, args).ToHashSets(); + } + + /// + public long?[] ObjLen(RedisKey key, string? path = null) + { + var args = AssembleNonNullArguments(key, path); + return _db.Execute(JSON.OBJLEN, args).ToNullableLongArray(); + } } \ No newline at end of file diff --git a/src/NRedisStack/Json/JsonType.cs b/src/NRedisStack/Json/JsonType.cs new file mode 100644 index 00000000..6e5ac114 --- /dev/null +++ b/src/NRedisStack/Json/JsonType.cs @@ -0,0 +1,13 @@ +namespace NRedisStack; + +public enum JsonType +{ + UNKNOWN = 0, + NULL = 1, + BOOLEAN = 2, + INTEGER = 3, + NUMBER = 4, + STRING = 5, + ARRAY = 6, + OBJECT = 7 +} \ No newline at end of file diff --git a/src/NRedisStack/Json/Literals/Commands.cs b/src/NRedisStack/Json/Literals/Commands.cs index 159a4c81..c455e6ea 100644 --- a/src/NRedisStack/Json/Literals/Commands.cs +++ b/src/NRedisStack/Json/Literals/Commands.cs @@ -11,10 +11,10 @@ internal class JSON public const string CLEAR = "JSON.CLEAR"; public const string DEBUG = "JSON.DEBUG"; public const string DEBUG_HELP = "JSON.DEBUG HELP"; - public const string DEBUG_MEMORY = "JSON.DEBUG MEMORY"; public const string DEL = "JSON.DEL"; public const string FORGET = "JSON.FORGET"; public const string GET = "JSON.GET"; + public const string MEMORY = "MEMORY"; public const string MGET = "JSON.MGET"; public const string NUMINCRBY = "JSON.NUMINCRBY"; public const string NUMMULTBY = "JSON.NUMMULTBY"; diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index 6cf0c475..5e9f87d4 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -531,5 +531,53 @@ public static IReadOnlyList ToStringArray(this RedisResult result) Array.ForEach(redisResults, str => list.Add((string)str)); return list; } + + public static long?[] ToNullableLongArray(this RedisResult result) + { + if (result.IsNull) + { + return Array.Empty(); + } + + if (result.Type == ResultType.Integer) + { + return new[] { (long?)result }; + } + + return ((RedisResult[])result!).Select(x=>(long?)x).ToArray(); + } + + public static IEnumerable> ToHashSets(this RedisResult result) + { + if (result.IsNull) + { + return Array.Empty>(); + } + + var res = (RedisResult[])result!; + var sets = new List>(); + if (res.All(x => x.Type != ResultType.MultiBulk)) + { + var keys = res.Select(x => x.ToString()!); + sets.Add(keys.ToHashSet()); + return sets; + } + + foreach (var arr in res) + { + var set = new HashSet(); + if (arr.Type == ResultType.MultiBulk) + { + var resultArr = (RedisResult[])arr!; + foreach (var item in resultArr) + { + set.Add(item.ToString()!); + } + } + sets.Add(set); + } + + return sets; + } } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Json/JsonTests.cs b/tests/NRedisStack.Tests/Json/JsonTests.cs index 33b0eaf7..b35fb9ab 100644 --- a/tests/NRedisStack.Tests/Json/JsonTests.cs +++ b/tests/NRedisStack.Tests/Json/JsonTests.cs @@ -1,3 +1,6 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Nodes; using Xunit; using StackExchange.Redis; using Moq; @@ -9,67 +12,23 @@ namespace NRedisStack.Tests; public class JsonTests : AbstractNRedisStackTest, IDisposable { Mock _mock = new Mock(); - private readonly string key = "JSON_TESTS"; + private readonly string _testName = "JSON_TESTS"; public JsonTests(RedisFixture redisFixture) : base(redisFixture) { } public void Dispose() { - redisFixture.Redis.GetDatabase().KeyDelete(key); + redisFixture.Redis.GetDatabase().KeyDelete(_testName); } - // [Fact] - // public void TestJsonSet() - // { - // var obj = new Person { Name = "Shachar", Age = 23 }; - // _mock.Object.JSON().Set("Person:Shachar", "$", obj, When.Exists); - // _mock.Verify(x => x.Execute("JSON.SET", "Person:Shachar", "$", "{\"Name\":\"Shachar\",\"Age\":23}", "XX")); - // } - [Fact] public void TestJsonSetNotExist() { var obj = new Person { Name = "Shachar", Age = 23 }; + _mock.Setup(x => x.Execute(It.IsAny(), It.IsAny())).Returns((RedisResult.Create(new RedisValue("OK")))); _mock.Object.JSON().Set("Person:Shachar", "$", obj, When.NotExists); _mock.Verify(x => x.Execute("JSON.SET", "Person:Shachar", "$", "{\"Name\":\"Shachar\",\"Age\":23}", "NX")); } - //TODO: understand why this 2 tests are not pass what we do - //"dotnet test" but they pass when we do "dotnet test --filter ..." - // [Fact] - // public void TestSimpleJsonGet() - // { - // var obj = new Person { Name = "Shachar", Age = 23 }; - // IDatabase db = redisFixture.Redis.GetDatabase(); - // db.Execute("FLUSHALL"); - // var cf = db.JSON(); - - // json.Set(key, "$", obj); - // string expected = "{\"Name\":\"Shachar\",\"Age\":23}"; - // var result = json.Get(key).ToString(); - // if(result == null) - // throw new ArgumentNullException(nameof(result)); - - // Assert.Equal(result, expected); - // } - - // [Fact] - // public void TestJsonGet() - // { - // var obj = new Person { Name = "Shachar", Age = 23 }; - // IDatabase db = redisFixture.Redis.GetDatabase(); - // db.Execute("FLUSHALL"); - // var cf = db.JSON(); - - // json.Set(key, "$", obj); - - // var expected = "[222111\"Shachar\"222]"; - // var result = json.Get(key, "111", "222", "333", "$.Name"); - // // if(result == null) - // // throw new ArgumentNullException(nameof(result)); - // Assert.Equal(result.ToString(), expected); - // } - - [Fact] public void TestModulePrefixs() { @@ -102,6 +61,769 @@ public void TestModulePrefixs1() // ... conn.Dispose(); } + } + + [Fact] + public void TestResp() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(1); + + var key = keys[0]; + commands.Set(key, "$", new { name = "Steve", age = 33 }); + + //act + var respResult = commands.Resp(key); + + //assert + var i = 0; + Assert.Equal("{", respResult[i++]!.ToString()); + Assert.Equal("name", respResult[i++]!.ToString()); + Assert.Equal("Steve", respResult[i++]!.ToString()); + Assert.Equal("age", respResult[i++]!.ToString()); + Assert.Equal(33, (long)respResult[i]!); + conn.GetDatabase().KeyDelete(key); + } + + [Fact] + public async Task TestRespAsync() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(1); + + var key = keys[0]; + await commands.SetAsync(key, "$", new { name = "Steve", age = 33 }); + + //act + var respResult = await commands.RespAsync(key); + + //assert + var i = 0; + Assert.Equal("{", respResult[i++]!.ToString()); + Assert.Equal("name", respResult[i++]!.ToString()); + Assert.Equal("Steve", respResult[i++]!.ToString()); + Assert.Equal("age", respResult[i++]!.ToString()); + Assert.Equal(33, (long)respResult[i]!); + conn.GetDatabase().KeyDelete(key); + } + + [Fact] + public void TestStringAppend() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + + var key = keys[0]; + commands.Set(key, "$", new { name = "Steve", sibling = new {name = "christopher"}, age = 33}); + var simpleStringKey = keys[1]; + commands.Set(simpleStringKey, "$", "\"foo\""); + + //act + var nullResult = commands.StrAppend(key, " Lorello", "$.age"); + var keyResult = commands.StrAppend(key, " Lorello", "$..name"); + var simpleKeyResult = commands.StrAppend(simpleStringKey, "bar"); + + //assert + var i = 0; + Assert.Equal(2, keyResult.Length); + Assert.Equal(13, keyResult[i++]); + Assert.Equal(19, keyResult[i]); + Assert.Null(nullResult[0]); + Assert.Equal(6, simpleKeyResult[0]); + } + + [Fact] + public async Task TestStringAppendAsync() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + + var key = keys[0]; + await commands.SetAsync(key, "$", new { name = "Steve", sibling = new {name = "christopher"}, age = 33}); + var simpleStringKey = keys[1]; + await commands.SetAsync(simpleStringKey, "$", "\"foo\""); + + //act + var nullResult = await commands.StrAppendAsync(key, " Lorello", "$.age"); + var keyResult = await commands.StrAppendAsync(key, " Lorello", "$..name"); + var simpleKeyResult = await commands.StrAppendAsync(simpleStringKey, "bar"); + + //assert + var i = 0; + Assert.Equal(2, keyResult.Length); + Assert.Equal(13, keyResult[i++]); + Assert.Equal(19, keyResult[i]); + Assert.Null(nullResult[0]); + Assert.Equal(6, simpleKeyResult[0]); + } + + [Fact] + public void StringLength() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleStringKey = keys[1]; + + commands.Set(key, "$", new { name = "Steve", sibling = new {name = "christopher"}, age = 33}); + commands.Set(simpleStringKey, "$", "\"foo\""); + + var normalResult = commands.StrLen(key, "$..name"); + var nullResult = commands.StrLen(key, "$.age"); + var simpleResult = commands.StrLen(simpleStringKey); + + var i = 0; + Assert.Equal(5, normalResult[i++]); + Assert.Equal(11, normalResult[i]); + Assert.Null(nullResult[0]); + Assert.Equal(3,simpleResult[0]); + } + + [Fact] + public async Task StringLengthAsync() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleStringKey = keys[1]; + + await commands.SetAsync(key, "$", new { name = "Steve", sibling = new {name = "christopher"}, age = 33}); + await commands.SetAsync(simpleStringKey, "$", "\"foo\""); + + var normalResult = await commands.StrLenAsync(key, "$..name"); + var nullResult = await commands.StrLenAsync(key, "$.age"); + var simpleResult = await commands.StrLenAsync(simpleStringKey); + + var i = 0; + Assert.Equal(5, normalResult[i++]); + Assert.Equal(11, normalResult[i]); + Assert.Null(nullResult[0]); + Assert.Equal(3,simpleResult[0]); + } + + [Fact] + public void Toggle() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + + commands.Set(key, "$", new { @bool = true, other = new {@bool = false}, age = 33}); + commands.Set(simpleKey, "$", true); + + var result = commands.Toggle(key, "$..bool"); + var simpleResult = commands.Toggle(simpleKey); + + Assert.False(result[0]); + Assert.True(result[1]); + Assert.False(simpleResult[0]); + } + + [Fact] + public async Task ToggleAsync() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + + await commands.SetAsync(key, "$", new { @bool = true, other = new {@bool = false}, age = 33}); + await commands.SetAsync(simpleKey, "$", true); + + var result = await commands.ToggleAsync(key, "$..bool"); + var simpleResult = await commands.ToggleAsync(simpleKey); + + Assert.False(result[0]); + Assert.True(result[1]); + Assert.False(simpleResult[0]); + } + + [Fact] + public void Type() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Steve", sibling = new {name = "christopher"}, age = 33, aDouble = 3.5}); + commands.Set(simpleKey, "$", "true"); + + var result = commands.Type(key, "$..name"); + Assert.Equal(JsonType.STRING, result[0]); + Assert.Equal(JsonType.STRING, result[1]); + result = commands.Type(key, "$..age"); + Assert.Equal(JsonType.INTEGER, result[0]); + result = commands.Type(key, "$..aDouble"); + Assert.Equal(JsonType.NUMBER, result[0]); + result = commands.Type(simpleKey); + Assert.Equal(JsonType.BOOLEAN, result[0]); + } + + [Fact] + public async Task TypeAsync() + { + //arrange + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + await commands.SetAsync(key, "$", new { name = "Steve", sibling = new {name = "christopher"}, age = 33, aDouble = 3.5}); + await commands.SetAsync(simpleKey, "$", "true"); + + var result = await commands.TypeAsync(key, "$..name"); + Assert.Equal(JsonType.STRING, result[0]); + Assert.Equal(JsonType.STRING, result[1]); + result = await commands.TypeAsync(key, "$..age"); + Assert.Equal(JsonType.INTEGER, result[0]); + result = await commands.TypeAsync(key, "$..aDouble"); + Assert.Equal(JsonType.NUMBER, result[0]); + result = await commands.TypeAsync(simpleKey); + Assert.Equal(JsonType.BOOLEAN, result[0]); + } + + [Fact] + public void ArrayAppend() + { + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var complexKey = keys[1]; + + commands.Set(key, "$", new { name = "Elizabeth", nickNames = new[] { "Beth" } }); + commands.Set(complexKey, "$", new { name = "foo", people = new[] { new { name = "steve" } } }); + var result = commands.ArrAppend(key, "$.nickNames", "Elle", "Liz","Betty"); + Assert.Equal(4, result[0]); + result = commands.ArrAppend(complexKey, "$.people", new { name = "bob" }); + Assert.Equal(2, result[0]); + } + + [Fact] + public async Task ArrayAppendAsync() + { + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(2); + var key = keys[0]; + var complexKey = keys[1]; + + await commands.SetAsync(key, "$", new { name = "Elizabeth", nickNames = new[] { "Beth" } }); + await commands.SetAsync(complexKey, "$", new { name = "foo", people = new[] { new { name = "steve" } } }); + var result = await commands.ArrAppendAsync(key, "$.nickNames", "Elle", "Liz","Betty"); + Assert.Equal(4, result[0]); + result = await commands.ArrAppendAsync(complexKey, "$.people", new { name = "bob" }); + Assert.Equal(2, result[0]); + } + + [Fact] + public void ArrayIndex() + { + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(1); + var key = keys[0]; + commands.Set(key, "$", new { name = "Elizabeth", nicknames = new[] { "Beth", "Betty", "Liz" }, sibling = new {name="Johnathan", nicknames = new [] {"Jon", "Johnny"}} }); + var res = commands.ArrIndex(key, "$..nicknames", "Betty", 0,5); + Assert.Equal(1,res[0]); + Assert.Equal(-1,res[1]); + } + + [Fact] + public async Task ArrayIndexAsync() + { + var conn = redisFixture.Redis; + var db = conn.GetDatabase(); + IJsonCommands commands = new JsonCommands(db); + var keys = CreateKeyNames(1); + var key = keys[0]; + await commands.SetAsync(key, "$", new { name = "Elizabeth", nicknames = new[] { "Beth", "Betty", "Liz" }, sibling = new {name="Johnathan", nicknames = new [] {"Jon", "Johnny"}} }); + var res = await commands.ArrIndexAsync(key, "$..nicknames", "Betty", 0,5); + Assert.Equal(1,res[0]); + Assert.Equal(-1,res[1]); + } + + [Fact] + public void ArrayInsert() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.ArrInsert(key, $"$.nicknames", 1, "Lys"); + Assert.Equal(4,result[0]); + result = commands.ArrInsert(simpleKey, "$", 1, "Lys"); + Assert.Equal(4, result[0]); + } + + [Fact] + public async Task ArrayInsertAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + + await commands.SetAsync(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + await commands.SetAsync(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = await commands.ArrInsertAsync(key, $"$.nicknames", 1, "Lys"); + Assert.Equal(4,result[0]); + result = await commands.ArrInsertAsync(simpleKey, "$", 1, "Lys"); + Assert.Equal(4, result[0]); + } + + [Fact] + public void ArrayLength() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.ArrLen(key, $"$.nicknames"); + Assert.Equal(3, result[0]); + result = commands.ArrLen(simpleKey); + Assert.Equal(3, result[0]); + } + + [Fact] + public async Task ArrayLengthAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + await commands.SetAsync(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + await commands.SetAsync(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = await commands.ArrLenAsync(key, $"$.nicknames"); + Assert.Equal(3, result[0]); + result = await commands.ArrLenAsync(simpleKey); + Assert.Equal(3, result[0]); + } + + [Fact] + public void ArrayPop() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.ArrPop(key, "$.nicknames", 1); + Assert.Equal("\"Ali\"", result[0].ToString()); + result = commands.ArrPop(key, "$.nicknames"); + Assert.Equal("\"Ally\"", result[0].ToString()); + result = commands.ArrPop(simpleKey); + Assert.Equal("\"Ally\"", result[0].ToString()); + } + + [Fact] + public void ArrayTrim() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.ArrTrim(key, "$.nicknames", 0, 0); + Assert.Equal(1,result[0]); + result = commands.ArrTrim(simpleKey, "$", 0, 1); + Assert.Equal(2,result[0]); + } + + [Fact] + public async Task ArrayTrimAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + await commands.SetAsync(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + await commands.SetAsync(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = await commands.ArrTrimAsync(key, "$.nicknames", 0, 0); + Assert.Equal(1,result[0]); + result = await commands.ArrTrimAsync(simpleKey, "$", 0, 1); + Assert.Equal(2,result[0]); + } + + [Fact] + public void Clear() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.Clear(key, "$.nicknames"); + Assert.Equal(1,result); + result = commands.Clear(simpleKey); + Assert.Equal(1,result); + } + + [Fact] + public async Task ClearAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + await commands.SetAsync(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + await commands.SetAsync(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = await commands.ClearAsync(key, "$.nicknames"); + Assert.Equal(1,result); + result = await commands.ClearAsync(simpleKey); + Assert.Equal(1,result); + } + + [Fact] + public void Del() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.Del(key, "$.nicknames"); + Assert.Equal(1,result); + result = commands.Del(simpleKey); + Assert.Equal(1,result); + } + + [Fact] + public async Task DelAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + await commands.SetAsync(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + await commands.SetAsync(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = await commands.DelAsync(key, "$.nicknames"); + Assert.Equal(1,result); + result = await commands.DelAsync(simpleKey); + Assert.Equal(1,result); + } + + [Fact] + public void Forget() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + commands.Set(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + commands.Set(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = commands.Forget(key, "$.nicknames"); + Assert.Equal(1,result); + result = commands.Forget(simpleKey); + Assert.Equal(1,result); + } + + [Fact] + public async Task ForgetAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var simpleKey = keys[1]; + await commands.SetAsync(key, "$", new { name = "Alice", nicknames = new[] { "Al", "Ali", "Ally" } }); + await commands.SetAsync(simpleKey, "$", new[] { "Al", "Ali", "Ally" }); + + var result = await commands.ForgetAsync(key, "$.nicknames"); + Assert.Equal(1,result); + result = await commands.ForgetAsync(simpleKey); + Assert.Equal(1,result); + } + + [Fact] + public void Get() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var complexKey = keys[1]; + commands.Set(key, "$", new Person(){Age = 35, Name = "Alice"}); + commands.Set(complexKey, "$", new {a=new Person(){Age = 35, Name = "Alice"}, b = new {a = new Person(){Age = 35, Name = "Alice"}}}); + var result = commands.Get(key); + Assert.Equal("Alice", result!.Name); + Assert.Equal(35, result.Age); + var people = commands.GetEnumerable(complexKey, "$..a").ToArray(); + Assert.Equal(2, people.Length); + Assert.Equal("Alice", people[0]!.Name); + Assert.Equal(35, people[0]!.Age); + Assert.Equal("Alice", people[1]!.Name); + Assert.Equal(35, people[1]!.Age); + } + + [Fact] + public async Task GetAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key = keys[0]; + var complexKey = keys[1]; + await commands.SetAsync(key, "$", new Person(){Age = 35, Name = "Alice"}); + await commands.SetAsync(complexKey, "$", new {a=new Person(){Age = 35, Name = "Alice"}, b = new {a = new Person(){Age = 35, Name = "Alice"}}}); + var result = await commands.GetAsync(key); + Assert.Equal("Alice", result!.Name); + Assert.Equal(35, result.Age); + var people = commands.GetEnumerable(complexKey, "$..a").ToArray(); + Assert.Equal(2, people.Length); + Assert.Equal("Alice", people[0]!.Name); + Assert.Equal(35, people[0]!.Age); + Assert.Equal("Alice", people[1]!.Name); + Assert.Equal(35, people[1]!.Age); + } + + [Fact] + public void MGet() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key1 = keys[0]; + var key2 = keys[1]; + commands.Set(key1, "$", new { a = "hello" }); + commands.Set(key2, "$", new { a = "world" }); + var result = commands.MGet(keys.Select(x => new RedisKey(x)).ToArray(), "$.a"); + + Assert.Equal("[\"hello\"]", result[0].ToString()); + Assert.Equal("[\"world\"]", result[1].ToString()); + } + + [Fact] + public async Task MGetAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(2); + var key1 = keys[0]; + var key2 = keys[1]; + await commands.SetAsync(key1, "$", new { a = "hello" }); + await commands.SetAsync(key2, "$", new { a = "world" }); + var result = await commands.MGetAsync(keys.Select(x => new RedisKey(x)).ToArray(), "$.a"); + + Assert.Equal("[\"hello\"]", result[0].ToString()); + Assert.Equal("[\"world\"]", result[1].ToString()); + } + + [Fact] + public void NumIncrby() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(1); + var key = keys[0]; + commands.Set(key, "$", new { age = 33, a = new { age = 34 }, b = new {age = "cat"} }); + var result = commands.NumIncrby(key, "$..age", 2); + Assert.Equal(35, result[0]); + Assert.Equal(36, result[1]); + Assert.Null(result[2]); + } + + [Fact] + public async Task NumIncrbyAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(1); + var key = keys[0]; + await commands.SetAsync(key, "$", new { age = 33, a = new { age = 34 }, b = new {age = "cat"} }); + var result = await commands.NumIncrbyAsync(key, "$..age", 2); + Assert.Equal(35, result[0]); + Assert.Equal(36, result[1]); + Assert.Null(result[2]); + } + + [Fact] + public void ObjectKeys() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(3); + var key = keys[0]; + commands.Set(key, "$", new { a = 5, b = 10, c = "hello", d = new { a = new { a = 6, b = "hello" }, b = 7 } }); + var result = commands.ObjKeys(key).ToArray(); + Assert.Contains("a", result[0]); + Assert.Contains("b", result[0]); + Assert.Contains("c", result[0]); + Assert.Contains("d", result[0]); + result = commands.ObjKeys(key, "$..a").ToArray(); + Assert.Empty(result[0]); + Assert.Contains("a", result[1]); + Assert.Contains("b", result[1]); + } + + [Fact] + public async Task ObjectKeysAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(3); + var key = keys[0]; + await commands.SetAsync(key, "$", new { a = 5, b = 10, c = "hello", d = new { a = new { a = 6, b = "hello" }, b = 7 } }); + var result = (await commands.ObjKeysAsync(key)).ToArray(); + Assert.Contains("a", result[0]); + Assert.Contains("b", result[0]); + Assert.Contains("c", result[0]); + Assert.Contains("d", result[0]); + result = (await commands.ObjKeysAsync(key, "$..a")).ToArray(); + Assert.Empty(result[0]); + Assert.Contains("a", result[1]); + Assert.Contains("b", result[1]); + } + + [Fact] + public void ObjectLength() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(3); + var key = keys[0]; + commands.Set(key, "$", new { a = 5, b = 10, c = "hello", d = new { a = new { a = 6, b = "hello" }, b = 7 } }); + var result = commands.ObjLen(key); + Assert.Equal(4, result[0]); + result = commands.ObjLen(key, $"$..a"); + Assert.Null(result[0]); + Assert.Equal(2, result[1]); + Assert.Null(result[2]); + + } + + [Fact] + public async Task ObjectLengthAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(3); + var key = keys[0]; + await commands.SetAsync(key, "$", new { a = 5, b = 10, c = "hello", d = new { a = new { a = 6, b = "hello" }, b = 7 } }); + var result = await commands.ObjLenAsync(key); + Assert.Equal(4, result[0]); + result = await commands.ObjLenAsync(key, $"$..a"); + Assert.Null(result[0]); + Assert.Equal(2, result[1]); + Assert.Null(result[2]); + + } + + [Fact] + public void TestMultiPathGet() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(1); + var key = keys[0]; + commands.Set(key, "$", new { a = "hello", b = new { a = "world" } }); + var res = commands.Get(key, new[] { "$..a", "$.b" }).ToString(); + var obj = JsonSerializer.Deserialize(res); + Assert.True(obj.ContainsKey("$..a")); + Assert.True(obj.ContainsKey("$.b")); + if (obj["$..a"] is JsonArray arr) + { + Assert.Equal("hello", arr[0]!.ToString()); + Assert.Equal("world", arr[1]!.ToString()); + } + else + { + Assert.True(false, "$..a was not a json array"); + } + + Assert.True(obj["$.b"]![0]!["a"]!.ToString() == "world"); + } + + [Fact] + public async Task TestMultiPathGetAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(1); + var key = keys[0]; + await commands.SetAsync(key, "$", new { a = "hello", b = new { a = "world" } }); + var res = (await commands.GetAsync(key, new[] { "$..a", "$.b" })).ToString(); + var obj = JsonSerializer.Deserialize(res); + Assert.True(obj.ContainsKey("$..a")); + Assert.True(obj.ContainsKey("$.b")); + if (obj["$..a"] is JsonArray arr) + { + Assert.Equal("hello", arr[0]!.ToString()); + Assert.Equal("world", arr[1]!.ToString()); + } + else + { + Assert.True(false, "$..a was not a json array"); + } + + Assert.True(obj["$.b"]![0]!["a"]!.ToString() == "world"); + } + + [Fact] + public void Memory() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(1); + var key = keys[0]; + + commands.Set(key, "$", new {a="hello", b=new {a="world"}}); + var res = commands.DebugMemory(key); + Assert.Equal(45, res); + res = commands.DebugMemory("non-existent key"); + Assert.Equal(0,res); + } + + [Fact] + public async Task MemoryAsync() + { + IJsonCommands commands = new JsonCommands(redisFixture.Redis.GetDatabase()); + var keys = CreateKeyNames(1); + var key = keys[0]; + await commands.SetAsync(key, "$", new {a="hello", b=new {a="world"}}); + var res = await commands.DebugMemoryAsync(key); + Assert.Equal(45, res); + res = await commands.DebugMemoryAsync("non-existent key"); + Assert.Equal(0,res); } } \ No newline at end of file