use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use r402_protocol::{ClientError, PaymentRequired};
#[cfg(test)]
use serde_json as _;
#[cfg(test)]
use tokio as _;
use crate::candidate::SchemeClient;
use crate::extension::{ClientExtension, DynClientExtension};
use crate::hooks::{
ClientHooks, CreatedPayment, DynClientHooks, FailureRecovery, HookDecision,
PaymentCreationContext,
};
use crate::policy::PaymentPolicy;
use crate::select::{FirstMatch, PaymentSelector};
use crate::spend::SpendControls;
pub struct PaymentClient<S = FirstMatch> {
pub(crate) schemes: Vec<Arc<dyn SchemeClient>>,
pub(crate) selector: S,
pub(crate) policies: Vec<Arc<dyn PaymentPolicy>>,
pub(crate) hooks: Vec<Arc<dyn DynClientHooks>>,
pub(crate) extensions: Vec<Arc<dyn DynClientExtension>>,
pub(crate) spend_controls: Option<SpendControls>,
}
impl PaymentClient<FirstMatch> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for PaymentClient<FirstMatch> {
fn default() -> Self {
Self {
schemes: Vec::new(),
selector: FirstMatch,
policies: Vec::new(),
hooks: Vec::new(),
extensions: Vec::new(),
spend_controls: Some(SpendControls::default()),
}
}
}
impl<S> Debug for PaymentClient<S> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("PaymentClient")
.field("schemes", &self.schemes.len())
.field("policies", &self.policies.len())
.field("hooks", &self.hooks.len())
.field("extensions", &self.extensions.len())
.finish_non_exhaustive()
}
}
impl<S> PaymentClient<S> {
#[must_use]
pub fn register(mut self, scheme: impl SchemeClient + 'static) -> Self {
self.schemes.push(Arc::new(scheme));
self
}
#[must_use]
pub fn with_selector<P: PaymentSelector>(self, selector: P) -> PaymentClient<P> {
PaymentClient {
schemes: self.schemes,
selector,
policies: self.policies,
hooks: self.hooks,
extensions: self.extensions,
spend_controls: self.spend_controls,
}
}
#[must_use]
pub fn with_spend_controls(mut self, controls: SpendControls) -> Self {
self.spend_controls = Some(controls);
self
}
#[must_use]
pub fn disable_spend_controls(mut self) -> Self {
self.spend_controls = None;
self
}
#[must_use]
pub fn with_policy(mut self, policy: impl PaymentPolicy + 'static) -> Self {
self.policies.push(Arc::new(policy));
self
}
#[must_use]
pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
self.hooks.push(Arc::new(hook));
self
}
#[must_use]
pub fn with_extension(mut self, extension: impl ClientExtension + 'static) -> Self {
self.extensions.push(Arc::new(extension));
self
}
#[must_use]
pub fn scheme_count(&self) -> usize {
self.schemes.len()
}
#[must_use]
pub fn hook_count(&self) -> usize {
self.hooks.len()
}
#[must_use]
pub fn extension_count(&self) -> usize {
self.extensions.len()
}
}
impl<S: PaymentSelector> PaymentClient<S> {
pub async fn create_payment(
&self,
payment_required: &PaymentRequired,
) -> Result<CreatedPayment, ClientError> {
let ctx = PaymentCreationContext {
payment_required: payment_required.clone(),
};
if let Some(err) = self.run_before_creation(&ctx).await {
return Err(err);
}
match self.create_payment_inner(payment_required).await {
Ok(created) => {
self.run_after_creation(&ctx, &created).await;
Ok(created)
}
Err(err) => self.recover_creation(&ctx, err).await,
}
}
async fn run_before_creation(&self, ctx: &PaymentCreationContext) -> Option<ClientError> {
for hook in &self.hooks {
if let Some(err) =
Self::client_error_from_abort(hook.before_payment_creation(ctx).await)
{
return Some(err);
}
}
None
}
fn client_error_from_abort(decision: HookDecision) -> Option<ClientError> {
let HookDecision::Abort { reason, message } = decision else {
return None;
};
let detail = if message.is_empty() {
reason
} else {
format!("{reason}: {message}")
};
Some(ClientError::Parse(detail))
}
async fn run_after_creation(&self, ctx: &PaymentCreationContext, created: &CreatedPayment) {
for hook in &self.hooks {
hook.after_payment_creation(ctx, created).await;
}
}
async fn recover_creation(
&self,
ctx: &PaymentCreationContext,
err: ClientError,
) -> Result<CreatedPayment, ClientError> {
let msg = err.to_string();
for hook in &self.hooks {
let recovery = hook.on_payment_creation_failure(ctx, &msg).await;
if let FailureRecovery::Recovered(created) = recovery {
return Ok(created);
}
}
Err(err)
}
async fn create_payment_inner(
&self,
payment_required: &PaymentRequired,
) -> Result<CreatedPayment, ClientError> {
let candidates = self.candidates(payment_required);
let selected = self.select_candidate(&candidates)?;
let signed_payload = selected.sign().await?;
let signed_payload = self
.enrich_signed_payload(&signed_payload, payment_required)
.await?;
Ok(CreatedPayment::new(
signed_payload,
payment_required.clone(),
))
}
}