use std::sync::Arc;
use axum_core::extract::rejection::BytesRejection;
use axum_core::extract::{FromRef, FromRequest, FromRequestParts, Request};
use axum_core::response::IntoResponse;
use bytes::Bytes;
use http_types::{header, HeaderValue, StatusCode};
use crate::error::{MppError, PaymentError, PaymentErrorDetails};
#[cfg(any(feature = "stripe", feature = "tempo"))]
use crate::protocol::core::headers::parse_authorization;
use crate::protocol::core::headers::{
extract_payment_scheme, format_receipt, format_www_authenticate, with_private_cache_control,
PAYMENT_RECEIPT_HEADER, WWW_AUTHENTICATE_HEADER,
};
use crate::protocol::core::{PaymentChallenge, Receipt};
#[derive(Debug)]
pub struct PaymentRequired(pub PaymentChallenge);
impl IntoResponse for PaymentRequired {
fn into_response(self) -> axum_core::response::Response {
PaymentOffers(vec![self.0]).into_response()
}
}
#[derive(Debug)]
pub struct PaymentOffers(pub Vec<PaymentChallenge>);
impl IntoResponse for PaymentOffers {
fn into_response(self) -> axum_core::response::Response {
let Some(values) = challenge_header_values(&self.0) else {
return internal_error_response();
};
let mut resp = (
StatusCode::PAYMENT_REQUIRED,
serde_json::json!({ "error": "Payment Required" }).to_string(),
)
.into_response();
for value in values {
resp.headers_mut().append(WWW_AUTHENTICATE_HEADER, value);
}
resp.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
resp.headers_mut()
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
resp
}
}
#[derive(Debug)]
pub struct PaymentProblem {
pub problem: PaymentErrorDetails,
pub offers: Vec<PaymentChallenge>,
}
impl PaymentProblem {
pub fn new(error: &impl PaymentError, offers: Vec<PaymentChallenge>) -> Self {
let problem = error.to_problem_details(offers.first().map(|offer| offer.id.as_str()));
Self { problem, offers }
}
}
impl IntoResponse for PaymentProblem {
fn into_response(self) -> axum_core::response::Response {
let status =
StatusCode::from_u16(self.problem.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let values = if status.is_server_error() {
Vec::new()
} else {
match challenge_header_values(&self.offers) {
Some(values) => values,
None => return internal_error_response(),
}
};
let Ok(body) = serde_json::to_string(&self.problem) else {
return internal_error_response();
};
let mut resp = (status, body).into_response();
for value in values {
resp.headers_mut().append(WWW_AUTHENTICATE_HEADER, value);
}
resp.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/problem+json"),
);
resp.headers_mut()
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
resp
}
}
fn challenge_header_values(challenges: &[PaymentChallenge]) -> Option<Vec<HeaderValue>> {
challenges
.iter()
.map(|challenge| {
let value = format_www_authenticate(challenge).ok()?;
HeaderValue::from_str(&value).ok()
})
.collect()
}
fn internal_error_response() -> axum_core::response::Response {
PaymentProblem::new(&MppError::Internal(String::new()), Vec::new()).into_response()
}
pub trait ChargeConfig {
fn amount() -> &'static str;
fn description() -> Option<&'static str> {
None
}
}
#[derive(Debug, Default, Clone)]
pub struct ChallengeOptions {
pub description: Option<&'static str>,
pub mppx_scope: Option<serde_json::Value>,
}
#[derive(Debug)]
pub struct MppCharge<C: ChargeConfig> {
pub receipt: Receipt,
_config: std::marker::PhantomData<C>,
}
#[derive(Debug)]
pub struct MppChargeWithBody<C: ChargeConfig> {
pub receipt: Receipt,
pub body: Bytes,
_config: std::marker::PhantomData<C>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum MppChargeRejection {
Challenge(PaymentRequired),
VerificationFailed(PaymentRequired),
InternalError(String),
Offers(PaymentOffers),
VerificationFailedOffers(PaymentOffers),
Body(BytesRejection),
Problem(PaymentProblem),
}
impl MppChargeRejection {
fn from_offers(mut offers: Vec<PaymentChallenge>) -> Self {
match offers.len() {
0 => Self::InternalError("No payment challenges generated".into()),
1 => Self::Challenge(PaymentRequired(offers.remove(0))),
_ => Self::Offers(PaymentOffers(offers)),
}
}
fn from_error(
error: MppError,
offers: impl FnOnce() -> Result<Vec<PaymentChallenge>, String>,
) -> Self {
if !error.is_payment_problem() {
return Self::Problem(PaymentProblem::new(&error, Vec::new()));
}
match offers() {
Ok(offers) if offers.is_empty() => {
Self::InternalError("No payment challenges generated".into())
}
Ok(offers) => Self::Problem(PaymentProblem::new(&error, offers)),
Err(e) => Self::InternalError(e),
}
}
}
impl IntoResponse for MppChargeRejection {
fn into_response(self) -> axum_core::response::Response {
match self {
MppChargeRejection::Challenge(pr) => pr.into_response(),
MppChargeRejection::VerificationFailed(pr) => pr.into_response(),
MppChargeRejection::InternalError(_) => internal_error_response(),
MppChargeRejection::Offers(offers) => offers.into_response(),
MppChargeRejection::VerificationFailedOffers(offers) => offers.into_response(),
MppChargeRejection::Body(rejection) => rejection.into_response(),
MppChargeRejection::Problem(problem) => problem.into_response(),
}
}
}
pub trait ChargeChallenger: Send + Sync + 'static {
fn challenge(
&self,
amount: &str,
options: ChallengeOptions,
) -> Result<PaymentChallenge, String>;
fn challenge_with_body(
&self,
amount: &str,
options: ChallengeOptions,
body: &[u8],
) -> Result<PaymentChallenge, String> {
let _ = body;
self.challenge(amount, options)
}
fn verify_payment(
&self,
credential_str: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>>;
fn verify_payment_for_amount(
&self,
credential_str: &str,
_amount: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
self.verify_payment(credential_str)
}
fn verify_payment_for_amount_and_scope(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
if mppx_scope.is_some() {
let _ = credential_str;
let _ = amount;
return Box::pin(std::future::ready(Err(
"framework scope verification is not implemented for this ChargeChallenger".into(),
)));
}
self.verify_payment_for_amount(credential_str, amount)
}
fn verify_payment_for_amount_with_body(
&self,
credential_str: &str,
amount: &str,
body: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let _ = body;
self.verify_payment_for_amount(credential_str, amount)
}
fn verify_payment_for_amount_scope_and_body(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
body: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
if mppx_scope.is_some() {
let _ = credential_str;
let _ = amount;
let _ = body;
return Box::pin(std::future::ready(Err(
"framework scope verification is not implemented for this ChargeChallenger".into(),
)));
}
self.verify_payment_for_amount_with_body(credential_str, amount, body)
}
fn verify_payment_for_route(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
body: Option<&[u8]>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, MppError>> + Send>>
{
let verified = match body {
Some(body) => self.verify_payment_for_amount_scope_and_body(
credential_str,
amount,
mppx_scope,
body,
),
None => self.verify_payment_for_amount_and_scope(credential_str, amount, mppx_scope),
};
Box::pin(async move { verified.await.map_err(MppError::verification_failed) })
}
fn credential_header(&self) -> &str {
header::AUTHORIZATION.as_str()
}
fn challenges(
&self,
amount: &str,
options: ChallengeOptions,
) -> Result<Vec<PaymentChallenge>, String> {
self.challenge(amount, options)
.map(|challenge| vec![challenge])
}
fn challenges_with_body(
&self,
amount: &str,
options: ChallengeOptions,
body: &[u8],
) -> Result<Vec<PaymentChallenge>, String> {
self.challenge_with_body(amount, options, body)
.map(|challenge| vec![challenge])
}
}
#[cfg(any(feature = "stripe", feature = "tempo"))]
fn verify_expected<M, S>(
mpp: &super::Mpp<M, S>,
credential_str: &str,
expected: crate::error::Result<PaymentChallenge>,
body: Option<&[u8]>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, MppError>> + Send>>
where
M: crate::protocol::traits::ChargeMethod + Clone + Send + Sync + 'static,
S: Clone + Send + Sync + 'static,
{
let prepared = parse_authorization(credential_str).and_then(|credential| {
let expected_request = expected?.request.decode()?;
Ok((credential, expected_request))
});
let mpp = mpp.clone();
let body = body.map(<[u8]>::to_vec);
Box::pin(async move {
let (credential, expected_request) = prepared?;
let receipt = match &body {
Some(body) => {
mpp.verify_credential_with_expected_request_and_body(
&credential,
&expected_request,
body,
)
.await
}
None => {
mpp.verify_credential_with_expected_request(&credential, &expected_request)
.await
}
};
Ok(receipt?)
})
}
#[cfg(any(feature = "stripe", feature = "tempo"))]
fn stringify_error(
verified: std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Receipt, MppError>> + Send>,
>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
Box::pin(async move { verified.await.map_err(|e| e.to_string()) })
}
#[cfg(feature = "tempo")]
impl<P, S> ChargeChallenger for super::Mpp<super::TempoChargeMethod<P>, S>
where
P: alloy::providers::Provider<tempo_alloy::TempoNetwork> + Clone + Send + Sync + 'static,
S: Clone + Send + Sync + 'static,
{
fn challenge(
&self,
amount: &str,
options: ChallengeOptions,
) -> Result<PaymentChallenge, String> {
self.charge_with_options(
amount,
super::ChargeOptions {
description: options.description,
mppx_scope: options.mppx_scope.as_ref(),
..Default::default()
},
)
.map(|mut offers| offers.remove(0))
.map_err(|e| e.to_string())
}
fn challenge_with_body(
&self,
amount: &str,
options: ChallengeOptions,
body: &[u8],
) -> Result<PaymentChallenge, String> {
self.charge_with_options_and_body(
amount,
super::ChargeOptions {
description: options.description,
mppx_scope: options.mppx_scope.as_ref(),
..Default::default()
},
body,
)
.map(|mut offers| offers.remove(0))
.map_err(|e| e.to_string())
}
fn verify_payment(
&self,
credential_str: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let credential = match parse_authorization(credential_str) {
Ok(c) => c,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Invalid credential: {}",
e
))))
}
};
let mpp = self.clone();
Box::pin(async move {
super::Mpp::broadcast_credential(&mpp, &credential)
.await
.map_err(|e| e.to_string())
})
}
fn verify_payment_for_amount(
&self,
credential_str: &str,
amount: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let credential = match parse_authorization(credential_str) {
Ok(c) => c,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Invalid credential: {}",
e
))))
}
};
let expected_challenge = match self.charge(amount).map(|mut offers| offers.remove(0)) {
Ok(challenge) => challenge,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to generate expected challenge: {}",
e
))))
}
};
let expected_request = match expected_challenge.request.decode() {
Ok(request) => request,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to decode expected request: {}",
e
))))
}
};
let mpp = self.clone();
Box::pin(async move {
super::Mpp::verify_credential_with_expected_request(
&mpp,
&credential,
&expected_request,
)
.await
.map_err(|e| e.to_string())
})
}
fn verify_payment_for_amount_and_scope(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
stringify_error(self.verify_payment_for_route(credential_str, amount, mppx_scope, None))
}
fn verify_payment_for_amount_with_body(
&self,
credential_str: &str,
amount: &str,
body: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let credential = match parse_authorization(credential_str) {
Ok(c) => c,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Invalid credential: {}",
e
))))
}
};
let expected_challenge = match self.charge(amount).map(|mut offers| offers.remove(0)) {
Ok(challenge) => challenge,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to generate expected challenge: {}",
e
))))
}
};
let expected_request = match expected_challenge.request.decode() {
Ok(request) => request,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to decode expected request: {}",
e
))))
}
};
let mpp = self.clone();
let body = body.to_vec();
Box::pin(async move {
super::Mpp::verify_credential_with_expected_request_and_body(
&mpp,
&credential,
&expected_request,
&body,
)
.await
.map_err(|e| e.to_string())
})
}
fn verify_payment_for_amount_scope_and_body(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
body: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
stringify_error(self.verify_payment_for_route(
credential_str,
amount,
mppx_scope,
Some(body),
))
}
fn verify_payment_for_route(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
body: Option<&[u8]>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, MppError>> + Send>>
{
let expected = self
.charge_with_options(
amount,
super::ChargeOptions {
mppx_scope: mppx_scope.as_ref(),
..Default::default()
},
)
.map(|mut offers| offers.remove(0));
verify_expected(self, credential_str, expected, body)
}
fn credential_header(&self) -> &str {
super::Mpp::credential_header(self)
}
fn challenges(
&self,
amount: &str,
options: ChallengeOptions,
) -> Result<Vec<PaymentChallenge>, String> {
self.charge_with_options(
amount,
super::ChargeOptions {
description: options.description,
mppx_scope: options.mppx_scope.as_ref(),
..Default::default()
},
)
.map_err(|e| e.to_string())
}
fn challenges_with_body(
&self,
amount: &str,
options: ChallengeOptions,
body: &[u8],
) -> Result<Vec<PaymentChallenge>, String> {
self.charge_with_options_and_body(
amount,
super::ChargeOptions {
description: options.description,
mppx_scope: options.mppx_scope.as_ref(),
..Default::default()
},
body,
)
.map_err(|e| e.to_string())
}
}
#[cfg(feature = "stripe")]
impl<S> ChargeChallenger for super::Mpp<super::StripeChargeMethod, S>
where
S: Clone + Send + Sync + 'static,
{
fn challenge(
&self,
amount: &str,
options: ChallengeOptions,
) -> Result<PaymentChallenge, String> {
self.stripe_charge_with_options(
amount,
super::StripeChargeOptions {
description: options.description,
mppx_scope: options.mppx_scope.as_ref(),
..Default::default()
},
)
.map_err(|e| e.to_string())
}
fn challenge_with_body(
&self,
amount: &str,
options: ChallengeOptions,
body: &[u8],
) -> Result<PaymentChallenge, String> {
self.stripe_charge_with_options_and_body(
amount,
super::StripeChargeOptions {
description: options.description,
mppx_scope: options.mppx_scope.as_ref(),
..Default::default()
},
body,
)
.map_err(|e| e.to_string())
}
fn verify_payment(
&self,
credential_str: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let credential = match parse_authorization(credential_str) {
Ok(c) => c,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Invalid credential: {}",
e
))))
}
};
let mpp = self.clone();
Box::pin(async move {
super::Mpp::broadcast_credential(&mpp, &credential)
.await
.map_err(|e| e.to_string())
})
}
fn verify_payment_for_amount(
&self,
credential_str: &str,
amount: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let credential = match parse_authorization(credential_str) {
Ok(c) => c,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Invalid credential: {}",
e
))))
}
};
let expected_challenge = match self.stripe_charge(amount) {
Ok(challenge) => challenge,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to generate expected challenge: {}",
e
))))
}
};
let expected_request = match expected_challenge.request.decode() {
Ok(request) => request,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to decode expected request: {}",
e
))))
}
};
let mpp = self.clone();
Box::pin(async move {
super::Mpp::verify_credential_with_expected_request(
&mpp,
&credential,
&expected_request,
)
.await
.map_err(|e| e.to_string())
})
}
fn verify_payment_for_amount_and_scope(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
stringify_error(self.verify_payment_for_route(credential_str, amount, mppx_scope, None))
}
fn verify_payment_for_amount_with_body(
&self,
credential_str: &str,
amount: &str,
body: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
let credential = match parse_authorization(credential_str) {
Ok(c) => c,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Invalid credential: {}",
e
))))
}
};
let expected_challenge = match self.stripe_charge(amount) {
Ok(challenge) => challenge,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to generate expected challenge: {}",
e
))))
}
};
let expected_request = match expected_challenge.request.decode() {
Ok(request) => request,
Err(e) => {
return Box::pin(std::future::ready(Err(format!(
"Failed to decode expected request: {}",
e
))))
}
};
let mpp = self.clone();
let body = body.to_vec();
Box::pin(async move {
super::Mpp::verify_credential_with_expected_request_and_body(
&mpp,
&credential,
&expected_request,
&body,
)
.await
.map_err(|e| e.to_string())
})
}
fn verify_payment_for_amount_scope_and_body(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
body: &[u8],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, String>> + Send>> {
stringify_error(self.verify_payment_for_route(
credential_str,
amount,
mppx_scope,
Some(body),
))
}
fn verify_payment_for_route(
&self,
credential_str: &str,
amount: &str,
mppx_scope: Option<serde_json::Value>,
body: Option<&[u8]>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Receipt, MppError>> + Send>>
{
let expected = self.stripe_charge_with_options(
amount,
super::StripeChargeOptions {
mppx_scope: mppx_scope.as_ref(),
..Default::default()
},
);
verify_expected(self, credential_str, expected, body)
}
fn credential_header(&self) -> &str {
super::Mpp::credential_header(self)
}
}
impl<S, C> FromRequestParts<S> for MppCharge<C>
where
Arc<dyn ChargeChallenger>: FromRef<S>,
C: ChargeConfig,
S: Send + Sync,
{
type Rejection = MppChargeRejection;
fn from_request_parts(
parts: &mut http_types::request::Parts,
state: &S,
) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
let challenger: Arc<dyn ChargeChallenger> = FromRef::from_ref(state);
let mppx_scope = mppx_scope_from_parts(parts);
let auth_header = parts
.headers
.get(challenger.credential_header())
.and_then(|v| v.to_str().ok())
.and_then(extract_payment_scheme)
.map(|s| s.to_string());
async move {
let options = ChallengeOptions {
description: C::description(),
mppx_scope: mppx_scope.clone(),
};
let credential_str = match auth_header {
Some(c) => c,
None => {
let offers = challenger
.challenges(C::amount(), options)
.map_err(MppChargeRejection::InternalError)?;
return Err(MppChargeRejection::from_offers(offers));
}
};
let receipt = match challenger
.verify_payment_for_route(&credential_str, C::amount(), mppx_scope, None)
.await
{
Ok(r) => r,
Err(error) => {
return Err(MppChargeRejection::from_error(error, || {
challenger.challenges(C::amount(), options)
}));
}
};
Ok(MppCharge {
receipt,
_config: std::marker::PhantomData,
})
}
}
}
impl<S, C> FromRequest<S> for MppChargeWithBody<C>
where
Arc<dyn ChargeChallenger>: FromRef<S>,
C: ChargeConfig,
S: Send + Sync,
{
type Rejection = MppChargeRejection;
fn from_request(
req: Request,
state: &S,
) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
let challenger: Arc<dyn ChargeChallenger> = FromRef::from_ref(state);
let (parts, body) = req.into_parts();
let mppx_scope = mppx_scope_from_parts(&parts);
let auth_header = parts
.headers
.get(challenger.credential_header())
.and_then(|v| v.to_str().ok())
.and_then(extract_payment_scheme)
.map(|s| s.to_string());
let req = Request::from_parts(parts, body);
async move {
let body = Bytes::from_request(req, state)
.await
.map_err(MppChargeRejection::Body)?;
let options = ChallengeOptions {
description: C::description(),
mppx_scope: mppx_scope.clone(),
};
let credential_str = match auth_header {
Some(c) => c,
None => {
let offers = challenger
.challenges_with_body(C::amount(), options, &body)
.map_err(MppChargeRejection::InternalError)?;
return Err(MppChargeRejection::from_offers(offers));
}
};
let receipt = match challenger
.verify_payment_for_route(&credential_str, C::amount(), mppx_scope, Some(&body))
.await
{
Ok(r) => r,
Err(error) => {
return Err(MppChargeRejection::from_error(error, || {
challenger.challenges_with_body(C::amount(), options, &body)
}));
}
};
Ok(MppChargeWithBody {
receipt,
body,
_config: std::marker::PhantomData,
})
}
}
}
fn mppx_scope_from_parts(parts: &http_types::request::Parts) -> Option<serde_json::Value> {
let mut scope = serde_json::Map::new();
let path = parts.uri.path();
let route = parts
.extensions
.get::<::axum::extract::MatchedPath>()
.map(|matched| matched.as_str())
.unwrap_or(path);
if !route.is_empty() {
scope.insert("route".into(), serde_json::Value::String(route.to_string()));
}
if !path.is_empty() {
scope.insert(
"resource".into(),
serde_json::Value::String(path.to_string()),
);
}
if let Some(query) = parts.uri.query() {
if !query.is_empty() {
scope.insert("query".into(), serde_json::Value::String(query.to_string()));
}
}
if scope.is_empty() {
None
} else {
Some(serde_json::Value::Object(scope))
}
}
pub struct WithReceipt<T> {
pub receipt: Receipt,
pub body: T,
}
impl<T: IntoResponse> IntoResponse for WithReceipt<T> {
fn into_response(self) -> axum_core::response::Response {
let mut resp = self.body.into_response();
if !resp.status().is_success() {
return resp;
}
let Ok(header_val) = format_receipt(&self.receipt) else {
return resp;
};
let Ok(val) = HeaderValue::from_str(&header_val) else {
return resp;
};
resp.headers_mut().insert(PAYMENT_RECEIPT_HEADER, val);
let existing_cc = resp
.headers()
.get_all(header::CACHE_CONTROL)
.iter()
.filter_map(|v| v.to_str().ok())
.collect::<Vec<_>>()
.join(", ");
let cache_control = with_private_cache_control(Some(existing_cc.as_str()));
if let Ok(cc) = HeaderValue::from_str(&cache_control) {
resp.headers_mut().insert(header::CACHE_CONTROL, cc);
}
resp
}
}
#[cfg(test)]
#[allow(clippy::result_large_err)]
mod tests;