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}