magnetar/src/web/pagination.rs

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)
}
}