Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.2.0 ()

- Added `:on` option to `@cache` decorator — allows conditional caching based on the function result.

## 0.1.1 (2025-10-03)

- Loosening the required elixir version to `~> 1.15` (previously `~> 1.18`)
Expand Down
105 changes: 66 additions & 39 deletions lib/cache_decorator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ defmodule CacheDecorator do

Then decorate your functions with:

- `@cache key: "cache_key_template"`
- `@cache key: "cache_key_template", on: <pattern or list_of_patterns>`
- Caches the result of the function under the given key.
- The key can contain placeholders like `{arg_name}` that will be replaced
- The `:key` can contain placeholders like `{arg_name}` that will be replaced
by the string representation of the corresponding function argument.
- Optionally provide additional options like `ttl` in the attribute.
- The `:on` option allows specifying one or multiple patterns to match
against the result of the function call; result will be cached only
if it matches one of these patterns.
- If `:on` is omitted, cache happens for any function call result.
- You can provide additional options like `ttl` which
will be passed to `cache_module.put/4` as `opts`

- `@invalidate key: "cache_key_template", on: <pattern or list_of_patterns>`
- Invalidates the cache entry for the given key when the decorated function
is called.
- Invalidates the cache entry for the given key.
- The `:key` can contain placeholders like `{arg_name}` that will be replaced
by the string representation of the corresponding function argument.
- The `:on` option allows specifying one or multiple patterns to match
against the result of the function call; cache invalidation only occurs
if the result matches one of these patterns.
Expand Down Expand Up @@ -211,9 +217,13 @@ defmodule CacheDecorator do
for {name, args_ast, opts} <- specs do
Module.make_overridable(env.module, [{name, length(args_ast)}])

key_template = Keyword.fetch!(opts, :key)
{internal_opts, opts} = Keyword.split(opts, [:key, :on])

key_template = Keyword.fetch!(internal_opts, :key)
key_ast = compile_key_ast!({key_template, args_ast, env.module, name, :cache})

on_pattern = Keyword.get(internal_opts, :on, :no_on_pattern)

quote do
def unquote(name)(unquote_splicing(args_ast)) do
key = unquote(key_ast)
Expand All @@ -222,9 +232,7 @@ defmodule CacheDecorator do
{:ok, nil} ->
value = super(unquote_splicing(args_ast))

:ok = @cache_module.put(@cache_opts, key, value, unquote(opts))

value
unquote(put_cache(opts, on_pattern))

{:ok, value} ->
value
Expand All @@ -238,46 +246,68 @@ defmodule CacheDecorator do
end
end

defp put_cache(opts, :no_on_pattern) do
quote do
:ok = @cache_module.put(@cache_opts, key, value, unquote(opts))

value
end
end

defp put_cache(opts, on_pattern) do
cache_ast = put_cache(opts, :no_on_pattern)

quote do
case value do
unquote(compile_on_patterns_ast(on_pattern, cache_ast))
end
end
end

defp handle_invalidations(env) do
invalidations = Module.get_attribute(env.module, :invalidations) || []

for {name, args, opts} <- invalidations do
Module.make_overridable(env.module, [{name, length(args)}])
for {name, args_ast, opts} <- invalidations do
Module.make_overridable(env.module, [{name, length(args_ast)}])

raw_key = Keyword.fetch!(opts, :key)
key_ast = compile_key_ast!({raw_key, args, env.module, name, :invalidate})
{internal_opts, opts} = Keyword.split(opts, [:key, :on])

invalidate_ast =
quote do
:ok = @cache_module.del(@cache_opts, unquote(key_ast))
end
raw_key = Keyword.fetch!(internal_opts, :key)
key_ast = compile_key_ast!({raw_key, args_ast, env.module, name, :invalidate})

function_body_ast =
if on = Keyword.get(opts, :on) do
quote do
case super(unquote_splicing(args)) do
unquote(compile_on_patterns_ast(on, invalidate_ast))
end
end
else
quote do
result = super(unquote_splicing(args))
on_pattern = Keyword.get(internal_opts, :on, :no_on_pattern)

unquote(invalidate_ast)
quote do
def unquote(name)(unquote_splicing(args_ast)) do
key = unquote(key_ast)

result
end
end
value = super(unquote_splicing(args_ast))

quote do
def unquote(name)(unquote_splicing(args)) do
unquote(function_body_ast)
unquote(put_invalidation(opts, on_pattern))
end
end
end
end

defp compile_on_patterns_ast(on, invalidate_ast) do
defp put_invalidation(_opts, :no_on_pattern) do
quote do
:ok = @cache_module.del(@cache_opts, key)

value
end
end

defp put_invalidation(opts, on_pattern) do
invalidation_ast = put_invalidation(opts, :no_on_pattern)

quote do
case value do
unquote(compile_on_patterns_ast(on_pattern, invalidation_ast))
end
end
end

defp compile_on_patterns_ast(on, action_ast) do
on = List.wrap(on)

unmatched_pattern_ast =
Expand All @@ -288,10 +318,7 @@ defmodule CacheDecorator do
patterns_ast =
for pattern <- on do
quote do
unquote(pattern) = result ->
unquote(invalidate_ast)

result
unquote(pattern) -> unquote(action_ast)
end
end

Expand Down
80 changes: 79 additions & 1 deletion test/cache_decorator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ defmodule CacheDecoratorTest do
@cache key: "test_{value1}_{value2}"
def cache_with_multiple_args(value1, value2), do: {value1, value2}

@cache key: "test_{value}", on: quote(do: {:ok, _result})
def cache_with_single_on_pattern(value, return) do
_ = value

return
end

@cache key: "test_{value}", on: [{:ok, 1}, {:ok, 2}]
def cache_with_multiple_on_patterns(value, return) do
_ = value

return
end

@invalidate key: "test_{value}"
def invalidate_without_on_pattern(value), do: value

Expand Down Expand Up @@ -98,7 +112,7 @@ defmodule CacheDecoratorTest do
end

describe "cache_with_multiple_args/1" do
test "cachess value" do
test "caches value" do
value1 = random()
value2 = random()
cache_key = "test_#{value1}_#{value2}"
Expand All @@ -113,6 +127,70 @@ defmodule CacheDecoratorTest do
end
end

describe "cache_with_single_on_pattern/1" do
test "caches value when on: pattern matches" do
value = random()
return = {:ok, value}
cache_key = "test_#{value}"

assert ^return = Example.cache_with_single_on_pattern(value, return)
assert ^return = Example.cache_with_single_on_pattern(value, return)

assert_called! Cachex, :get, args: [@cache_name, ^cache_key], times: 2
assert_called! Cachex, :put, args: [@cache_name, ^cache_key, ^return, []], times: 1
end

test "doesn't cache when on: pattern doesn't match" do
value = random()
return = {:error, "reason"}
cache_key = "test_#{value}"

assert ^return = Example.cache_with_single_on_pattern(value, return)
assert ^return = Example.cache_with_single_on_pattern(value, return)

assert_called! Cachex, :get, args: [@cache_name, ^cache_key], times: 2
refute_called! Cachex, :put, args: [@cache_name, ^cache_key, ^return, []]
end
end

describe "cache_with_multiple_on_pattern/1" do
test "caches value when first on: pattern matches" do
value = random()
return = {:ok, 1}
cache_key = "test_#{value}"

assert ^return = Example.cache_with_multiple_on_patterns(value, return)
assert ^return = Example.cache_with_multiple_on_patterns(value, return)

assert_called! Cachex, :get, args: [@cache_name, ^cache_key], times: 2
assert_called! Cachex, :put, args: [@cache_name, ^cache_key, ^return, []], times: 1
end

test "caches value when second on: pattern matches" do
value = random()
return = {:ok, 2}
cache_key = "test_#{value}"

assert ^return = Example.cache_with_multiple_on_patterns(value, return)
assert ^return = Example.cache_with_multiple_on_patterns(value, return)

assert_called! Cachex, :get, args: [@cache_name, ^cache_key], times: 2
assert_called! Cachex, :put, args: [@cache_name, ^cache_key, ^return, []], times: 1
end

test "doesn't cache when on: pattern doesn't match" do
value = random()
return = {:ok, 3}
cache_key = "test_#{value}"

assert ^return = Example.cache_with_multiple_on_patterns(value, return)
assert ^return = Example.cache_with_multiple_on_patterns(value, return)

assert_called! Cachex, :get, args: [@cache_name, ^cache_key], times: 2
refute_called! Cachex, :put, args: [@cache_name, ^cache_key, ^return, []]
end
end

describe "invalidate_without_on_pattern/1" do
test "invalidates cache" do
value = random()
Expand Down