Skip to content

Commit

Permalink
add json_get/2 and json_set/3 (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
CorneliaKelinske committed Jun 28, 2023
1 parent f49ccae commit fe662ef
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 32 deletions.
105 changes: 79 additions & 26 deletions lib/cache/sandbox.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Cache.Sandbox do
use Agent

alias Cache.Redis.JSON
alias Cache.TermEncoder

@behaviour Cache

Expand Down Expand Up @@ -119,40 +120,36 @@ defmodule Cache.Sandbox do
end
end

defp put_hash_field_values(state, key, fields_values) do
Map.update(
state,
key,
Map.new(fields_values),
&Enum.reduce(fields_values, &1, fn {field, value}, acc -> Map.put(acc, field, value) end)
)
def json_get(cache_name, key, path, _opts) when path in [nil, ["."]] do
get(cache_name, key)
end

def json_get(cache_name, key, path, _opts) do
path = JSON.serialize_path(path)
Agent.get(cache_name, fn state ->
case get_in(state, [key | String.split(path, ".")]) do
nil -> {:error, ErrorMessage.not_found("ERR Path '$.#{path}' does not exist")}
value -> {:ok, value}
if contains_index?(path) do
[index | path ] = Enum.reverse(path)
with {:ok, value} <- serialize_path_and_get_value(cache_name, key, path) do
{:ok, Enum.at(value, index)}
end
end)
end

def json_set(cache_name, key, path, value, _opts) do
path = JSON.serialize_path(path)
Agent.update(cache_name, fn state ->
put_in(state, add_defaults([key | String.split(path, ".")]), value)
end)
else
serialize_path_and_get_value(cache_name, key, path)
end
end

defp add_defaults([key | keys]) do
[Access.key(key, key_default(key)) | add_defaults(keys)]
def json_set(cache_name, key, path, value, _opts) when path in [nil, ["."]] do
put(cache_name, key, stringify_value(value))
end

defp add_defaults(keys), do: keys

defp key_default(key) do
if Regex.match?(~r/\d+/, key), do: [], else: %{}
def json_set(cache_name, key, path, value, _opts) do
state = Agent.get(cache_name, & &1)
path = JSON.serialize_path(path)
with :ok <- check_key_exists(state, key),
:ok <- check_path_exists(state, key, path) do
path = add_defaults([key | String.split(path, ".")])
value = stringify_value(value)
Agent.update(cache_name, fn state ->
put_in(state, path, value)
end)
end
end

def json_incr(cache_name, key, path, incr \\ 1, _opts) do
Expand Down Expand Up @@ -215,4 +212,60 @@ defmodule Cache.Sandbox do
def hash_scan(_cache_name, _key, _scan_opts, _opts) do
raise "Not Implemented"
end

defp put_hash_field_values(state, key, fields_values) do
Map.update(
state,
key,
Map.new(fields_values),
&Enum.reduce(fields_values, &1, fn {field, value}, acc -> Map.put(acc, field, value) end)
)
end

defp check_key_exists(state, key) do
if Map.has_key?(state, key) do
:ok
else
{:error, ErrorMessage.bad_request("ERR new objects must be created at the root")}
end
end

defp check_path_exists(state, key, path) do
case get_in(state, [key | String.split(path, ".")]) do
nil -> {:ok, nil}
_ -> :ok
end
end

defp add_defaults([key | keys]) do
[Access.key(key, key_default(key)) | add_defaults(keys)]
end

defp add_defaults(keys), do: keys

defp key_default(key) do
if Regex.match?(~r/\d+/, key), do: [], else: %{}
end

defp stringify_value(value) do
value
|> TermEncoder.encode_json( )
|> TermEncoder.decode_json()
end

defp contains_index?(path) do
path
|> List.last()
|> is_integer()
end

defp serialize_path_and_get_value(cache_name, key, path) do
path = JSON.serialize_path(path)
Agent.get(cache_name, fn state ->
case get_in(state, [key | String.split(path, ".")]) do
nil -> {:error, ErrorMessage.not_found("ERR Path '$.#{path}' does not exist")}
value -> {:ok, value}
end
end)
end
end
79 changes: 73 additions & 6 deletions test/cache_sandbox_test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule CacheSandboxTest do

use ExUnit.Case, async: true

defmodule TestCache do
Expand All @@ -11,12 +12,21 @@ defmodule CacheSandboxTest do

@cache_key "SomeKey"
@cache_value 1234
@cache_path [:a, :b]
@json_test_value %{
some_integer: 1234,
some_array: [1, 2, 3, 4],
some_empty_array: [],
some_map: %{one: 1, two: 2, three: 3, four: 4}
}


setup do
Cache.SandboxRegistry.start(TestCache)
json_test_key = Base.encode32(:crypto.strong_rand_bytes(64))
:ok = TestCache.json_set(json_test_key, @json_test_value)

%{key: json_test_key}

:ok
end

describe "sandboxing caches" do
Expand All @@ -30,11 +40,25 @@ defmodule CacheSandboxTest do
end
end

describe "&json_get/1" do
test "gets full json item", %{key: key} do
assert {:ok, %{
"some_integer" => 1234,
"some_array" => [1, 2, 3, 4],
"some_empty_array" => [],
"some_map" => %{"one" => 1, "two" => 2, "three" => 3, "four" => 4}
}} === TestCache.json_get(key)
end

test "returns tuple with :ok and nil if key is not found" do
assert {:ok, nil} === TestCache.json_get("non_existing")
end
end

describe "&json_get/2" do
test "gets an item at path" do
assert :ok = TestCache.json_set(@cache_key, @cache_path, @cache_value)
assert {:ok, @cache_value} = TestCache.json_get(@cache_key, @cache_path)
assert {:ok, @cache_value} = TestCache.json_get(@cache_key, ["a.b"])
test "gets an item at path", %{key: key} do
assert {:ok, @json_test_value.some_map.one} === TestCache.json_get(key, [:some_map, :one])
assert {:ok, Enum.at(@json_test_value.some_array, 0)} === TestCache.json_get(key, [:some_array, 0])
end

test "returns :error tuple if path not found" do
Expand All @@ -47,4 +71,47 @@ defmodule CacheSandboxTest do

end
end

describe "&json_set/2" do
test "sets a full json item", %{key: key} do
assert :ok = TestCache.json_set(key, %{test: 1})
assert {:ok, %{"test" => 1}} = TestCache.json_get(key)
end
end

describe "&json_set/3" do
test "updates a json path", %{key: key} do
assert :ok = TestCache.json_set(key, [:some_map, :one], 4)
assert {:ok, 4} = TestCache.json_get(key, [:some_map, :one])
assert :ok = TestCache.json_set(key, ["some_map.one"], 5)
assert {:ok, 5} = TestCache.json_get(key, [:some_map, :one])
end

test "returns error tuple if key does not exist" do
assert {:error,
%ErrorMessage{
message: "ERR new objects must be created at the root",
code: :bad_request,
details: nil
}} === TestCache.json_set("non_existing", [:some_map, :one], 4)
end

test "returns :ok and nil if key exists but not the path", %{key: key} do
assert {:ok, nil} = TestCache.json_set(key, [:some_other_map, :two], 4)

assert {:ok,
%{
"some_integer" => 1234,
"some_array" => [1, 2, 3, 4],
"some_empty_array" => [],
"some_map" => %{"one" => 1, "two" => 2, "three" => 3, "four" => 4}
}} === TestCache.json_get(key)
end

test "ignores'.' as path", %{key: key} do
assert :ok = TestCache.json_set(key, ["."], "some value")
assert {:ok, "some value"} === TestCache.json_get(key)
assert {:ok, "some value"} === TestCache.json_get(key, ["."])
end
end
end

0 comments on commit fe662ef

Please sign in to comment.