Skip to main content

r402_client/
register.rs

1//! [`PaymentClient`]: register schemes, policies, hooks, and spend controls.
2
3use std::fmt::{self, Debug, Formatter};
4use std::sync::Arc;
5
6use r402_protocol::{ClientError, PaymentRequired};
7#[cfg(test)]
8use serde_json as _;
9#[cfg(test)]
10use tokio as _;
11
12use crate::candidate::SchemeClient;
13use crate::extension::{ClientExtension, DynClientExtension};
14use crate::hooks::{
15    ClientHooks, CreatedPayment, DynClientHooks, FailureRecovery, HookDecision,
16    PaymentCreationContext,
17};
18use crate::policy::PaymentPolicy;
19use crate::select::{FirstMatch, PaymentSelector};
20use crate::spend::SpendControls;
21
22/// Buyer payment client.
23pub struct PaymentClient<S = FirstMatch> {
24    pub(crate) schemes: Vec<Arc<dyn SchemeClient>>,
25    pub(crate) selector: S,
26    pub(crate) policies: Vec<Arc<dyn PaymentPolicy>>,
27    pub(crate) hooks: Vec<Arc<dyn DynClientHooks>>,
28    pub(crate) extensions: Vec<Arc<dyn DynClientExtension>>,
29    pub(crate) spend_controls: Option<SpendControls>,
30}
31
32impl PaymentClient<FirstMatch> {
33    /// Empty client with [`FirstMatch`] selection and default `$1` spend controls.
34    #[must_use]
35    pub fn new() -> Self {
36        Self::default()
37    }
38}
39
40impl Default for PaymentClient<FirstMatch> {
41    fn default() -> Self {
42        Self {
43            schemes: Vec::new(),
44            selector: FirstMatch,
45            policies: Vec::new(),
46            hooks: Vec::new(),
47            extensions: Vec::new(),
48            spend_controls: Some(SpendControls::default()),
49        }
50    }
51}
52
53impl<S> Debug for PaymentClient<S> {
54    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55        f.debug_struct("PaymentClient")
56            .field("schemes", &self.schemes.len())
57            .field("policies", &self.policies.len())
58            .field("hooks", &self.hooks.len())
59            .field("extensions", &self.extensions.len())
60            .finish_non_exhaustive()
61    }
62}
63
64impl<S> PaymentClient<S> {
65    /// Registers a scheme client.
66    #[must_use]
67    pub fn register(mut self, scheme: impl SchemeClient + 'static) -> Self {
68        self.schemes.push(Arc::new(scheme));
69        self
70    }
71
72    /// Replaces the payment selector.
73    #[must_use]
74    pub fn with_selector<P: PaymentSelector>(self, selector: P) -> PaymentClient<P> {
75        PaymentClient {
76            schemes: self.schemes,
77            selector,
78            policies: self.policies,
79            hooks: self.hooks,
80            extensions: self.extensions,
81            spend_controls: self.spend_controls,
82        }
83    }
84
85    /// Enables spend controls with the given configuration.
86    #[must_use]
87    pub fn with_spend_controls(mut self, controls: SpendControls) -> Self {
88        self.spend_controls = Some(controls);
89        self
90    }
91
92    /// Disables all spend controls (any asset, no caps).
93    #[must_use]
94    pub fn disable_spend_controls(mut self) -> Self {
95        self.spend_controls = None;
96        self
97    }
98
99    /// Appends a payment policy (applied in registration order).
100    #[must_use]
101    pub fn with_policy(mut self, policy: impl PaymentPolicy + 'static) -> Self {
102        self.policies.push(Arc::new(policy));
103        self
104    }
105
106    /// Registers a client lifecycle hook.
107    #[must_use]
108    pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
109        self.hooks.push(Arc::new(hook));
110        self
111    }
112
113    /// Registers a client extension (enrich and/or HTTP 402 header hook).
114    #[must_use]
115    pub fn with_extension(mut self, extension: impl ClientExtension + 'static) -> Self {
116        self.extensions.push(Arc::new(extension));
117        self
118    }
119
120    /// Number of registered scheme clients.
121    #[must_use]
122    pub fn scheme_count(&self) -> usize {
123        self.schemes.len()
124    }
125
126    /// Number of registered hooks.
127    #[must_use]
128    pub fn hook_count(&self) -> usize {
129        self.hooks.len()
130    }
131
132    /// Number of registered client extensions.
133    #[must_use]
134    pub fn extension_count(&self) -> usize {
135        self.extensions.len()
136    }
137}
138
139impl<S: PaymentSelector> PaymentClient<S> {
140    /// Creates a signed payment payload for a 402 challenge.
141    ///
142    /// # Errors
143    ///
144    /// Propagates selection, signing, and before-hook abort errors.
145    pub async fn create_payment(
146        &self,
147        payment_required: &PaymentRequired,
148    ) -> Result<CreatedPayment, ClientError> {
149        let ctx = PaymentCreationContext {
150            payment_required: payment_required.clone(),
151        };
152
153        if let Some(err) = self.run_before_creation(&ctx).await {
154            return Err(err);
155        }
156
157        match self.create_payment_inner(payment_required).await {
158            Ok(created) => {
159                self.run_after_creation(&ctx, &created).await;
160                Ok(created)
161            }
162            Err(err) => self.recover_creation(&ctx, err).await,
163        }
164    }
165
166    async fn run_before_creation(&self, ctx: &PaymentCreationContext) -> Option<ClientError> {
167        for hook in &self.hooks {
168            if let Some(err) =
169                Self::client_error_from_abort(hook.before_payment_creation(ctx).await)
170            {
171                return Some(err);
172            }
173        }
174        None
175    }
176
177    fn client_error_from_abort(decision: HookDecision) -> Option<ClientError> {
178        let HookDecision::Abort { reason, message } = decision else {
179            return None;
180        };
181        let detail = if message.is_empty() {
182            reason
183        } else {
184            format!("{reason}: {message}")
185        };
186        Some(ClientError::Parse(detail))
187    }
188
189    async fn run_after_creation(&self, ctx: &PaymentCreationContext, created: &CreatedPayment) {
190        for hook in &self.hooks {
191            hook.after_payment_creation(ctx, created).await;
192        }
193    }
194
195    async fn recover_creation(
196        &self,
197        ctx: &PaymentCreationContext,
198        err: ClientError,
199    ) -> Result<CreatedPayment, ClientError> {
200        let msg = err.to_string();
201        for hook in &self.hooks {
202            let recovery = hook.on_payment_creation_failure(ctx, &msg).await;
203            if let FailureRecovery::Recovered(created) = recovery {
204                return Ok(created);
205            }
206        }
207        Err(err)
208    }
209
210    async fn create_payment_inner(
211        &self,
212        payment_required: &PaymentRequired,
213    ) -> Result<CreatedPayment, ClientError> {
214        let candidates = self.candidates(payment_required);
215        let selected = self.select_candidate(&candidates)?;
216        let signed_payload = selected.sign().await?;
217        let signed_payload = self
218            .enrich_signed_payload(&signed_payload, payment_required)
219            .await?;
220        Ok(CreatedPayment::new(
221            signed_payload,
222            payment_required.clone(),
223        ))
224    }
225}