use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use axum_core::extract::Request;
use axum_core::response::Response;
use http::{HeaderMap, Uri};
use r402::facilitator::Facilitator;
use r402::proto::v2;
use tower::util::BoxCloneSyncService;
use tower::{Layer, Service};
use url::Url;
use super::facilitator::FacilitatorClient;
use super::paygate::{Paygate, ResourceTemplate};
use super::pricing::{DynamicPriceTags, PriceTagSource, StaticPriceTags};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum SettlementMode {
#[default]
Sequential,
Concurrent,
Background,
}
pub struct X402Middleware<F> {
facilitator: F,
base_url: Option<Url>,
}
impl<F: Clone> Clone for X402Middleware<F> {
fn clone(&self) -> Self {
Self {
facilitator: self.facilitator.clone(),
base_url: self.base_url.clone(),
}
}
}
impl<F: std::fmt::Debug> std::fmt::Debug for X402Middleware<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("X402Middleware")
.field("facilitator", &self.facilitator)
.field("base_url", &self.base_url)
.finish()
}
}
impl<F> X402Middleware<F> {
#[must_use]
pub const fn from_facilitator(facilitator: F) -> Self {
Self {
facilitator,
base_url: None,
}
}
pub const fn facilitator(&self) -> &F {
&self.facilitator
}
}
impl X402Middleware<Arc<FacilitatorClient>> {
#[must_use]
#[allow(
clippy::expect_used,
reason = "constructor panics on invalid URL by design"
)]
pub fn new(url: &str) -> Self {
let facilitator = FacilitatorClient::try_from(url).expect("Invalid facilitator URL");
Self {
facilitator: Arc::new(facilitator),
base_url: None,
}
}
pub fn try_new(url: &str) -> Result<Self, Box<dyn std::error::Error>> {
let facilitator = FacilitatorClient::try_from(url)?;
Ok(Self {
facilitator: Arc::new(facilitator),
base_url: None,
})
}
#[must_use]
pub fn facilitator_url(&self) -> &Url {
self.facilitator.base_url()
}
#[must_use]
pub fn with_supported_cache_ttl(&self, ttl: Duration) -> Self {
let inner = Arc::unwrap_or_clone(Arc::clone(&self.facilitator));
let facilitator = Arc::new(inner.with_supported_cache_ttl(ttl));
Self {
facilitator,
base_url: self.base_url.clone(),
}
}
#[must_use]
pub fn with_facilitator_timeout(&self, timeout: Duration) -> Self {
let inner = Arc::unwrap_or_clone(Arc::clone(&self.facilitator));
let facilitator = Arc::new(inner.with_timeout(timeout));
Self {
facilitator,
base_url: self.base_url.clone(),
}
}
}
impl TryFrom<&str> for X402Middleware<Arc<FacilitatorClient>> {
type Error = Box<dyn std::error::Error>;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_new(value)
}
}
impl TryFrom<String> for X402Middleware<Arc<FacilitatorClient>> {
type Error = Box<dyn std::error::Error>;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_new(&value)
}
}
impl<F> X402Middleware<F>
where
F: Clone,
{
#[must_use]
pub fn with_base_url(&self, base_url: Url) -> Self {
let mut this = self.clone();
this.base_url = Some(base_url);
this
}
}
impl<TFacilitator> X402Middleware<TFacilitator>
where
TFacilitator: Clone,
{
#[must_use]
pub fn with_price_tag(
&self,
price_tag: v2::PriceTag,
) -> X402LayerBuilder<StaticPriceTags, TFacilitator> {
X402LayerBuilder {
facilitator: self.facilitator.clone(),
price_source: StaticPriceTags::new(vec![price_tag]),
base_url: self.base_url.clone().map(Arc::new),
resource: Arc::new(ResourceTemplate::default()),
settlement_mode: SettlementMode::default(),
}
}
#[must_use]
pub fn with_price_tags(
&self,
price_tags: Vec<v2::PriceTag>,
) -> X402LayerBuilder<StaticPriceTags, TFacilitator> {
X402LayerBuilder {
facilitator: self.facilitator.clone(),
price_source: StaticPriceTags::new(price_tags),
base_url: self.base_url.clone().map(Arc::new),
resource: Arc::new(ResourceTemplate::default()),
settlement_mode: SettlementMode::default(),
}
}
#[must_use]
pub fn with_dynamic_price<F, Fut>(
&self,
callback: F,
) -> X402LayerBuilder<DynamicPriceTags, TFacilitator>
where
F: Fn(&HeaderMap, &Uri, Option<&Url>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Vec<v2::PriceTag>> + Send + 'static,
{
X402LayerBuilder {
facilitator: self.facilitator.clone(),
price_source: DynamicPriceTags::new(callback),
base_url: self.base_url.clone().map(Arc::new),
resource: Arc::new(ResourceTemplate::default()),
settlement_mode: SettlementMode::default(),
}
}
}
#[derive(Clone)]
#[allow(
missing_debug_implementations,
reason = "generic types may not impl Debug"
)]
pub struct X402LayerBuilder<TSource, TFacilitator> {
facilitator: TFacilitator,
base_url: Option<Arc<Url>>,
price_source: TSource,
resource: Arc<ResourceTemplate>,
settlement_mode: SettlementMode,
}
impl<TFacilitator> X402LayerBuilder<StaticPriceTags, TFacilitator> {
#[must_use]
pub fn with_price_tag(mut self, price_tag: v2::PriceTag) -> Self {
self.price_source = self.price_source.with_price_tag(price_tag);
self
}
}
#[allow(
missing_debug_implementations,
reason = "generic types may not impl Debug"
)]
impl<TSource, TFacilitator> X402LayerBuilder<TSource, TFacilitator> {
#[must_use]
pub fn with_description(mut self, description: String) -> Self {
let mut new_resource = (*self.resource).clone();
new_resource.description = description;
self.resource = Arc::new(new_resource);
self
}
#[must_use]
pub fn with_mime_type(mut self, mime: String) -> Self {
let mut new_resource = (*self.resource).clone();
new_resource.mime_type = mime;
self.resource = Arc::new(new_resource);
self
}
#[must_use]
#[allow(
clippy::needless_pass_by_value,
reason = "Url consumed via to_string()"
)]
pub fn with_resource(mut self, resource: Url) -> Self {
let mut new_resource = (*self.resource).clone();
new_resource.url = Some(resource.to_string());
self.resource = Arc::new(new_resource);
self
}
#[must_use]
pub const fn with_settlement_mode(mut self, mode: SettlementMode) -> Self {
self.settlement_mode = mode;
self
}
}
impl<S, TSource, TFacilitator> Layer<S> for X402LayerBuilder<TSource, TFacilitator>
where
S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + Sync + 'static,
S::Future: Send + 'static,
TFacilitator: Facilitator + Clone,
TSource: PriceTagSource,
{
type Service = X402MiddlewareService<TSource, TFacilitator>;
fn layer(&self, inner: S) -> Self::Service {
X402MiddlewareService {
facilitator: self.facilitator.clone(),
base_url: self.base_url.clone(),
price_source: self.price_source.clone(),
resource: Arc::clone(&self.resource),
settlement_mode: self.settlement_mode,
inner: BoxCloneSyncService::new(inner),
}
}
}
#[derive(Clone)]
#[allow(
missing_debug_implementations,
reason = "BoxCloneSyncService does not impl Debug"
)]
pub struct X402MiddlewareService<TSource, TFacilitator> {
facilitator: TFacilitator,
base_url: Option<Arc<Url>>,
price_source: TSource,
resource: Arc<ResourceTemplate>,
settlement_mode: SettlementMode,
inner: BoxCloneSyncService<Request, Response, Infallible>,
}
impl<TSource, TFacilitator> Service<Request> for X402MiddlewareService<TSource, TFacilitator>
where
TSource: PriceTagSource,
TFacilitator: Facilitator + Clone + Send + Sync + 'static,
{
type Response = Response;
type Error = Infallible;
type Future = Pin<Box<dyn Future<Output = Result<Response, Infallible>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
let price_source = self.price_source.clone();
let facilitator = self.facilitator.clone();
let base_url = self.base_url.clone();
let resource_builder = Arc::clone(&self.resource);
let settlement_mode = self.settlement_mode;
let mut inner = self.inner.clone();
Box::pin(async move {
let accepts = price_source
.resolve(req.headers(), req.uri(), base_url.as_deref())
.await;
if accepts.is_empty() {
return inner.call(req).await;
}
let resource = resource_builder.resolve(base_url.as_deref(), &req);
let mut gate = Paygate::builder(facilitator)
.accepts(accepts)
.resource(resource)
.build();
gate.enrich_accepts().await;
let result = match settlement_mode {
SettlementMode::Sequential => gate.handle_request(inner, req).await,
SettlementMode::Concurrent => gate.handle_request_concurrent(inner, req).await,
SettlementMode::Background => gate.handle_request_background(inner, req).await,
};
Ok(result.unwrap_or_else(|err| gate.error_response(err)))
})
}
}