API Reference Cooper v#0.1.0

Copy Markdown

Modules

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.