unis 0.1.8

轻量级CQRS框架。
Documentation
use crate::{domain::Command, i18n};
use axum::{
    body::Bytes,
    extract::{FromRequest, Json, Request},
    http::{StatusCode, header::ACCEPT_LANGUAGE},
    response::{IntoResponse, Response},
};
use rkyv::{
    Archive, Deserialize,
    bytecheck::CheckBytes,
    de::Pool,
    rancor::{Error, Strategy},
    util::AlignedVec,
    validation::{Validator, archive::ArchiveValidator, shared::SharedValidator},
};
use serde::de::DeserializeOwned;
use std::marker::PhantomData;
use uuid::Uuid;
use validator::Validate;

/// 命令请求的路径参数
#[derive(serde::Deserialize)]
pub struct UniKey {
    /// 聚合 Id
    pub agg_id: Uuid,
    /// 聚合命令 Id
    pub com_id: Uuid,
}

/// Json 格式
pub struct JsonFormat;
/// Rkyv 格式
pub struct RkyvFormat;

/// 解析命令请求体
pub struct UniCommand<T, F>(pub T, pub String, pub PhantomData<F>);

impl<T, S> FromRequest<S> for UniCommand<T, RkyvFormat>
where
    T: Command + Validate,
    <T as Archive>::Archived: Deserialize<T, Strategy<Pool, Error>>,
    <T as Archive>::Archived:
        for<'m> CheckBytes<Strategy<Validator<ArchiveValidator<'m>, SharedValidator>, Error>>,
    Bytes: FromRequest<S>,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
        let lang = extract_language(&req);
        let bytes = Bytes::from_request(req, state)
            .await
            .map_err(|e| e.into_response())?;

        let required_align = std::mem::align_of::<T::Archived>();
        let com = match bytes.as_ptr().align_offset(required_align) {
            0 => rkyv::from_bytes::<T, Error>(&bytes)
                .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()).into_response())?,
            _ => {
                let mut aligned = AlignedVec::<16>::with_capacity(bytes.len());
                aligned.extend_from_slice(&bytes);
                rkyv::from_bytes::<T, Error>(&aligned)
                    .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()).into_response())?
            }
        };

        com.validate().map_err(|e| {
            let mut result = String::new();
            let err = match i18n::validation(&mut result, &e, &lang) {
                Ok(()) => result,
                Err(_) => e.to_string(),
            };
            (StatusCode::BAD_REQUEST, err).into_response()
        })?;

        Ok(UniCommand(com, lang, PhantomData))
    }
}

impl<T, S> FromRequest<S> for UniCommand<T, JsonFormat>
where
    T: Command + Validate + DeserializeOwned,
    Bytes: FromRequest<S>,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
        let lang = extract_language(&req);
        let bytes = Bytes::from_request(req, state)
            .await
            .map_err(|e| e.into_response())?;

        let Json(com) = Json::<T>::from_bytes(&bytes).map_err(|e| e.into_response())?;

        com.validate().map_err(|e| {
            let mut result = String::new();
            let err = match i18n::validation(&mut result, &e, &lang) {
                Ok(()) => result,
                Err(_) => e.to_string(),
            };
            (StatusCode::BAD_REQUEST, err).into_response()
        })?;

        Ok(UniCommand(com, lang, PhantomData))
    }
}

fn extract_language(req: &Request) -> String {
    req.headers()
        .get(ACCEPT_LANGUAGE)
        .and_then(|v| v.to_str().ok())
        .and_then(|ls| ls.split(',').next())
        .and_then(|l| l.split(';').next())
        .and_then(|t| t.split('-').next())
        .unwrap_or("zh")
        .to_string()
}