Skip to main content

cloud/
capability.rs

1//! Service capabilities and the mirror-side driver bindings that satisfy them
2//! (W265).
3//!
4//! The problem this solves: a `mesofact-static` component wants "somewhere to
5//! put bytes that a browser can GET". At the dev tier that used to be a
6//! disk-served directory, at sim it is MinIO over S3, at cloud it is R2. If the
7//! *app* has to know which, it grows an `if dev { fs::write } else { s3_put }`
8//! branch, and every later app inherits the same fork.
9//!
10//! So the app declares a tier-agnostic **capability** and the mirror declares
11//! which **driver** implements it at this tier:
12//!
13//! ```text
14//!   component kind  ──derives──▶  Capability  ◀──binds──  [drivers.<cap>] in mirrors/<env>.toml
15//! ```
16//!
17//! # P1 scope
18//!
19//! Requirements are derived from the component `kind` — one match arm per kind,
20//! zero per-service boilerplate, because every service in-tree today is
21//! kind-implied. The explicit `[requires]` block in `service.toml` (for needs
22//! that aren't kind-implied) and the binding-error surface ("service X needs s3,
23//! mirror Y binds no s3 driver") are P2; see W265 §"Open follow-ups".
24
25use crate::config::{MirrorConfig, MirrorProviderSlot};
26
27/// A tier-agnostic thing a service needs, independent of who provides it.
28///
29/// Deliberately small: a capability earns a variant when a *second*
30/// implementation of it exists at a different tier, because that is the moment
31/// an app would otherwise have to fork. Pub/sub and secret-store are the
32/// obvious next candidates and are not here yet.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub enum Capability {
35    /// An S3-compatible object store. `local-s3-fs` at dev, `minio-container`
36    /// at sim, `cloudflare-r2` at prod/ha.
37    S3,
38    /// A PostgreSQL server reachable over pgwire. `local-pg-dev` at dev
39    /// (R584-F1).
40    ///
41    /// Note what this is *not*: the camp's default store is service-owned
42    /// libsql, embedded in the service process, and those services never touch
43    /// this capability. `pg` is for non-rust services and rust services that
44    /// specifically want the wire protocol.
45    Pg,
46    /// An SMTP relay a service can hand outbound mail to. `local-mailcrab` at
47    /// dev and pond (R584-F2), where "delivery" means *capture*: the mail is
48    /// held in a browsable inbox and never leaves the machine.
49    ///
50    /// The fork this erases is the one every app grows otherwise — a
51    /// `if dev { log_the_email() } else { send_it() }` branch, and with it a
52    /// dev path whose rendering, headers and attachments are never exercised.
53    /// The app speaks SMTP at every tier; the mirror decides who answers.
54    ///
55    /// No cloud-tier driver binds this yet: a real relay needs credentials,
56    /// which is W265 §"Open follow-ups" P2 work.
57    Smtp,
58}
59
60impl Capability {
61    /// The key this capability is declared under in `[drivers.<key>]`.
62    pub fn wire_name(self) -> &'static str {
63        match self {
64            Self::S3 => "s3",
65            Self::Pg => "pg",
66            Self::Smtp => "smtp",
67        }
68    }
69
70    /// Capabilities implied by a component's `kind`.
71    ///
72    /// `kind` is a free-form string in `service.toml`, so an unknown kind maps
73    /// to no capabilities rather than an error — a component kind this build
74    /// doesn't know about is a forward-compat case, not a misconfiguration.
75    pub fn for_component_kind(kind: &str) -> &'static [Capability] {
76        match kind {
77            // Everything that publishes bytes for a browser to fetch wants a
78            // bucket with public read.
79            "mesofact-static" | "mesofact-spa" | "static-asset" => &[Capability::S3],
80            // No in-tree component kind implies pg today: the services that
81            // want it are out-of-tree and non-mesofact, and they'll declare it
82            // through P2's explicit `[requires]` block. The variant exists
83            // because the *driver* ships now (R584-F1) — activation at P1 is
84            // keyed off the mirror's `[drivers.pg]` binding, not off a kind.
85            _ => &[],
86        }
87    }
88}
89
90impl MirrorConfig {
91    /// The driver bound to `capability` in this mirror, if any.
92    ///
93    /// `None` means the tier declares no implementation. In P1 that's simply
94    /// "this mirror doesn't use that capability"; P2's binding-error surface is
95    /// what turns it into a diagnosable failure for a service that needs it.
96    pub fn driver(&self, capability: Capability) -> Option<&MirrorProviderSlot> {
97        self.drivers.get(capability.wire_name())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::config::{MirrorShape, Provider};
105
106    fn mirror_with(toml_src: &str) -> MirrorConfig {
107        toml::from_str(toml_src).expect("parse mirror")
108    }
109
110    #[test]
111    fn static_shaped_kinds_imply_s3_and_nothing_else_does() {
112        for kind in ["mesofact-static", "mesofact-spa", "static-asset"] {
113            assert_eq!(
114                Capability::for_component_kind(kind),
115                &[Capability::S3],
116                "{kind} should imply s3"
117            );
118        }
119        for kind in [
120            "cloudflare-worker",
121            "mesofact-bundle",
122            "not-a-real-kind",
123            "",
124        ] {
125            assert!(
126                Capability::for_component_kind(kind).is_empty(),
127                "{kind} should imply nothing"
128            );
129        }
130    }
131
132    #[test]
133    fn drivers_table_parses_and_resolves_by_capability() {
134        let mirror = mirror_with(
135            r#"
136schema_version = 1
137shape = "local"
138
139[drivers.pg]
140kind = "local-pg-dev"
141"#,
142        );
143        assert!(matches!(mirror.shape, MirrorShape::Local));
144        let slot = mirror.driver(Capability::Pg).expect("pg driver bound");
145        assert_eq!(slot.inline_kind(), Some(Provider::LocalPgDev));
146        assert!(mirror.driver(Capability::S3).is_none());
147    }
148
149    #[test]
150    fn the_smtp_capability_binds_the_mailcrab_driver() {
151        let mirror = mirror_with(
152            r#"
153schema_version = 1
154shape = "local"
155
156[drivers.smtp]
157kind = "local-mailcrab"
158"#,
159        );
160        let slot = mirror.driver(Capability::Smtp).expect("smtp driver bound");
161        assert_eq!(slot.inline_kind(), Some(Provider::LocalMailcrab));
162        assert!(mirror.driver(Capability::Pg).is_none());
163    }
164
165    /// The binding the static-asset kinds imply. Unlike pg and smtp this one
166    /// is optional in practice — `reconciler::s3_driver` activates the dev
167    /// driver whether or not it is written down — but the key still has to
168    /// parse and resolve, because pond and cloud bind the *same* capability to
169    /// a different implementation and that is not optional.
170    #[test]
171    fn the_s3_capability_binds_the_dev_fs_driver() {
172        let mirror = mirror_with(
173            r#"
174schema_version = 1
175shape = "local"
176
177[drivers.s3]
178kind = "local-s3-fs"
179"#,
180        );
181        let slot = mirror.driver(Capability::S3).expect("s3 driver bound");
182        assert_eq!(slot.inline_kind(), Some(Provider::LocalS3Fs));
183        assert!(mirror.driver(Capability::Pg).is_none());
184    }
185
186    /// Every capability's wire name is the TOML key an author types, so a
187    /// duplicate would make two capabilities read the same `[drivers.<key>]`
188    /// table and silently share one binding.
189    #[test]
190    fn wire_names_are_distinct_and_toml_key_shaped() {
191        let all = [Capability::S3, Capability::Pg, Capability::Smtp];
192        let names: std::collections::BTreeSet<_> = all.iter().map(|c| c.wire_name()).collect();
193        assert_eq!(names.len(), all.len(), "duplicate wire_name: {names:?}");
194        for name in names {
195            assert!(
196                !name.is_empty()
197                    && name
198                        .chars()
199                        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
200                "{name} is not a bare lowercase TOML key"
201            );
202        }
203    }
204
205    #[test]
206    fn a_mirror_with_no_drivers_table_is_unchanged() {
207        let mirror = mirror_with(
208            r#"
209schema_version = 1
210shape = "local"
211
212[providers.static]
213kind = "miniflare-native"
214port = 4324
215"#,
216        );
217        assert!(mirror.drivers.is_empty());
218        assert!(mirror.driver(Capability::Pg).is_none());
219        // …and round-trips without sprouting an empty `drivers` table.
220        let back = toml::to_string(&mirror).expect("serialize");
221        assert!(
222            !back.contains("drivers"),
223            "unexpected drivers table: {back}"
224        );
225    }
226}