Cheatsheet
Copy MarkdownQuick reference for Cooper's public API. See the
tutorial for a walkthrough, or
CASC.md for the full language reference.
Load config
Cooper.load_file(path, opts \\ [])
#=> {:ok, term()} | {:error, Ichor.Error.t() | [Ichor.Error.t()]}
Cooper.load_string(source, opts \\ [])
#=> {:ok, term()} | {:error, Ichor.Error.t() | [Ichor.Error.t()]}load_file/2 reads path and derives :root/import-cycle detection
from it automatically — the ordinary way to load a real file.
load_string/2 is for source that isn't (yet, or ever) in a file —
pass :root explicitly if it uses bare (non-scheme://) imports.
Options
| Option | Shape | Default | Effect |
|---|---|---|---|
:env | %{String.t() => String.t()} | System.get_env/0 | What ${...} reads from. |
:root | String.t() | path's directory (load_file/2) / File.cwd!/0 (load_string/2) | Where a bare import "..." resolves relative to. |
:resolvers | %{String.t() => (payload :: String.t() -> {:ok, term()} | {:error, term()})} | %{} | One function per !{resolver:...} name (CASC.md §7.4). Unregistered use is a load-time error. |
:tags | %{String.t() => (arg :: term() -> {:ok, term()} | {:error, term()})} | %{} | One function per !Name(...) beyond the 5 built-ins (CASC.md §7.5). Unregistered use is a load-time error. |
:import_schemes | %{String.t() => (rest :: String.t() -> {:ok, String.t()} | {:error, term()})} | %{} | One loader per import "scheme://..." scheme (CASC.md §5.1). Unregistered use is a load-time error. |
Built-in tags
!int(arg), !float(arg), !bool(arg), !duration(arg),
!bytes(arg) — always registered, need no :tags entry.
!duration(...) and a bare duration literal (500ms) both produce
{:duration, nanoseconds}; !bytes(...) and a bare byte-size literal
(512MiB) both produce {:bytes, count}.
IP addresses and CIDR
A bare 127.0.0.1/127.0.0.1/32 (CASC.md §6.7) literal resolves to
%Cooper.IPv4{}; ::1/::1/128 resolves to %Cooper.IPv6{}. Both
are validated at load time — an out-of-range octet, a malformed
address, or a CIDR prefix outside 0..32/0..128 is a load-time
error, not a crash or a silently-accepted value.
%Cooper.IPv4{address: :inet.ip4_address(), prefix: 0..32 | nil}
%Cooper.IPv6{address: :inet.ip6_address(), prefix: 0..128 | nil}
Cooper.IPv4.new(address, prefix \\ nil) #=> {:ok, t()} | {:error, String.t()}
Cooper.IPv4.contains?(cidr, other) #=> boolean() -- is `other` inside `cidr`'s block?
Cooper.IPv4.network(cidr) #=> the block's network address
Cooper.IPv4.broadcast(cidr) #=> the block's broadcast address (IPv4 only -- IPv6 has no equivalent)
Cooper.IPv4.netmask(cidr) #=> the netmask implied by the block's prefix
Cooper.IPv4.first_host(cidr) #=> first usable host address
Cooper.IPv4.last_host(cidr) #=> last usable host addressCooper.IPv6 has the identical API minus broadcast/1. Both
implement String.Chars, rendering back in CASC's own literal syntax
("127.0.0.1/24"), including when embedded in an interpolated string.
Secrets
%Cooper.Secret{value: term(), redacted: String.t() | nil}
Cooper.Secret.reveal(secret)
#=> the real, fully unmasked valueAny key segment prefixed with * (*key, CASC.md §4.3) comes back
wrapped in Cooper.Secret. inspect/1 and to_string/1 redact
unconditionally — reveal/1 (or the :value field directly) is the
only way to the real value.
- Whole-value secret (
*password = "...") —:redactedisnil;inspect/to_stringrender the blanket"[~~REDACTED~~]"marker. - Secret embedded in a larger interpolated string
(
"conn=%{database.password}") — only that portion redacts, not the whole string ("conn=[~~REDACTED~~]"); several secrets interpolated into one string each redact independently at their own position. - Travels with the value, not just its declared path — a
%{...}reference or afor ... fromtemplate that copies a secret value elsewhere in the tree copies the wrapping too; a tag (!int(...)) or index ([i]) applied to a secret-sourced value re-wraps its result.
Errors
Every stage returns {:error, Ichor.Error.t() | [Ichor.Error.t()]} on
failure — never a raised exception. error.message is a human-readable
string; error.stage says roughly where it came from:
| Stage | Raised for |
|---|---|
:lexer / :parser | CASC syntax the grammar itself rejects. |
:action | A literal that parses but fails semantic validation (e.g. a malformed duration/bytes/IP literal, an out-of-range CIDR prefix, an unexpected version-header token). |
:loop | A for loop's bindings are invalid (index-only loop, mismatched element-list lengths, a %{...} used as an iterable). |
:import | A file couldn't be read, an import cycle, or an unregistered scheme://. |
:merge | A ~/+/- sigil applied to a value it can't operate on (e.g. +/- against a tuple). |
:resolve | An undefined @{}/${} reference with no default, a ${NAME:?"msg"} failure, a %{...}/@{...} reference cycle, or an unregistered resolver/tag. |
Test-time injection
Every option that would otherwise touch the real filesystem, OS
environment, or an external service is a plain function/map you supply
— nothing in Cooper reaches for global state on its own:
Cooper.load_string(source,
env: %{"DB_PASSWORD" => "test-secret"},
resolvers: %{"vault" => fn payload -> {:ok, "stub:" <> payload} end},
import_schemes: %{"mem" => fn _rest -> {:ok, other_source} end}
)Pipeline modules
Only relevant if you need to call a stage directly instead of going
through Cooper.load_file/2/load_string/2:
| Module | Stage |
|---|---|
Cooper.Grammar / Cooper.Actions | Lex + parse into flattened ops. |
Cooper.Loop | Expand for statements into the ops they generate. |
Cooper.Loader | Resolve and recursively load import statements. |
Cooper.Merge | Fold flattened ops into a tree. |
Cooper.Resolver | Resolve everything left unresolved (@{}/${}/%{}/!{}/!Name()). |