165 lines
5.1 KiB
Rust
165 lines
5.1 KiB
Rust
use crate::service::MagnetarService;
|
|
use crate::util::serialize_as_urlenc;
|
|
use crate::web::{ApiError, IntoErrorCode};
|
|
use axum::extract::rejection::QueryRejection;
|
|
use axum::extract::{FromRequestParts, OriginalUri, Query};
|
|
use axum::http::header::InvalidHeaderValue;
|
|
use axum::http::request::Parts;
|
|
use axum::http::{HeaderValue, StatusCode, Uri};
|
|
use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts};
|
|
use axum::RequestPartsExt;
|
|
use either::Either;
|
|
use itertools::Itertools;
|
|
use magnetar_core::web_model::rel::{RelNext, RelPrev};
|
|
use magnetar_model::sea_orm::prelude::async_trait::async_trait;
|
|
use magnetar_sdk::types::{PaginationShape, SpanFilter};
|
|
use magnetar_sdk::util_types::U64Range;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use strum::IntoStaticStr;
|
|
use thiserror::Error;
|
|
use tracing::error;
|
|
|
|
#[derive(Debug)]
|
|
pub struct Pagination {
|
|
base_uri: Uri,
|
|
pub current: SpanFilter,
|
|
pub prev: Option<SpanFilter>,
|
|
pub next: Option<SpanFilter>,
|
|
pub limit: U64Range<10, 100>,
|
|
query_rest: HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct PaginationQuery {
|
|
#[serde(flatten)]
|
|
pagination: PaginationShape,
|
|
#[serde(flatten)]
|
|
query_rest: HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Error, IntoStaticStr)]
|
|
pub enum PaginationBuilderError {
|
|
#[error("Query rejection: {0}")]
|
|
QueryRejection(#[from] QueryRejection),
|
|
#[error("HTTP error: {0}")]
|
|
HttpError(#[from] axum::http::Error),
|
|
#[error("Value of out of range error")]
|
|
OutOfRange,
|
|
#[error("Invalid header value")]
|
|
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
|
#[error("Query string serialization error: {0}")]
|
|
SerializationErrorQuery(#[from] serde_urlencoded::ser::Error),
|
|
#[error("Query string serialization error: {0}")]
|
|
SerializationErrorJson(#[from] serde_json::Error),
|
|
}
|
|
|
|
impl From<PaginationBuilderError> for ApiError {
|
|
fn from(err: PaginationBuilderError) -> Self {
|
|
Self {
|
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
|
code: err.error_code(),
|
|
message: if cfg!(debug_assertions) {
|
|
format!("Pagination builder error: {}", err)
|
|
} else {
|
|
"Pagination builder error".to_string()
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for PaginationBuilderError {
|
|
fn into_response(self) -> Response {
|
|
ApiError::from(self).into_response()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FromRequestParts<Arc<MagnetarService>> for Pagination {
|
|
type Rejection = PaginationBuilderError;
|
|
|
|
async fn from_request_parts(
|
|
parts: &mut Parts,
|
|
state: &Arc<MagnetarService>,
|
|
) -> Result<Self, Self::Rejection> {
|
|
let OriginalUri(original_uri) = parts.extract::<OriginalUri>().await.unwrap();
|
|
|
|
let base_uri = Uri::builder()
|
|
.scheme(state.config.networking.protocol.as_ref())
|
|
.authority(state.config.networking.host.as_str())
|
|
.path_and_query(original_uri.path())
|
|
.build()?;
|
|
|
|
let Query(PaginationQuery {
|
|
pagination,
|
|
query_rest,
|
|
}) = parts.extract::<Query<PaginationQuery>>().await?;
|
|
|
|
Ok(Pagination {
|
|
base_uri,
|
|
prev: None,
|
|
next: None,
|
|
current: pagination.pagination,
|
|
limit: pagination.limit,
|
|
query_rest,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl IntoResponseParts for Pagination {
|
|
type Error = PaginationBuilderError;
|
|
|
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
|
let wrap = |uri: Uri, query: String, rel: Either<RelPrev, RelNext>| {
|
|
format!(
|
|
"<{}?{}>; rel=\"{}\"",
|
|
uri,
|
|
query,
|
|
rel.as_ref()
|
|
.map_either(RelPrev::as_ref, RelNext::as_ref)
|
|
.into_inner()
|
|
)
|
|
};
|
|
|
|
let url_prev = if let Some(prev) = self.prev {
|
|
let query_prev = serialize_as_urlenc(&serde_json::to_value(PaginationQuery {
|
|
pagination: PaginationShape {
|
|
pagination: prev,
|
|
limit: self.limit,
|
|
},
|
|
query_rest: self.query_rest.clone(),
|
|
})?);
|
|
|
|
Some(wrap(
|
|
self.base_uri.clone(),
|
|
query_prev,
|
|
Either::Left(RelPrev),
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let url_next = if let Some(next) = self.next {
|
|
let query_next = serialize_as_urlenc(&serde_json::to_value(PaginationQuery {
|
|
pagination: PaginationShape {
|
|
pagination: next,
|
|
limit: self.limit,
|
|
},
|
|
query_rest: self.query_rest,
|
|
})?);
|
|
|
|
Some(wrap(self.base_uri, query_next, Either::Right(RelNext)))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let parts = [url_prev, url_next].iter().flatten().join(", ");
|
|
if !parts.is_empty() {
|
|
res.headers_mut()
|
|
.insert(axum::http::header::LINK, HeaderValue::from_str(&parts)?);
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|