Skip to main content

cratefield_core/ports/
mod.rs

1//! Port traits (ADR 0002, architecture section 5). Modules see these traits
2//! and nothing else — never a Cloudflare binding, never a vendor client.
3//!
4//! All traits are `Send + Sync` and object-safe, used as `Arc<dyn Trait>`.
5//! Async methods use `async_trait` until native `async fn` in traits is
6//! ergonomic for trait objects.
7
8mod blob;
9mod captcha;
10mod clock;
11mod database;
12mod defer;
13mod dispatcher;
14mod http;
15mod idgen;
16mod kv;
17mod mailer;
18mod rate_limiter;
19pub(crate) mod signer;
20
21pub use blob::{Blob, BlobError, BlobObject, ScopedBlob};
22pub use captcha::{Captcha, CaptchaError, Verdict};
23pub use clock::{Clock, SystemClock, timeout};
24pub use database::{Database, DbError, Row, Rows, Statement, TryFromValue};
25pub use defer::{Defer, NoopDefer};
26pub use dispatcher::{DispatchError, Dispatcher};
27pub use http::{HttpClient, HttpError};
28pub use idgen::{IdGen, UlidIdGen};
29pub use kv::{KeyValue, KvError};
30pub use mailer::{MailError, Mailer, Message, SendOutcome};
31pub use rate_limiter::{Decision, RateLimitError, RateLimiter};
32pub use signer::{Kid, Payload, SignatureError, Signer};
33
34use crate::config::Config;
35use crate::module::Module;
36use std::sync::Arc;
37use tracing::warn;
38
39/// Every port a module can declare in `requires()` / `optional()`
40/// (architecture section 4).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Port {
43    Db,
44    Mailer,
45    Captcha,
46    RateLimiter,
47    Signer,
48    KeyValue,
49    Blob,
50    HttpClient,
51    Clock,
52    IdGen,
53    Defer,
54}
55
56impl Port {
57    pub const ALL: &'static [Port] = &[
58        Port::Db,
59        Port::Mailer,
60        Port::Captcha,
61        Port::RateLimiter,
62        Port::Signer,
63        Port::KeyValue,
64        Port::Blob,
65        Port::HttpClient,
66        Port::Clock,
67        Port::IdGen,
68        Port::Defer,
69    ];
70
71    pub fn name(&self) -> &'static str {
72        match self {
73            Port::Db => "Database",
74            Port::Mailer => "Mailer",
75            Port::Captcha => "Captcha",
76            Port::RateLimiter => "RateLimiter",
77            Port::Signer => "Signer",
78            Port::KeyValue => "KeyValue",
79            Port::Blob => "Blob",
80            Port::HttpClient => "HttpClient",
81            Port::Clock => "Clock",
82            Port::IdGen => "IdGen",
83            Port::Defer => "Defer",
84        }
85    }
86}
87
88/// The per-request bundle of resolved port implementations plus the typed
89/// config the runtime built from environment/secrets.
90///
91/// Every port is optional: the Cloudflare runtime resolves what the venture's
92/// bindings actually provide and leaves the rest `None`.
93pub struct Ports {
94    pub config: Arc<dyn Config>,
95    pub db: Option<Arc<dyn Database>>,
96    pub mailer: Option<Arc<dyn Mailer>>,
97    pub captcha: Option<Arc<dyn Captcha>>,
98    pub rate_limiter: Option<Arc<dyn RateLimiter>>,
99    pub signer: Option<Arc<dyn Signer>>,
100    pub kv: Option<Arc<dyn KeyValue>>,
101    pub blob: Option<Arc<dyn Blob>>,
102    pub http: Option<Arc<dyn HttpClient>>,
103    pub clock: Option<Arc<dyn Clock>>,
104    pub id_gen: Option<Arc<dyn IdGen>>,
105    pub defer: Option<Arc<dyn Defer>>,
106    /// Set by the runtime when the venture mounts sidecar modules. Not a
107    /// [`Port`], so `view_for` never copies it and no module can reach it.
108    pub dispatcher: Option<Arc<dyn Dispatcher>>,
109}
110
111impl Ports {
112    /// An empty bundle with no ports resolved and an
113    /// [`EmptyConfig`](crate::config::EmptyConfig).
114    pub fn empty() -> Self {
115        Self::with_config(Arc::new(crate::config::EmptyConfig))
116    }
117
118    pub fn with_config(config: Arc<dyn Config>) -> Self {
119        Self {
120            config,
121            db: None,
122            mailer: None,
123            captcha: None,
124            rate_limiter: None,
125            signer: None,
126            kv: None,
127            blob: None,
128            http: None,
129            clock: None,
130            id_gen: None,
131            defer: None,
132            dispatcher: None,
133        }
134    }
135
136    /// The set of ports this bundle actually provides.
137    pub fn provides(&self) -> Vec<Port> {
138        let mut provided = Vec::new();
139        if self.db.is_some() {
140            provided.push(Port::Db);
141        }
142        if self.mailer.is_some() {
143            provided.push(Port::Mailer);
144        }
145        if self.captcha.is_some() {
146            provided.push(Port::Captcha);
147        }
148        if self.rate_limiter.is_some() {
149            provided.push(Port::RateLimiter);
150        }
151        if self.signer.is_some() {
152            provided.push(Port::Signer);
153        }
154        if self.kv.is_some() {
155            provided.push(Port::KeyValue);
156        }
157        if self.http.is_some() {
158            provided.push(Port::HttpClient);
159        }
160        if self.clock.is_some() {
161            provided.push(Port::Clock);
162        }
163        if self.id_gen.is_some() {
164            provided.push(Port::IdGen);
165        }
166        if self.defer.is_some() {
167            provided.push(Port::Defer);
168        }
169        provided
170    }
171
172    /// A copy of this bundle in which every port the module did not declare
173    /// in `requires()` or `optional()` is `None`, so a module cannot use
174    /// what it did not declare (issue #3). Undeclared-but-provided ports are
175    /// logged once per module by `Harness::build`.
176    #[must_use]
177    pub fn view_for(&self, module: &dyn Module) -> Self {
178        let declared = module
179            .requires()
180            .iter()
181            .chain(module.optional())
182            .copied()
183            .collect::<Vec<_>>();
184        let allows = |p: &[Port], port: Port| p.contains(&port);
185        let mut view = Ports::with_config(self.config.clone());
186        if allows(&declared, Port::Db) {
187            view.db.clone_from(&self.db);
188        }
189        if allows(&declared, Port::Mailer) {
190            view.mailer.clone_from(&self.mailer);
191        }
192        if allows(&declared, Port::Captcha) {
193            view.captcha.clone_from(&self.captcha);
194        }
195        if allows(&declared, Port::RateLimiter) {
196            view.rate_limiter.clone_from(&self.rate_limiter);
197        }
198        if allows(&declared, Port::Signer) {
199            view.signer.clone_from(&self.signer);
200        }
201        if allows(&declared, Port::KeyValue) {
202            view.kv.clone_from(&self.kv);
203        }
204        if allows(&declared, Port::Blob) {
205            // Scope the store to this module's prefix, the blob equivalent of
206            // the table-ownership rule: a module cannot name another's objects.
207            view.blob = self.blob.as_ref().map(|blob| {
208                Arc::new(ScopedBlob::new(Arc::clone(blob), module.name())) as Arc<dyn Blob>
209            });
210        }
211        if allows(&declared, Port::HttpClient) {
212            view.http.clone_from(&self.http);
213        }
214        if allows(&declared, Port::Clock) {
215            view.clock.clone_from(&self.clock);
216        }
217        if allows(&declared, Port::IdGen) {
218            view.id_gen.clone_from(&self.id_gen);
219        }
220        if allows(&declared, Port::Defer) {
221            view.defer.clone_from(&self.defer);
222        }
223        view
224    }
225}
226
227/// Log (once, at `Harness::build`) the provided ports a module did not
228/// declare — the ports `view_for` will hide from it.
229pub(crate) fn warn_undeclared_ports(module: &dyn Module, provided: &[Port]) {
230    for port in provided {
231        if !module.requires().contains(port) && !module.optional().contains(port) {
232            warn!(
233                module = module.name(),
234                port = port.name(),
235                "runtime provides a port the module did not declare; hiding it",
236            );
237        }
238    }
239}