Skip to main content

Module merge

Module merge 

Source
Expand description

Port of internal/config/merge.go: extends:/local-override composition, RFC 7386-style deep merge with append-list semantics for a handful of keys, and the caproni-project.yaml per-repository fragment machinery.

§Remote HTTP fragment resolution: ureq (blocking), not reqwest/async

The Go original threads a *http.Client through resolveExtends, doing ordinary blocking HTTP calls — Go’s goroutine model makes a blocking call cheap and doesn’t force the whole call graph onto a runtime. Every other function in this crate is synchronous (load_caproni_config and everything it calls), so introducing an async HTTP client (reqwest) here would mean either colouring load_caproni_config and its ~15 callers async fn for the sake of one HTTP call three layers deep, or spinning up a throwaway Tokio runtime inside a sync function just to .block_on() an async request — both worse than the alternative. ureq (a small, dependency-light blocking HTTP client with its own rustls-backed TLS) keeps resolve_extends/load_caproni_config fully synchronous, matching the Go original’s call shape exactly: a RemoteFetcher trait object is threaded through the same call chain the Go original threads *http.Client through, with UreqFetcher as the production implementation.

Testing shape: RemoteFetcher is itself the injection seam UreqFetcher’s HTTP logic sits behind, so exercising resolve_extends/fetch_deployer_files doesn’t require a real network call at all — an earlier draft of this module planned a wiremock+ tokio mock-HTTP-server setup (rewriting the https://-only scheme check’s URL to a real http://127.0.0.1:<port> mock address before dispatch, wrapping each test in tokio::task::spawn_blocking since wiremock’s server is async and this crate’s HTTP calls are blocking), but every remote-fragment test in merge.rs’s test module instead uses an in-process FakeFetcher: an in-memory HashMap<String, Vec<u8>> keyed by the exact https://fake-remote.test/... URL a test’s caproni.yaml fixture embeds. This tests the same call-graph behavior (resolve_extends dispatching to RemoteFetcher::fetch, size-limit and not-found handling, caching) without a real socket, TLS story, or async runtime anywhere in the crate — wiremock/tokio were dropped from dev-dependencies once this was confirmed sufficient.

Caching, scoped down from the Go original: the Go original threads a filecache.Cache (a persistent, on-disk, TTL-based cache with a stale-fallback path) through the same call chain, shared across a whole caproni invocation so the same URL is fetched at most once per process. UreqFetcher reproduces only the “fetched at most once per UreqFetcher instance” behavior (an in-memory HashMap keyed by URL, populated on first fetch) — no on-disk persistence, no TTL, no stale-fallback. Nothing in this crate’s test suite exercises staleness or cross-process caching (TestLoadCaproniConfig_ExtendsRemoteFragment’s "caching" subtest only asserts that two load_caproni_config calls sharing one fetcher instance issue one HTTP request between them), so the full filecache port is deferred rather than built against no test that needs it.

Structs§

UreqFetcher
The production RemoteFetcher: a blocking ureq GET per URL, with an in-memory fetch-at-most-once-per-instance cache. See this module’s doc comment for why ureq and why the cache is scoped down from the Go original’s persistent filecache.Cache.

Traits§

RemoteFetcher
Fetches a fragment or file’s raw bytes from a URL. Injected through resolve_extends/load_caproni_config so tests can substitute a wiremock-backed double for the real UreqFetcher; see this module’s doc comment for the full design rationale.

Functions§

deep_merge
Deep-merges src into dst (RFC 7386-ish): maps merge recursively, a handful of well-known list keys (additional_hostnames, lifecycle_hooks, dynamic_overrides) append rather than replace, other lists replace wholesale, and an explicit null in src deletes the key from the result at any depth. dst is not modified; the merged result is returned as a new value.