Current section
Files
Jump to
Current section
Files
lib/static_context.ex
defmodule StaticContext do
@moduledoc """
Generates standard data access functions for static lookup modules at compile time.
Static lookup modules hold lists of structs defined in code, not the database —
think lookup tables like states, types, or categories. The `static_context` block
is the table of contents; everything below it is domain logic.
String ids throughout. Matches DB storage, URL params, and form values with no
atom/string boundary to cross.
## Usage
import StaticContext
static_context struct: MineState do
list()
list_by()
get()
get!()
get_by()
get_by!()
end
def entries do
[
%MineState{id: "production", name: "Production"},
%MineState{id: "care_and_maintenance", name: "Care and Maintenance"},
%MineState{id: "closed", name: "Closed"}
]
end
## Generated functions
| Function | Signature | Notes |
|-------------|------------------------|--------------------------------------------|
| `list` | `list()` | All entries via `entries/0` |
| `list_by` | `list_by(clauses)` | Enum.filter with clause key validation |
| `list_for` | `list_for(assoc, id)` | Filter by `assoc_id` foreign key |
| `get` | `get(id)` | Binary-only guard, nil if not found |
| `get!` | `get!(id)` | Binary-only guard, raises if not found |
| `get_by` | `get_by(clauses)` | First match or nil |
| `get_by!` | `get_by!(clauses)` | First match or raises |
`get` and `get!` are guarded with `when is_binary(id)`. Passing an atom raises
`FunctionClauseError` — intentional, enforces string ids at the call site.
"""
@doc """
Declares the generated functions for a static lookup module.
Accepts a keyword list with `:struct` pointing to the struct module,
and a `do` block containing function declarations like `list()`, `get!()`, `get_by()`.
The calling module must define an `entries/0` function returning a list of structs.
See the module documentation for the full list of supported functions.
"""
defmacro static_context(context_opts_ast, do: block) do
{context_opts, _} =
context_opts_ast
|> Macro.expand(__CALLER__)
|> Code.eval_quoted([], __CALLER__)
for declaration <- parse_declarations(block, __CALLER__) do
declaration
|> generate_function_string(context_opts)
|> Code.string_to_quoted!()
end
end
@doc false
@spec generate_function_string(%{type: atom(), opts: keyword()}, keyword()) :: String.t()
def generate_function_string(%{type: type, opts: _opts}, context_opts) do
Path.join([
:code.priv_dir(:static_context) |> to_string(),
"templates",
"static_context",
"#{type}.ex.eex"
])
|> EEx.eval_file(context_opts)
end
###########################################################################
### AST parsing
###########################################################################
@doc false
@spec parse_declarations(Macro.t(), Macro.Env.t()) :: [%{type: atom(), opts: keyword()}]
def parse_declarations({:__block__, _, calls}, caller_env) do
Enum.map(calls, &parse_single_declaration(&1, caller_env))
end
def parse_declarations(ast_call, caller_env),
do: [parse_single_declaration(ast_call, caller_env)]
defp parse_single_declaration({function_name, _, raw_args}, caller_env) do
{opts_ast} =
case raw_args do
[] -> {[]}
[opts] -> {opts}
end
{evaluated_opts, _} = Code.eval_quoted(opts_ast, [], caller_env)
%{type: function_name, opts: evaluated_opts}
end
end