API Reference Cooper v#0.1.0
Copy MarkdownModules
Parses and loads CASC config files (see CASC.md) into native Elixir
terms: maps (string keys), atoms, real tuples (never lists --
CASC.md §6.11 is explicit that this is a correctness requirement, not
an implementation detail), and Cooper.Secret-wrapped values for any
*key-prefixed secret.
Ichor.Actions for casc.aether. Turns the raw parse into
Internal marker wrapping a { ... } block's already-flattened entries,
distinguishing "this statement's right-hand side was a block" from "was
a scalar value" (Cooper.Actions' own handle_rule/3 clause for
:kv_statement branches on it). A struct rather than a tagged tuple
deliberately --
CASC tuples (§6.11) are real Elixir tuples built from user data, and a
plain {:block_ops, list} tag could theoretically collide with one; a
struct never can.
Shared bitwise CIDR math for Cooper.IPv4/Cooper.IPv6 -- both
address families need the identical operations (prefix -> mask,
address <-> integer, network/broadcast/host-range math), differing
only in bit width (32 vs 128) and part width (8-bit octets vs 16-bit
hextets), so this holds one implementation instead of two
near-copies. Not meant to be called directly -- use Cooper.IPv4/
Cooper.IPv6's own functions, which already know their own bit width.
Stringifies a resolved value for embedding into a string-interpolation
segment -- shared by Cooper.Loop (a loop-bound value substituted into
an interpolated destination path or body string) and Cooper.Resolver
(an ordinary @{}/${}/%{}/!{}/!Name() result embedded the same
way, CASC.md §7).
Orchestrates casc.aether parsing against Cooper.NativeGrammar
(Grammar.Native, compiled ahead of time by mix ichor.gen -- see
its own moduledoc). Grammar.VM -- the interpreted bytecode backend
that ichor proper still ships -- is no longer reachable from
anything under lib/ at all: ichor is an only: [:dev, :test], runtime: false dependency now (the ichor/ichor_runtime split),
so nothing a mix release build ships is allowed to call into it.
Cooper.Test.VMParity (test/support/, compiled only under
MIX_ENV=test) keeps the old Grammar.VM path alive purely for
bench/native_vs_vm.exs and test/cooper/backend_parity_test.exs.
A parsed IPv4 address, with an optional CIDR prefix (CASC.md §6.7 --
127.0.0.1 or 127.0.0.1/32). new/2 is the only way to build one,
and it's the same validation Cooper.Actions runs on a bare literal
at parse time -- an out-of-range octet or CIDR prefix is a load-time
Ichor.Error, never a crash or a silently-accepted garbage value.
A parsed IPv6 address, with an optional CIDR prefix (CASC.md §6.7 --
::1 or ::1/128). new/2 is the only way to build one, and it's
the same validation Cooper.Actions runs on a bare literal at parse
time -- a malformed address or an out-of-range CIDR prefix is a
load-time Ichor.Error, never a crash or a silently-accepted garbage
value.
A double-quoted string's content, once it's been found to contain at
least one interpolation reference -- an ordered list of literal string
runs and Cooper.Ref.* nodes. Cooper.Resolver resolves each segment
and joins the results into the final string. A double-quoted string with no
references at all is represented as a plain Elixir string instead
(Cooper.Actions never wraps pure literal text in this struct) --
keeps the common case cheap and keeps existing scalar-string
assumptions elsewhere intact.
Ichor.Actions for casc_interp.aether -- parses a double-quoted
string's already escape-processed content into an ordered list of
literal string runs and Cooper.Ref.* nodes.
Loads casc_interp.aether. Used only for a double-quoted string's
content (CASC.md §7) -- see that grammar file's own moduledoc-style
comment for why bare interpolated values in the main grammar don't go
through here.
Duration (CASC.md §6.8) and byte-size (§6.9) text parsing, shared
between Cooper.Actions (the bare 500ms/512MiB literal forms) and
Cooper.Resolver (the equivalent !duration("5m")/!bytes("512MiB")
tagged-value forms, §7.5 -- "reachable ... for computed/interpolated
values"). Same rules, same errors, one implementation.
Resolves and loads a CASC.md §5.1 import "..." statement, called
directly from Cooper.Actions' own handle_rule/3 clause for
:import_statement (not a separate, later pass) -- an import has to
be resolved inline, at the point it's parsed, so that variables it
brings in are visible
to whatever follows it in the importing file, for loops included,
exactly the way an ordinary @name = ... declaration already is.
Expands a parsed for statement (CASC.md §5.5) into the ops it
generates.
Folds the flattened op/var-decl/clear-scope entries Cooper.Actions
(and Cooper.Loader, for imports) produce into (almost) the final
tree -- CASC.md §8/§5.7. "Almost": leaf values may still be unresolved
Cooper.Ref.* nodes or Cooper.Merge.Layered values (a lazy
for-loop base with overrides on top) -- Cooper.Resolver walks
this next.
A path whose value is a lazy base (a for-loop's from <template>,
CASC.md §5.5, resolved by Cooper.Resolver against the final merged
tree) with literal statements layered on top as overrides --
Cooper.Merge builds this whenever a later op writes underneath a
path currently holding a Cooper.Ref.Config. overrides mirrors the
assembled tree's own shape (a plain nested map, itself possibly
containing more Layered values at deeper paths); Cooper.Resolver
resolves base, deep-merges overrides on top, and that's this
path's final value.
@native("Cooper.Native.ResolverRef", "scan") implementation for
RESOLVER_REF_RAW (casc.aether/casc_interp.aether): scans a
!{name:payload} resolver reference (CASC.md §7.4) with genuinely
unbounded brace nesting in payload, tracking depth as it goes.
casc.aether, compiled to direct Elixir function calls -- ~2x
throughput per Ichor's own docs, now that the grammar has stopped
changing weekly.
casc_interp.aether, compiled the same way Cooper.NativeGrammar is
(see its own moduledoc for the full ahead-of-time-generation
rationale). This sub-grammar is invoked dynamically -- once per
double-quoted string literal encountered during parsing, not once
overall -- but that's an argument for native codegen here, not
against it: under Grammar.VM (Cooper.Test.VMParity.run_interp_vm/1,
test/support only), every one of those calls re-parses
casc_interp.aether's own source text from scratch; here it's
compiled exactly once, ahead of Cooper's own build, regardless of how
many string literals a given file has.
One flattened leaf assignment: every assignment or block statement --
regardless of surface form, dotted path, or nested block -- resolves
recursively into a list of these before merge ever runs, so the merge
engine (Cooper.Merge) only ever deep-merges paths, never
re-discovers which parts of two differently-shaped literals mean the
same key.
An unresolved %{path} config reference (CASC.md §7.3). Lazy --
resolved by Cooper.Resolver only after Cooper.Merge has run,
against the fully merged final tree; cycle detection happens there
too.
An unresolved ${NAME} environment reference (CASC.md §7.2). Eager,
same timing as Cooper.Ref.Var -- resolved by Cooper.Resolver,
always to a string unless wrapped in a tagged value (!int(...) etc).
An unresolved !{resolver:payload} extensible-resolver call (CASC.md
§7.4). payload is carried verbatim (never parsed further) -- giving
it meaning is entirely the consumer-registered resolver's job, wired
up via Cooper.Resolver and the :resolvers option (see Cooper).
An unregistered resolver name is a hard error at resolve time, never
silently passed through.
An unresolved !Name(arg) tagged value (CASC.md §7.5). The grammar
only recognizes "a tag plus one parenthesized argument" -- arg is
already a fully evaluated ordinary value (recursively -- it may
itself be a nested ref); giving name meaning (built-in int/
float/bool/duration/bytes, or a consumer-registered tag) is
Cooper.Resolver's job, via the :tags option (see Cooper).
An unresolved @{name} variable reference (CASC.md §7.1). Eager --
Cooper.Resolver resolves these against the per-file variable
environment, independent of the final merged tree.
Shared across Cooper.Actions and Cooper.InterpActions: the
:ref_suffix (:default/:+alt/:?"msg", CASC.md §7.2's "one rule,
not three near-identical ones", reused across @{}/${}/%{} in
both grammars) and :env_bracket (${NAME[i]} vs ${NAME[]})
handling, plus the small eval helpers every custom handle_rule in
either module needs.
Resolves everything Cooper.Merge.assemble/1 left unresolved --
Cooper.Ref.Var (@{}, eager), Cooper.Ref.Env (${}, eager),
Cooper.Ref.Config (%{}, lazy against the final tree),
Cooper.Ref.Resolver (!{resolver:payload}, dispatch), Cooper.Ref.Tagged
(!Name(arg), dispatch), and Cooper.Merge.Layered (a for loop's
lazy from base with overrides on top, see Cooper.Loop).
Wraps a secret leaf value (CASC.md §4.3's *key-prefixed keys) in the
final result Cooper.load_file/2/Cooper.load_string/2 return, so it
never leaks by accident: inspect/1 (via IO.inspect/2, pattern-match
failures, logging, ...) and to_string/1 both redact regardless of the
wrapped value. Reach the real value deliberately via reveal/1 (or the
:value field directly) -- the wrapping is only ever a display-time
concern, never a barrier to the developer's own deliberate use of the
real value; there would be no point config-loading a secret otherwise.
A parsed @name = value / @*name = value variable declaration
(CASC.md §5.2). Resolution against a variable environment, and
public/private visibility across imports, is Cooper.Loader/
Cooper.Resolver territory -- this struct only carries what the
grammar can determine locally.