cratefield_cli/sidecars.rs
1//! What the CLI can know about sidecar-mounted modules (issue #66).
2//!
3//! `fz` is linked into the venture, so it sees exactly the modules that
4//! were compiled in. A sidecar is a separate deployment: its tables, its
5//! migrations and its SQL live in someone else's repository, and no tool
6//! here can read them. The mount table is the one thing the venture does
7//! know, so every command that walks `harness.modules()` and would
8//! otherwise quietly under-report says what it cannot see.
9//!
10//! The table comes from `--sidecars '<json>'` or, unset, from the
11//! `HARNESS_SIDECARS` environment variable — the same table the runtime
12//! reads from config. It is passed down from `run` rather than read
13//! here, so nothing below the command line depends on process state.
14
15use cratefield_core::{
16 HARNESS_ONE_WORKER, HARNESS_SIDECARS, Harness, MapConfig, SidecarMount, SidecarMounts,
17};
18
19/// The mount table the command line gave, else the environment's.
20#[must_use]
21pub fn from_cli_or_env(flag: Option<&str>) -> Option<String> {
22 flag.map(str::to_owned)
23 .or_else(|| std::env::var(HARNESS_SIDECARS).ok())
24 .filter(|table| !table.trim().is_empty())
25}
26
27/// Parses the table. `None` is `Ok(empty)`: most ventures mount no
28/// sidecars.
29///
30/// The [`HARNESS_ONE_WORKER`] flag is read from the environment here, not
31/// passed down, so every command sees exactly the table the runtime would
32/// build — including the rejection of a sidecar in a one-Worker
33/// deployment (issue #131).
34///
35/// # Errors
36///
37/// Every malformed entry, as `SidecarMounts::from_config` reports them.
38pub fn parse(table: Option<&str>) -> Result<SidecarMounts, Vec<String>> {
39 let Some(table) = table else {
40 return Ok(SidecarMounts::default());
41 };
42 let mut pairs = vec![(HARNESS_SIDECARS.to_string(), table.to_string())];
43 if let Some(flag) = std::env::var(HARNESS_ONE_WORKER)
44 .ok()
45 .filter(|value| !value.trim().is_empty())
46 {
47 pairs.push((HARNESS_ONE_WORKER.to_string(), flag));
48 }
49 SidecarMounts::from_config(&MapConfig::from_pairs(pairs))
50}
51
52/// Mounts that name a module compiled into this harness. The runtime
53/// ignores these and keeps the compiled-in module, logging as it goes,
54/// so nothing breaks — but the table is wrong and someone believes a
55/// sidecar is serving that prefix.
56#[must_use]
57pub fn shadowing<'a>(harness: &Harness, mounts: &'a SidecarMounts) -> Vec<&'a SidecarMount> {
58 let names: Vec<&str> = harness.modules().iter().map(|m| m.name()).collect();
59 mounts
60 .iter()
61 .filter(|mount| names.contains(&mount.name.as_str()))
62 .collect()
63}
64
65/// One line per sidecar, for a message that has to name what is missing.
66#[must_use]
67pub fn listed(mounts: &SidecarMounts) -> String {
68 mounts
69 .iter()
70 .map(|mount| format!("`{}` (binding `{}`)", mount.name, mount.binding))
71 .collect::<Vec<_>>()
72 .join(", ")
73}