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