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}