Cheatsheet

Copy Markdown

Quick 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

OptionShapeDefaultEffect
:env%{String.t() => String.t()}System.get_env/0What ${...} reads from.
:rootString.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 address

Cooper.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 value

Any 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 = "...") — :redacted is nil; inspect/to_string render 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 a for ... from template 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:

StageRaised for
:lexer / :parserCASC syntax the grammar itself rejects.
:actionA 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).
:loopA for loop's bindings are invalid (index-only loop, mismatched element-list lengths, a %{...} used as an iterable).
:importA file couldn't be read, an import cycle, or an unregistered scheme://.
:mergeA ~/+/- sigil applied to a value it can't operate on (e.g. +/- against a tuple).
:resolveAn 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:

ModuleStage
Cooper.Grammar / Cooper.ActionsLex + parse into flattened ops.
Cooper.LoopExpand for statements into the ops they generate.
Cooper.LoaderResolve and recursively load import statements.
Cooper.MergeFold flattened ops into a tree.
Cooper.ResolverResolve everything left unresolved (@{}/${}/%{}/!{}/!Name()).