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 cloud/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}
47
48impl Capability {
49 /// The key this capability is declared under in `[drivers.<key>]`.
50 pub fn wire_name(self) -> &'static str {
51 match self {
52 Self::S3 => "s3",
53 Self::Pg => "pg",
54 }
55 }
56
57 /// Capabilities implied by a component's `kind`.
58 ///
59 /// `kind` is a free-form string in `service.toml`, so an unknown kind maps
60 /// to no capabilities rather than an error — a component kind this build
61 /// doesn't know about is a forward-compat case, not a misconfiguration.
62 pub fn for_component_kind(kind: &str) -> &'static [Capability] {
63 match kind {
64 // Everything that publishes bytes for a browser to fetch wants a
65 // bucket with public read.
66 "mesofact-static" | "mesofact-spa" | "static-asset" => &[Capability::S3],
67 // No in-tree component kind implies pg today: the services that
68 // want it are out-of-tree and non-mesofact, and they'll declare it
69 // through P2's explicit `[requires]` block. The variant exists
70 // because the *driver* ships now (R584-F1) — activation at P1 is
71 // keyed off the mirror's `[drivers.pg]` binding, not off a kind.
72 _ => &[],
73 }
74 }
75}
76
77impl MirrorConfig {
78 /// The driver bound to `capability` in this mirror, if any.
79 ///
80 /// `None` means the tier declares no implementation. In P1 that's simply
81 /// "this mirror doesn't use that capability"; P2's binding-error surface is
82 /// what turns it into a diagnosable failure for a service that needs it.
83 pub fn driver(&self, capability: Capability) -> Option<&MirrorProviderSlot> {
84 self.drivers.get(capability.wire_name())
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::config::{MirrorShape, Provider};
92
93 fn mirror_with(toml_src: &str) -> MirrorConfig {
94 toml::from_str(toml_src).expect("parse mirror")
95 }
96
97 #[test]
98 fn static_shaped_kinds_imply_s3_and_nothing_else_does() {
99 for kind in ["mesofact-static", "mesofact-spa", "static-asset"] {
100 assert_eq!(
101 Capability::for_component_kind(kind),
102 &[Capability::S3],
103 "{kind} should imply s3"
104 );
105 }
106 for kind in [
107 "cloudflare-worker",
108 "mesofact-bundle",
109 "not-a-real-kind",
110 "",
111 ] {
112 assert!(
113 Capability::for_component_kind(kind).is_empty(),
114 "{kind} should imply nothing"
115 );
116 }
117 }
118
119 #[test]
120 fn drivers_table_parses_and_resolves_by_capability() {
121 let mirror = mirror_with(
122 r#"
123schema_version = 1
124shape = "local"
125
126[drivers.pg]
127kind = "local-pg-dev"
128"#,
129 );
130 assert!(matches!(mirror.shape, MirrorShape::Local));
131 let slot = mirror.driver(Capability::Pg).expect("pg driver bound");
132 assert_eq!(slot.inline_kind(), Some(Provider::LocalPgDev));
133 assert!(mirror.driver(Capability::S3).is_none());
134 }
135
136 #[test]
137 fn a_mirror_with_no_drivers_table_is_unchanged() {
138 let mirror = mirror_with(
139 r#"
140schema_version = 1
141shape = "local"
142
143[providers.static]
144kind = "local-static"
145port = 4324
146"#,
147 );
148 assert!(mirror.drivers.is_empty());
149 assert!(mirror.driver(Capability::Pg).is_none());
150 // …and round-trips without sprouting an empty `drivers` table.
151 let back = toml::to_string(&mirror).expect("serialize");
152 assert!(
153 !back.contains("drivers"),
154 "unexpected drivers table: {back}"
155 );
156 }
157}