Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add examples for wrapped Query and Path extractors #137

Open
borolgs opened this issue May 13, 2024 · 4 comments
Open

Add examples for wrapped Query and Path extractors #137

borolgs opened this issue May 13, 2024 · 4 comments

Comments

@borolgs
Copy link

borolgs commented May 13, 2024

For example, I wrapped Query to Another Extractor as recommended for getting a custom error response:

{
  "error": "QueryValidation",
  "details": "Failed to deserialize query string"
}

And on the aide side, I did this:

#[derive(FromRequestParts, OperationIo)]
#[from_request(via(axum::extract:: Query), rejection(Error))]
#[aide(
    input_with = "axum::extract::Query<T>", // axum::extract:: Path<T>
    output_with = "axum_jsonschema::Json<T>",
    json_schema
)]
pub struct Query<T>(pub T);

If it's correct, maybe it needs to be added to the documentation?

I think it's a common pattern.

@Rolv-Apneseth
Copy link

Thanks for this. Were you able to use this in a handler? I'm getting an error complaining about an unmet trait bound when using with get_with, whereas it works fine with the regular Query extractor.

@borolgs
Copy link
Author

borolgs commented Jul 1, 2024

@Rolv-Apneseth, here is a fully working example.
There is some redundant error mapping here. The main thing is to have impl From<QueryRejection> for Error.

GET http://localhost:3030/test/some-path?some_query=11

use aide::{
    axum::{routing::get_with, ApiRouter, IntoApiResponse},
    openapi::OpenApi,
    transform::TransformOpenApi,
    OperationIo,
};
use axum::{
    extract::rejection::{PathRejection, QueryRejection},
    http::StatusCode,
    response::{IntoResponse, Response},
};
use axum_jsonschema::JsonSchemaRejection;
use axum_macros::{FromRequest, FromRequestParts};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;
use tracing_subscriber::prelude::*;

// Errors

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("validation")]
    SchemaValidation(JsonSchemaRejection),
    #[error("validation")]
    QueryValidation(QueryRejection),
    #[error("validation")]
    PathValidation(PathRejection),

    #[error("unexpected")]
    Unexpected(String),
}

impl From<QueryRejection> for Error {
    fn from(rejection: QueryRejection) -> Self {
        Self::QueryValidation(rejection)
    }
}

impl From<PathRejection> for Error {
    fn from(rejection: PathRejection) -> Self {
        Self::PathValidation(rejection)
    }
}

impl From<JsonSchemaRejection> for Error {
    fn from(rejection: JsonSchemaRejection) -> Self {
        Self::SchemaValidation(rejection)
    }
}

impl IntoResponse for Error {
    fn into_response(self) -> Response {
        match self {
            Error::QueryValidation(error) => ErrorResponse::QueryValidation {
                message: error.body_text(),
            }
            .into_response(),
            _ => ErrorResponse::Unexpected {
                message: "Unexpected error".into(),
            }
            .into_response(),
        }
    }
}

#[derive(Serialize, JsonSchema)]
#[serde(tag = "error", rename_all = "snake_case")]
pub enum ErrorResponse {
    QueryValidation { message: String },
    Unexpected { message: String },
}

impl IntoResponse for ErrorResponse {
    fn into_response(self) -> Response {
        let status = match &self {
            ErrorResponse::QueryValidation { message } => StatusCode::BAD_REQUEST,
            _ => StatusCode::INTERNAL_SERVER_ERROR,
        };
        (status, Json(self)).into_response()
    }
}

pub type Result<T> = std::result::Result<T, Error>;

// Aide newtypes

#[derive(FromRequest, OperationIo)]
#[from_request(via(axum_jsonschema::Json), rejection(Error))]
#[aide(
    input_with = "axum_jsonschema::Json<T>",
    output_with = "axum_jsonschema::Json<T>",
    json_schema
)]
pub struct Json<T>(pub T);

impl<T> IntoResponse for Json<T>
where
    T: Serialize,
{
    fn into_response(self) -> axum::response::Response {
        axum::Json(self.0).into_response()
    }
}

#[derive(FromRequestParts, OperationIo)]
#[from_request(via(axum::extract::Query), rejection(Error))]
#[aide(
    input_with = "axum::extract::Query<T>",
    output_with = "axum_jsonschema::Json<T>",
    json_schema
)]
#[aide]
pub struct Query<T>(pub T);

#[derive(FromRequestParts, OperationIo)]
#[from_request(via(axum::extract::Path), rejection(Error))]
#[aide(
    input_with = "axum::extract::Path<T>",
    output_with = "axum_jsonschema::Json<T>",
    json_schema
)]
pub struct Path<T>(pub T);

// Example api

#[derive(Deserialize, JsonSchema)]
struct SomePath {
    some_path: String,
}
#[derive(Deserialize, JsonSchema)]
struct SomeQuery {
    some_query: String,
}

#[derive(Serialize, JsonSchema)]
struct SomeResponse {
    hello: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
                "playground_example=debug,tower_http=debug,axum::rejection=trace".into()
            }),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    let mut api = OpenApi::default();

    let app = ApiRouter::new()
        .api_route(
            "/test/:some_path",
            get_with(test, |t| t.response::<200, Json<SomeResponse>>()),
        )
        .finish_api_with(&mut api, api_docs)
        .layer(TraceLayer::new_for_http());

    let json = serde_json::to_string_pretty(&api).unwrap();
    tracing::debug!("{}", json);

    let listener = TcpListener::bind(format!("127.0.0.1:3030")).await.unwrap();

    tracing::info!("listening on http://{}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();

    Ok(())
}

#[axum::debug_handler]
async fn test(
    Path(SomePath { some_path }): Path<SomePath>,
    Query(SomeQuery { some_query }): Query<SomeQuery>,
) -> impl IntoApiResponse {
    tracing::debug!("path: {some_path}, query: {some_query}");
    Json(SomeResponse {
        hello: "world".into(),
    })
    .into_response()
}

fn api_docs(api: TransformOpenApi) -> TransformOpenApi {
    api.title("Aide axum Open API")
        .summary("An example application")
        .default_response::<Json<ErrorResponse>>()
}

@Rolv-Apneseth
Copy link

Rolv-Apneseth commented Jul 1, 2024

impl From<QueryRejection> for Error

Ah didn't know about that. Thank you so much, I'll have a closer look at your code and try it out later.

@Rolv-Apneseth
Copy link

Yep, that was exactly it, thanks again. I agree this should be noted in the documentation or an example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants