Cooper. Merge
(Cooper v0.1.0)
Copy Markdown
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.
Each op's own :sigil field (see Cooper.Op) decides how it combines
with whatever's already at its path:
:merge-- CASC.md §8.1's default for a plain assignment. Deep-merge "emerges" for free from folding multiple ops at different sub-paths under the same parent; a single op just sets its own leaf.:replace(~key { ... }) -- CASC.md §8.4: every leaf op a~block flattens to arrives pre-tagged:replace, but only deleting each leaf's own path wouldn't drop sibling keys the new block never mentions (~server { port = 9090 }needshostgone, not justportset).Cooper.Actionsemits one{:clear, path}entry at the statement's own path before a~block's leaves, and this module deletes that whole subtree there, once, before folding the leaves in as ordinary overrides.:append/:remove(+key/-key) -- CASC.md §8.4: list-only: concatenate, orEnum.reject/2by value equality. A tuple at that path (CASC.md §8.3: "fixed arity ... never merged, only replaced wholesale") is a hard error, not a silent no-op or coercion; same for~'s clear step landing on an existing tuple (a~key { ... }block replace only makes sense where a map/block belongs).:delete(bare-key.path) -- removes the path outright, any type.
Summary
Functions
entries is exactly what Cooper.Actions' :file/:block handlers
(and Cooper.Loader) produce: {:op, %Cooper.Op{}}, {:var, ...}
(ignored here -- Cooper.Resolver/Cooper.Loader territory), and
{:clear, path}.
Functions
@spec assemble(list()) :: {:ok, map()} | {:error, Ichor.Error.t()}
entries is exactly what Cooper.Actions' :file/:block handlers
(and Cooper.Loader) produce: {:op, %Cooper.Op{}}, {:var, ...}
(ignored here -- Cooper.Resolver/Cooper.Loader territory), and
{:clear, path}.
Every path whose final value is secret (CASC.md §4.3's "a later
op's secret? wins over an earlier one at the same path", inferred,
not stated explicitly) is wrapped in Cooper.Secret here, before
Cooper.Resolver ever sees the tree -- deliberately not deferred
to a final path-based pass over the resolved result the way an
earlier version of this pipeline did it. That earlier approach only
protected a secret at the exact path it was declared at: a %{...}
reference or a for ... from template copying that same value to a
different path (neither tracked in any static "secret paths" set,
since both only exist once Cooper.Resolver runs) copied the raw,
unwrapped value right along with it -- a real leak. Wrapping here
instead means the secrecy travels with the value itself through
every later copy, since Cooper.Resolver re-wraps whatever a
%Cooper.Secret{}'s own inner value resolves to rather than
stripping the wrapper.