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 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}