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§
- Ureq
Fetcher - The production
RemoteFetcher: a blockingureqGET per URL, with an in-memory fetch-at-most-once-per-instance cache. See this module’s doc comment for whyureqand why the cache is scoped down from the Go original’s persistentfilecache.Cache.
Traits§
- Remote
Fetcher - Fetches a fragment or file’s raw bytes from a URL. Injected through
resolve_extends/load_caproni_configso tests can substitute awiremock-backed double for the realUreqFetcher; see this module’s doc comment for the full design rationale.
Functions§
- deep_
merge - Deep-merges
srcintodst(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 explicitnullinsrcdeletes the key from the result at any depth.dstis not modified; the merged result is returned as a new value.