rustswaggerrefactoring

How to simplify SwaggerUI code in Rust's Utoipa


I'm currently generating the Swagger docs HTTP error responses by adding them to each URI function like this:

#[utoipa::path(
    context_path = "/user",
    tag = "user",
    responses(
        (status = 200, description = "Endpoint to get all data from a user.",
            body = Value,
            example = json!({
                "id": 12,
                "user_id": "TestUser42",
                "class": "8a",
                "created_at": "07-04-2004",
                "user_data": {
                    "traffic": [],
                    "modules": []
                }
            })
        ),
        (status = 401, description = "Access token is missing or invalid",
            body = HttpError,
            example = json!({
                "time": "07-05-2024 08:17:33",
                "status_code": "401",
                "error_type": "Unauthorized",
                "reason": "Access token is missing or invalid"
            })
        ),
        (status = 404, description = "ID not found", body = HttpError,
            example = json!({
                "time": "07-05-2024 08:17:33",
                "status_code": "404",
                "error_type": "NotFound",
                "reason": "ID not found"
            })
        ),
        (status = 403, description = "Not allowed to call this endpoint with the current permission", body = HttpError,
            example = json!({
                "time": "07-05-2024 08:17:33",
                "status_code": "403",
                "error_type": "AccessForbidden",
                "reason": "Wrong or missing permissions"
            })
        )
    ),
    security(
        ("bearerAuth" = [])
    )
)]
#[get("/get")]
#[protect(any("STUDENT", "ADMIN", "TEACHER", "MAKERSPACE"), error = "access_denied")]
pub async fn get_user(
    claims: Option<web::ReqData<Claims>>
) -> impl Responder {

    if let Some(claims) = claims {
        match user_service::get_user(claims.sub.as_str()).await {
            Ok(u) => HttpResponse::Ok().json(u),
            Err(_) => HttpError::not_found("User does not exist").error_response()
        }
    } else {
        HttpError::bad_gateway("Unable to get user").error_response()
    }
}

How can refactor this by putting the HTTP error tuples into a JSON file or a different Rust file?

Because the 401, 403, and 404 are always the same code snippets.


Solution

  • After some help from the Rust forum I found a solution!

    I had to use IntoResponses as @kmdreko mentioned before and I didn't know that I can use ContentBuilder which allows me to create an example and return Content afterwards with build().

    You can find my solution here if you want to implement it too.

    impl IntoResponses for HttpError {
        fn responses() -> BTreeMap<String, RefOr<Response>> {
            ResponsesBuilder::new()
                .response("401", ResponseBuilder::new()
                    .description("JWT invalid or missing")
                    .content(
                        "application/json",
                        ContentBuilder::new()
                            .example(Some(json!({
                                "time": "07-05-2024 08:17:33".to_string(),
                                "status_code": "401".to_string(),
                                "error_type": ErrorResponse::Unauthorized,
                                "reason": Some("JWT missing or invalid".to_string()),
                             }))
                            ).build()
                    )
                )
                .response("403", ResponseBuilder::new()
                    .description("Not allowed to call this endpoint with the current permission")
                    .content(
                        "application/json",
                        ContentBuilder::new()
                            .example(Some(json!( {
                                 "time": "07-05-2024 08:17:33".to_string(),
                                 "status_code": "403".to_string(),
                                 "error_type": ErrorResponse::Forbidden,
                                 "reason": Some("Access Forbidden".to_string()),
                            }))
                            ).build()
                    )
                )
                .build()
                .into()
        }
    }
    

    HttpError is my struct to handle HTTP Error responses.