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

Document may not be right #1054

Open
ipconfiger opened this issue Sep 18, 2024 · 9 comments
Open

Document may not be right #1054

ipconfiger opened this issue Sep 18, 2024 · 9 comments
Labels
investigate Futher investigation needed before other action

Comments

@ipconfiger
Copy link

in :
Doc

It is possible to specify the title of each variant to help generators create named structures.

#[derive(ToSchema)]
enum ErrorResponse {
    #[schema(title = "InvalidCredentials")]
    InvalidCredentials,
    #[schema(title = "NotFound")]
    NotFound(String),
}

but build will fault:

   --> src/response.rs:133:14
    |
133 |     #[schema(title="Token String")]
    |              ^^^^^

Cargo.toml

utoipa = { version = "4.2.3", features = ["axum_extras"] }
utoipa-rapidoc = { version = "4", features = ["axum"] }

What i miss?

@juhaku
Copy link
Owner

juhaku commented Sep 21, 2024

@ipconfiger Indeed the docs might be incorrect, and in 4.x.x version there are lot of inconsistencies in enum schema processing. This is being fixed in PR #1059. Yet if you have only a plain enum having only unit variants, then title is not supported, see more details here: #954

@juhaku
Copy link
Owner

juhaku commented Oct 23, 2024

@ipconfiger I belive this has been solved with the 5.0.0 release. If you encounter more issues this can of course be reopened but for now closing.

@juhaku juhaku closed this as completed Oct 23, 2024
@abergmeier
Copy link

For me this still fails with unexpected attribute: title, expected any of: rename in 5.x

@juhaku
Copy link
Owner

juhaku commented Jan 29, 2025

For me this still fails with unexpected attribute: title, expected any of: rename in 5.x

Does the above example of @ipconfiger fail or do you have some other code that fails? Perhaps this needs some future investigation for possible fix.

@juhaku juhaku reopened this Jan 29, 2025
@juhaku juhaku added the investigate Futher investigation needed before other action label Jan 29, 2025
@abergmeier
Copy link

For me this still fails with unexpected attribute: title, expected any of: rename in 5.x

Does the above example of @ipconfiger fail or do you have some other code that fails? Perhaps this needs some future investigation for possible fix.

It still does complain about title. Maybe simply add this scenario to your tests?

@juhaku
Copy link
Owner

juhaku commented Jan 30, 2025

#[derive(ToSchema)]
enum ErrorResponse {
    #[schema(title = "InvalidCredentials")]
    InvalidCredentials,
    #[schema(title = "NotFound")]
    NotFound(String),
}

The above enum with title attribute does work. However title attribute cannot be used with simple enum (enum with only unit variants). That is because the simple enum will translate to the OpenAPI string enum representation https://json-schema.org/understanding-json-schema/reference/enum and cannot contain other descriptive fields. Otherwise the simple enum representation should also be changed to oneOf composition type instead.

So this will not work:

#[derive(ToSchema)]
enum ErrorResponse {
    #[schema(title = "InvalidCredentials")]
    InvalidCredentials,
    #[schema(title = "NotFound")]
    NotFound,
}

There are plenty of tests for enums with title e.g. this one in schema_derive_test.rs file: https://github.com/juhaku/utoipa/blob/88a5842a71f859a826f990a735f3bc312ef10749/utoipa-gen/tests/schema_derive_test.rs#L950C1-L966

@abergmeier
Copy link

The real question, which I did not find answered anywhere, is how do you properly express a string enum with utoipa:

components:
  schemas:
    Foo:
      type: object
      properties:
        state:
          type: string
          enum: [Free, InUse, ToCheck, Blocked, Disabled, Reserved]
#[derive(ToSchema)]
pub struct Foo {
  // whatever we need to insert here
}

#[derive(utoipa::OpenApi)]
#[openapi(
    components(schemas(Foo)),
)]
pub struct ApiDoc;

I (and I do not seem to be alone) assumed that utoipa is intelligent enough to generate bindings which bridge the divide between Rust and OpenApi enums.

pub enum MachineState {
    Free,
    InUse,
    ToCheck,
    Blocked,
    Disabled,
    Reserved,
}

@juhaku
Copy link
Owner

juhaku commented Jan 31, 2025

#[derive(ToSchema)]
pub enum MachineState {
   Free,
   InUse,
   ToCheck,
   Blocked,
   Disabled,
   Reserved,
}

The above will generate the spec as follows:

type: string
enum: [Free, InUse, ToCheck, Blocked, Disabled, Reserved]

@abergmeier
Copy link

I have this code and somehow the MachineState never shows up in schemas output in utoipa:

use crate::resources::modules::fabaccess::Status;
use crate::rest::user::User;
use crate::ResourcesHandle;
use crate::SessionManager;
use axum::extract::Path;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::Json;
use http::StatusCode;
use std::sync::Arc;
use tracing::info_span;
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
use uuid::Uuid;

#[utoipa::path(
  post,
  path = "/machines",
  summary = "Register machine",
  responses(
      (status = StatusCode::OK, response = responses::Machine),
      (status = StatusCode::BAD_REQUEST),
      (status = StatusCode::UNAUTHORIZED, response = crate::rest::authentication::responses::Unauthorized),
      (status = StatusCode::FORBIDDEN),
      (status = StatusCode::NOT_IMPLEMENTED),
  ),
)]
async fn register_machine(State(_ctx): State<Arc<MachineContext>>) -> impl IntoResponse {
    (StatusCode::NOT_IMPLEMENTED, Json(())).into_response()
}

#[utoipa::path(
  delete,
  path = "/machines/{id}",
  params(
      ("id" = String, description = "Machine ID"),
  ),
  summary = "Unregister machine",
  responses(
      (status = StatusCode::OK),
      (status = StatusCode::BAD_REQUEST),
      (status = StatusCode::UNAUTHORIZED, response = crate::rest::authentication::responses::Unauthorized),
      (status = StatusCode::FORBIDDEN),
      (status = StatusCode::NOT_IMPLEMENTED),
      (status = StatusCode::NOT_FOUND),
  ),
)]
async fn unregister_machine(
    State(_ctx): State<Arc<MachineContext>>,
    Path(_id): Path<String>,
) -> impl IntoResponse {
    (StatusCode::NOT_IMPLEMENTED, Json(())).into_response()
}

#[utoipa::path(
  get,
  path = "/machines/{id}",
  params(
      ("id" = String, description = "Machine ID"),
  ),
  summary = "Get machine",
  responses(
      (status = StatusCode::OK, response = responses::Machine),
      (status = StatusCode::BAD_REQUEST),
      (status = StatusCode::UNAUTHORIZED, response = crate::rest::authentication::responses::Unauthorized),
      (status = StatusCode::FORBIDDEN),
      (status = StatusCode::NOT_FOUND),
  ),
)]
async fn get_machine(
    State(ctx): State<Arc<MachineContext>>,
    Path(id): Path<String>,
) -> impl IntoResponse {
    let parent = info_span!("get_machine");
    if let Some(resource) = ctx.resources.get_by_id(id.as_str()) {
        let uid = "123";
        match ctx.sessionmanager.try_open(&parent, uid) {
            Some(session) => {
                if resource.visible(&session) {
                    responses::Machine::from(resource).into_response()
                } else {
                    (StatusCode::FORBIDDEN).into_response()
                }
            }
            None => (StatusCode::NOT_FOUND).into_response(),
        }
    } else {
        (StatusCode::NOT_FOUND).into_response()
    }
}

#[utoipa::path(
  get,
  path = "/machines",
  summary = "Get machine list",
  responses(
      (status = StatusCode::OK, response = responses::Machines),
      (status = StatusCode::BAD_REQUEST),
      (status = StatusCode::UNAUTHORIZED, response = crate::rest::authentication::responses::Unauthorized),
      (status = StatusCode::FORBIDDEN),
  ),
)]
async fn list_machines(State(ctx): State<Arc<MachineContext>>) -> impl IntoResponse {
    let parent = info_span!("list_machines");
    let uid = "123";
    match ctx.sessionmanager.try_open(&parent, uid) {
        Some(session) => {
            let machine_list: Vec<&crate::Resource> = ctx
                .resources
                .list_all()
                .into_iter()
                .filter(|resource| resource.visible(&session))
                .collect();

            responses::Machines::from(machine_list).into_response()
        }
        None => (StatusCode::NOT_FOUND).into_response(),
    }
}

#[derive(utoipa::ToSchema, serde::Deserialize, serde::Serialize)]
pub struct SetStateRequest {
  pub state: MachineState,
}

#[utoipa::path(
  post,
  path = "/machines/{id}/state",
  summary = "Set state of machine",
  params(
      ("id" = String, description = "Machine ID"),
  ),
  request_body = SetStateRequest,
  responses(
      (status = StatusCode::OK),
      (status = StatusCode::BAD_REQUEST),
      (status = StatusCode::UNAUTHORIZED, response = crate::rest::authentication::responses::Unauthorized),
      (status = StatusCode::FORBIDDEN),
      (status = StatusCode::NOT_FOUND),
  ),
)]
#[axum::debug_handler]
async fn set_state(
    State(ctx): State<Arc<MachineContext>>,
    Path(_id): Path<String>,
    Json(req): Json<SetStateRequest>,
) -> impl IntoResponse {
    let resource = ctx.resources.get_by_id(_id.as_str());
    if resource.is_none() {
        return (StatusCode::NOT_FOUND).into_response();
    }
    let resource = resource.unwrap();
    let parent = info_span!("list_machines");
    let uid = "123";
    let session = ctx.sessionmanager.try_open(&parent, uid);
    if session.is_none() {
        return (StatusCode::NOT_FOUND).into_response();
    }

    let session = session.unwrap();
    if !resource.visible(&session) {
        return (StatusCode::FORBIDDEN).into_response();
    }

    let state = match req.state {
        MachineState::Blocked => Status::Blocked(session.get_user_ref()),
        MachineState::Disabled => Status::Disabled,
        MachineState::Free => Status::Free,
        MachineState::InUse => Status::InUse(session.get_user_ref()),
        MachineState::Reserved => Status::Reserved(session.get_user_ref()),
        MachineState::ToCheck => Status::ToCheck(session.get_user_ref()),
    };
    resource.try_update(session.clone(), state).await;
    (StatusCode::OK).into_response()
}

#[utoipa::path(
  get,
  path = "/machine/{id}/reservations",
  summary = "Return reservations for resource",
  params(
      ("id" = String, description = "Machine ID"),
  ),
  responses(
      (status = StatusCode::OK, body = Vec<Reservation>),
      (status = StatusCode::BAD_REQUEST),
      (status = StatusCode::UNAUTHORIZED, response = crate::rest::authentication::responses::Unauthorized),
      (status = StatusCode::FORBIDDEN),
      (status = StatusCode::NOT_FOUND),
  ),
)]
async fn get_reservations(State(_ctx): State<Arc<MachineContext>>) -> impl IntoResponse {
    (StatusCode::NOT_IMPLEMENTED, Json(())).into_response()
}

#[derive(utoipa::ToSchema)]
pub struct Space {
    pub id: Uuid,
    pub name: String,
    pub info: String,
}

#[derive(utoipa::ToSchema, serde::Deserialize, serde::Serialize)]
pub enum MachineState {
    Free,
    InUse,
    ToCheck,
    Blocked,
    Disabled,
    Reserved,
}

#[derive(utoipa::ToSchema)]
pub struct Machine {
    pub id: String,
    pub space: Space,
    pub name: String,
    pub description: String,
    pub state: MachineState,
    pub manager: Option<User>,
    pub wiki: String,
    pub urn: String,
    pub category: String,
}

#[derive(utoipa::ToSchema, utoipa::ToResponse)]
pub struct Reservation {
    pub user: User,
    #[schema(format = DateTime)]
    pub start: String,
    #[schema(format = DateTime)]
    pub end: String,
}

pub mod responses {
    pub use crate::rest::authentication::responses::Unauthorized;
    use axum::body::Body;
    use axum::response::IntoResponse;
    use axum::Json;
    use http::StatusCode;
    use serde::Serialize;

    #[derive(Debug, Serialize, utoipa::ToResponse, utoipa::ToSchema)]
    pub struct Machine {
        pub id: String,
        pub name: String,
    }

    impl From<&crate::Resource> for Machine {
        fn from(resource: &crate::Resource) -> Self {
            Self {
                id: resource.get_id().to_string(),
                name: resource.get_name().to_string(),
            }
        }
    }

    impl IntoResponse for Machine {
        fn into_response(self) -> http::Response<Body> {
            (StatusCode::OK, Json(self)).into_response()
        }
    }

    #[derive(Debug, Serialize, utoipa::ToResponse)]
    pub struct Machines(Vec<Machine>);

    impl IntoResponse for Machines {
        fn into_response(self) -> http::Response<Body> {
            (StatusCode::OK, Json(self)).into_response()
        }
    }

    impl<'a> From<Vec<&crate::Resource>> for Machines {
        fn from(resources: Vec<&crate::Resource>) -> Self {
            Self(resources.iter().map(|r| Machine::from(*r)).collect())
        }
    }
}

pub fn router(sessionmanager: &SessionManager, ressources: &ResourcesHandle) -> OpenApiRouter {
    let ctx = Arc::new(MachineContext {
        resources: ressources.clone(),
        sessionmanager: sessionmanager.clone(),
    });
    OpenApiRouter::new()
        .routes(routes!(
            register_machine,
            unregister_machine,
            get_machine,
            list_machines,
            set_state,
            get_reservations,
        ))
        .with_state(ctx)
}

struct MachineContext {
    sessionmanager: SessionManager,
    resources: ResourcesHandle,
}

#[derive(utoipa::OpenApi)]
#[openapi(
    info(description = "FabAccess Machine API"),
    modifiers(&SecurityAddon),
    paths(
      register_machine,
      unregister_machine,
      get_machine,
      list_machines,
      set_state,
      get_reservations,
    ),
    components(responses(
      responses::Unauthorized,
      responses::Machine,
      responses::Machines,
    ),
    schemas(SetStateRequest)),
)]
pub struct ApiDoc;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
investigate Futher investigation needed before other action
Projects
None yet
Development

No branches or pull requests

3 participants