1mod auth;
9mod blob;
10mod captcha;
11mod classifier;
12mod clock;
13mod database;
14mod defer;
15mod dispatcher;
16mod http;
17mod idgen;
18mod kv;
19mod mailer;
20mod payments;
21mod push;
22mod rate_limiter;
23mod realtime;
24pub(crate) mod signer;
25mod text_model;
26mod tracker;
27
28pub use auth::{Auth, AuthError, Caller, Subject, Unconfigured};
29pub use blob::{Blob, BlobError, BlobObject, MAX_BLOB_BYTES, ScopedBlob, check_blob_size};
30pub use captcha::{Captcha, CaptchaBinding, CaptchaError, Verdict};
31pub use classifier::{
32 Answer, AnswerValue, Calibration, Classifier, ClassifierError, ClassifierProfile,
33 DEFAULT_MAX_STATE_CHARS, Question, validate_questions,
34};
35pub use clock::{Clock, SystemClock, timeout};
36pub use database::{Database, DbError, Row, Rows, Statement, TryFromValue};
37pub use defer::{Defer, NoopDefer};
38pub use dispatcher::{DispatchError, Dispatcher};
39pub use http::{
40 BoundedHttpClient, DEFAULT_RESPONSE_TIMEOUT, HttpClient, HttpError, HttpPolicy,
41 MAX_CONCURRENT_REQUESTS, MAX_RESPONSE_BYTES, MAX_RESPONSE_TIMEOUT, declared_content_length,
42};
43pub use idgen::{IdGen, UlidIdGen};
44pub use kv::{KeyValue, KvError};
45pub use mailer::{MailError, Mailer, Message, SendOutcome};
46pub use payments::{
47 Charge, CheckoutRequest, CheckoutSession, ConnectAccountLink, ConnectAccountLinkRequest,
48 LineItem, Money, Payments, PaymentsError, Refund, RefundRequest, SubscriptionCheckoutRequest,
49 TransferCharge, WebhookEvent,
50};
51pub use push::{
52 LocKeys, Notification, Platform, Priority, Push, PushError, PushOutcome, Recipient,
53 RoutingPush, retry_after, ttl_secs,
54};
55pub use rate_limiter::{Decision, Quota, RateLimitError, RateLimiter};
56pub use realtime::{Member, Realtime, RealtimeError, RoomContext, RoomHandler};
57pub use signer::{Kid, MAX_KID_NAME, Payload, SignatureError, Signer};
58pub use text_model::{
59 Completion, DEFAULT_MAX_TOKENS, ModelTier, Prompt, Role, RoutingTextModel, TextModel,
60 TextModelError, Turn,
61};
62pub use tracker::{
63 Credential, Destination, Filed, RoutingTracker, Severity, TicketDraft, TicketState,
64 TicketStatus, Tracker, TrackerError,
65};
66
67use crate::config::Config;
68use crate::module::Module;
69use std::sync::Arc;
70use tracing::warn;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum Port {
76 Db,
77 Auth,
78 Mailer,
79 Captcha,
80 RateLimiter,
81 Signer,
82 KeyValue,
83 Blob,
84 Push,
85 Payments,
86 Tracker,
87 Realtime,
88 TextModel,
89 Classifier,
90 HttpClient,
91 Clock,
92 IdGen,
93 Defer,
94}
95
96macro_rules! ports {
109 ($($variant:ident),+ $(,)?) => {
110 impl Port {
111 pub const ALL: &'static [Port] = &[$(Port::$variant),+];
113 }
114
115 #[expect(dead_code, reason = "its only job is to be exhaustive")]
118 fn every_port_is_in_all(port: Port) {
119 match port {
120 $(Port::$variant => {}),+
121 }
122 }
123 };
124}
125
126ports!(
127 Db,
128 Auth,
129 Mailer,
130 Captcha,
131 RateLimiter,
132 Signer,
133 KeyValue,
134 Blob,
135 Push,
136 Payments,
137 Tracker,
138 Realtime,
139 TextModel,
140 Classifier,
141 HttpClient,
142 Clock,
143 IdGen,
144 Defer,
145);
146
147impl Port {
148 pub fn name(&self) -> &'static str {
149 match self {
150 Port::Db => "Database",
151 Port::Auth => "Auth",
152 Port::Mailer => "Mailer",
153 Port::Captcha => "Captcha",
154 Port::RateLimiter => "RateLimiter",
155 Port::Signer => "Signer",
156 Port::KeyValue => "KeyValue",
157 Port::Blob => "Blob",
158 Port::Push => "Push",
159 Port::Payments => "Payments",
160 Port::Tracker => "Tracker",
161 Port::Realtime => "Realtime",
162 Port::TextModel => "TextModel",
163 Port::Classifier => "Classifier",
164 Port::HttpClient => "HttpClient",
165 Port::Clock => "Clock",
166 Port::IdGen => "IdGen",
167 Port::Defer => "Defer",
168 }
169 }
170}
171
172pub struct Ports {
178 pub config: Arc<dyn Config>,
179 pub db: Option<Arc<dyn Database>>,
180 pub auth: Option<Arc<dyn Auth>>,
184 pub mailer: Option<Arc<dyn Mailer>>,
185 pub captcha: Option<Arc<dyn Captcha>>,
186 pub rate_limiter: Option<Arc<dyn RateLimiter>>,
187 pub signer: Option<Arc<dyn Signer>>,
188 pub kv: Option<Arc<dyn KeyValue>>,
189 pub blob: Option<Arc<dyn Blob>>,
190 pub push: Option<Arc<dyn Push>>,
191 pub payments: Option<Arc<dyn Payments>>,
192 pub tracker: Option<Arc<dyn Tracker>>,
193 pub realtime: Option<Arc<dyn Realtime>>,
194 pub text_model: Option<Arc<dyn TextModel>>,
197 pub classifier: Option<Arc<dyn Classifier>>,
201 pub http: Option<Arc<dyn HttpClient>>,
202 pub clock: Option<Arc<dyn Clock>>,
203 pub id_gen: Option<Arc<dyn IdGen>>,
204 pub defer: Option<Arc<dyn Defer>>,
205 pub dispatcher: Option<Arc<dyn Dispatcher>>,
208 pub tenants: Option<Arc<dyn crate::tenant::TenantRouting>>,
219}
220
221impl Ports {
222 pub fn empty() -> Self {
225 Self::with_config(Arc::new(crate::config::EmptyConfig))
226 }
227
228 pub fn with_config(config: Arc<dyn Config>) -> Self {
229 Self {
230 config,
231 db: None,
232 auth: None,
233 mailer: None,
234 captcha: None,
235 rate_limiter: None,
236 signer: None,
237 kv: None,
238 blob: None,
239 push: None,
240 payments: None,
241 tracker: None,
242 realtime: None,
243 text_model: None,
244 classifier: None,
245 http: None,
246 clock: None,
247 id_gen: None,
248 defer: None,
249 dispatcher: None,
250 tenants: None,
251 }
252 }
253
254 #[must_use]
263 pub fn provides(&self) -> Vec<Port> {
264 Port::ALL
265 .iter()
266 .copied()
267 .filter(|port| self.has(*port))
268 .collect()
269 }
270
271 #[must_use]
277 pub fn has(&self, port: Port) -> bool {
278 match port {
279 Port::Db => self.db.is_some(),
280 Port::Auth => self.auth.is_some(),
281 Port::Mailer => self.mailer.is_some(),
282 Port::Captcha => self.captcha.is_some(),
283 Port::RateLimiter => self.rate_limiter.is_some(),
284 Port::Signer => self.signer.is_some(),
285 Port::KeyValue => self.kv.is_some(),
286 Port::Blob => self.blob.is_some(),
287 Port::Push => self.push.is_some(),
288 Port::Payments => self.payments.is_some(),
289 Port::Tracker => self.tracker.is_some(),
290 Port::Realtime => self.realtime.is_some(),
291 Port::TextModel => self.text_model.is_some(),
292 Port::Classifier => self.classifier.is_some(),
293 Port::HttpClient => self.http.is_some(),
294 Port::Clock => self.clock.is_some(),
295 Port::IdGen => self.id_gen.is_some(),
296 Port::Defer => self.defer.is_some(),
297 }
298 }
299
300 #[must_use]
305 pub fn view_for(&self, module: &dyn Module) -> Self {
306 let declared = module
307 .requires()
308 .iter()
309 .chain(module.optional())
310 .copied()
311 .collect::<Vec<_>>();
312 let allows = |p: &[Port], port: Port| p.contains(&port);
313 let mut view = Ports::with_config(self.config.clone());
314 if allows(&declared, Port::Db) {
315 view.db.clone_from(&self.db);
316 }
317 if allows(&declared, Port::Auth) {
318 view.auth.clone_from(&self.auth);
319 }
320 if allows(&declared, Port::Mailer) {
321 view.mailer.clone_from(&self.mailer);
322 }
323 if allows(&declared, Port::Captcha) {
324 view.captcha.clone_from(&self.captcha);
325 }
326 if allows(&declared, Port::RateLimiter) {
327 view.rate_limiter.clone_from(&self.rate_limiter);
328 }
329 if allows(&declared, Port::Signer) {
330 view.signer.clone_from(&self.signer);
331 }
332 if allows(&declared, Port::KeyValue) {
333 view.kv.clone_from(&self.kv);
334 }
335 if allows(&declared, Port::Blob) {
336 view.blob = self.blob.as_ref().map(|blob| {
339 Arc::new(ScopedBlob::new(Arc::clone(blob), module.name())) as Arc<dyn Blob>
340 });
341 }
342 if allows(&declared, Port::Push) {
343 view.push.clone_from(&self.push);
344 }
345 if allows(&declared, Port::Payments) {
346 view.payments.clone_from(&self.payments);
347 }
348 if allows(&declared, Port::Tracker) {
349 view.tracker.clone_from(&self.tracker);
350 }
351 if allows(&declared, Port::Realtime) {
352 view.realtime.clone_from(&self.realtime);
353 }
354 if allows(&declared, Port::TextModel) {
355 view.text_model.clone_from(&self.text_model);
356 }
357 if allows(&declared, Port::Classifier) {
358 view.classifier.clone_from(&self.classifier);
359 }
360 if allows(&declared, Port::HttpClient) {
361 view.http.clone_from(&self.http);
362 }
363 if allows(&declared, Port::Clock) {
364 view.clock.clone_from(&self.clock);
365 }
366 if allows(&declared, Port::IdGen) {
367 view.id_gen.clone_from(&self.id_gen);
368 }
369 if allows(&declared, Port::Defer) {
370 view.defer.clone_from(&self.defer);
371 }
372 view
373 }
374}
375
376pub(crate) fn warn_undeclared_ports(module: &dyn Module, provided: &[Port]) {
379 for port in provided {
380 if !module.requires().contains(port) && !module.optional().contains(port) {
381 warn!(
382 module = module.name(),
383 port = port.name(),
384 "runtime provides a port the module did not declare; hiding it",
385 );
386 }
387 }
388}