Skip to main content

agentmail/client/
scope.rs

1//! Type-level API scopes. Most AgentMail resources exist at more than one
2//! scope - organization-wide, within a single inbox, or within a pod - at the
3//! same URL shape under a different prefix. Rather than duplicate every method
4//! per scope, each scope is a zero-cost marker type ([`OrgScope`], [`InboxScope`],
5//! [`PodScope`]) and each resource is a capability trait ([`Threads`], [`Domains`],
6//! ...) implemented only for the scopes that support it. The [`Scoped`] handle
7//! carries the scope, and a resource's methods live in one generic
8//! `impl<S: Capability> Scoped<'_, S>` block, so the compiler - not a runtime
9//! 404 - rejects `client.inbox(id).list_domains()` (inboxes have no domains).
10//!
11//! Get a handle from [`Client::org`](crate::Client::org), [`Client::inbox`](crate::Client::inbox), or [`Client::pod`](crate::Client::pod).
12
13use crate::Client;
14use crate::util::urlish;
15
16/// The organization scope (top-level, `/v0/...`), spanning every inbox.
17#[derive(Clone, Copy, Debug)]
18pub struct OrgScope;
19
20/// A single inbox scope (`/v0/inboxes/{inbox_id}/...`).
21#[derive(Clone, Copy, Debug)]
22pub struct InboxScope<'a>(pub &'a str);
23
24/// A single pod scope (`/v0/pods/{pod_id}/...`).
25#[derive(Clone, Copy, Debug)]
26pub struct PodScope<'a>(pub &'a str);
27
28/// A scope's URL prefix. Implemented by [`OrgScope`], [`InboxScope`], and [`PodScope`].
29pub trait Scope {
30    /// The path prefix for this scope (no trailing slash), e.g. `/v0` or
31    /// `/v0/inboxes/ib_1`.
32    fn base(&self) -> String;
33}
34
35impl Scope for OrgScope {
36    fn base(&self) -> String {
37        "/v0".to_string()
38    }
39}
40impl Scope for InboxScope<'_> {
41    fn base(&self) -> String {
42        format!("/v0/inboxes/{}", urlish(self.0))
43    }
44}
45impl Scope for PodScope<'_> {
46    fn base(&self) -> String {
47        format!("/v0/pods/{}", urlish(self.0))
48    }
49}
50
51/// A [`Client`] bound to a [`Scope`]. Returned by [`Client::org`](crate::Client::org),
52/// [`Client::inbox`](crate::Client::inbox), and [`Client::pod`](crate::Client::pod); the resource methods it exposes
53/// depend on which capability traits the scope `S` implements.
54#[derive(Clone, Copy, Debug)]
55pub struct Scoped<'c, S> {
56    pub(crate) client: &'c Client,
57    pub(crate) scope: S,
58}
59
60impl<'c, S: Scope> Scoped<'c, S> {
61    /// This scope's URL prefix.
62    pub(crate) fn base(&self) -> String {
63        self.scope.base()
64    }
65}
66
67// ── Capability traits ─────────────────────────────────────────────────────────
68// Each marks the scopes at which a resource exists. The methods themselves live
69// in one generic `impl<S: Trait> Scoped<'_, S>` block in the resource's module.
70
71/// Scopes with threads (org / inbox / pod).
72pub trait Threads: Scope {}
73impl Threads for OrgScope {}
74impl Threads for InboxScope<'_> {}
75impl Threads for PodScope<'_> {}
76
77/// Scopes with readable drafts (org / inbox / pod). Draft *writes* (create,
78/// update, delete, send) are inbox-only and live on `Scoped<InboxScope>`.
79pub trait Drafts: Scope {}
80impl Drafts for OrgScope {}
81impl Drafts for InboxScope<'_> {}
82impl Drafts for PodScope<'_> {}
83
84/// Scopes with webhooks (org / inbox / pod).
85pub trait Webhooks: Scope {}
86impl Webhooks for OrgScope {}
87impl Webhooks for InboxScope<'_> {}
88impl Webhooks for PodScope<'_> {}
89
90/// Scopes with allow/block lists (org / inbox / pod).
91pub trait Lists: Scope {}
92impl Lists for OrgScope {}
93impl Lists for InboxScope<'_> {}
94impl Lists for PodScope<'_> {}
95
96/// Scopes with metrics (org / inbox / pod).
97pub trait Metrics: Scope {}
98impl Metrics for OrgScope {}
99impl Metrics for InboxScope<'_> {}
100impl Metrics for PodScope<'_> {}
101
102/// Scopes with API keys (org / inbox / pod).
103pub trait ApiKeys: Scope {}
104impl ApiKeys for OrgScope {}
105impl ApiKeys for InboxScope<'_> {}
106impl ApiKeys for PodScope<'_> {}
107
108/// Scopes with domains (org / pod). Inboxes have no domains.
109pub trait Domains: Scope {}
110impl Domains for OrgScope {}
111impl Domains for PodScope<'_> {}
112
113/// Scopes that contain inboxes (org / pod): list, create, get, update, delete
114/// inboxes within the scope. A pod owns its inboxes; the org owns all of them.
115pub trait Inboxes: Scope {}
116impl Inboxes for OrgScope {}
117impl Inboxes for PodScope<'_> {}
118
119impl Client {
120    /// The organization scope (top-level), spanning every inbox.
121    pub fn org(&self) -> Scoped<'_, OrgScope> {
122        Scoped {
123            client: self,
124            scope: OrgScope,
125        }
126    }
127
128    /// Scope operations to a single inbox. Inbox-only resources (messages,
129    /// events, draft writes) live here.
130    ///
131    /// The scope's capabilities are enforced at compile time - an inbox has no
132    /// domains, so this does not compile:
133    ///
134    /// ```compile_fail
135    /// # async fn f(client: agentmail::Client) -> Result<(), agentmail::Error> {
136    /// client.inbox("ib_1").list_domains(Default::default()).await?;
137    /// # Ok(()) }
138    /// ```
139    pub fn inbox<'a>(&'a self, inbox_id: &'a str) -> Scoped<'a, InboxScope<'a>> {
140        Scoped {
141            client: self,
142            scope: InboxScope(inbox_id),
143        }
144    }
145
146    /// Scope operations to a single pod.
147    pub fn pod<'a>(&'a self, pod_id: &'a str) -> Scoped<'a, PodScope<'a>> {
148        Scoped {
149            client: self,
150            scope: PodScope(pod_id),
151        }
152    }
153}