This repository has been archived by the owner on Jan 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
agency.ex
144 lines (114 loc) · 3.94 KB
/
agency.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
defmodule Agency do
@moduledoc """
`Agency` is an abstraction layer above `Agent` allowing to use any
container supporting `Access` behind and simplifying the client API
handling.
`Agency` itself implements `Access` behaviour, making it possible to
embed instances in the middle of `Access.keys` chains.
In a nutshell, `Agency` backs up the `Agent` holding a container.
All the standard CRUD-like calls are done through containers’
`Access` implementation, allowing transparent shared access.
The set of `before_***/1` and `after_***/1` functions are introduced,
so that the main `Agent` feature distinguishing it from the standard
`GenServer` holding state—namely, a separation of client and server
APIs—is exposed transparently to the consumers.
Consider the following example.
```elixir
defmodule MyAgent do
use Agency, into: %{} # default
def after_get(value) do
value + 1
end
...
end
```
The above code introduces an `Agent` backing up `Map` and
exposes the standard CRUD-like functionality. After the value
would be got from the server API, it’d be increased by `1`
and returned to the consumer.
### Options
`use Agency` accepts two options:
- `name: GenServer.name()` the name, this process will be identified by.
Optional, if not given the only instance of the `Agent` is allowed.
If given, all the subsequent calls to CRUD functions must include
this name as the first parameter. In that case, multiple instances
are allowed
- `into: Access.t()` the container to be used by `Agent`
- `data: map() | keyword()` the static data to be held by the instances
---
One might also pass any struct, or whatever else implementing
`Access` as `into:` option to be used as an `Agent` container.
"""
@type key :: any()
@type keys :: [key()]
@type keyz :: key() | keys()
@type value :: any()
@functions [
get: 1,
get_and_update: 2,
pop: 1,
put: 2,
update: 2
]
@doc false
def __functions__, do: @functions
# callbacks
@doc """
The callback that is called from all the interface
methods (but `this/0`) right before the `Agent`
is to be called.
Specific handlers take precedence over this one.
"""
@callback before_all(key()) :: key()
Enum.each(@functions, fn {fun, arity} ->
@doc """
The callback that is called from `#{fun}/#{arity}` right before
the `Agent` is to be called, passing key as an argument.
It should return a modified key. Note that the `key` here
is _always_ represented a list, even if the single value
was passed to the interface function.
"""
@callback unquote(:"before_#{fun}")(key) :: key when key: keys()
@doc """
The callback that is called from `#{fun}/#{arity}` right after
the `Agent` has returned the value, passing this value as
a parameter.
"""
@callback unquote(:"after_#{fun}")(value) :: value when value: value()
end)
@doc """
The callback that is called from `this/0` right after
the `Agent` has returned the value, passing this value as
a parameter.
"""
@callback after_this(value()) :: Access.t()
@doc """
Creates an `Agent` module.
If the module is already loaded, this is a no-op.
"""
@spec agent!(name :: binary() | atom()) :: module()
def agent!(name, opts \\ [])
def agent!(name, opts) when is_binary(name),
do: agent!(Module.concat(__MODULE__, String.capitalize(name)), opts)
def agent!(name, opts) when is_atom(name) do
opts = Macro.escape(opts)
case Code.ensure_compiled(name) do
{:module, module} ->
module
{:error, _reason} ->
{:module, module, _, _} =
Module.create(
name,
quote(do: use(Agency.Scaffold, unquote(opts))),
Macro.Env.location(__ENV__)
)
module
end
end
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
use Agency.Scaffold, opts
end
end
end