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 } needs host gone, not just port set). Cooper.Actions emits 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, or Enum.reject/2 by 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

assemble(entries)

@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.