netwo-io / apistos

Actix-web wrapper for automatic OpenAPI 3.0 documentation generation.
MIT License
146 stars 5 forks source link

Generates wrong path #122

Closed Ploppz closed 3 months ago

Ploppz commented 4 months ago

I made a running example reproducing one of the API endpoints of my applications that generates a strange openapi.json. Code below.

The problem is firstly that it generates the wrong URI path: /api/v1/admin/admin. Expected would be /api/v1/admin/measurement/{measurement_id}. Secondly, the swagger editor complains:

Semantic error at paths./api/v1/admin/admin.delete.parameters.0.name
Path parameter "" must have the corresponding {} segment in the "/api/v1/admin/admin" path
Jump to line 16

main.rs

use actix_web::middleware::Logger;
use actix_web::web::{Json, Path};
use actix_web::Error as ActixError;
use actix_web::{App, HttpServer};
use apistos::app::OpenApiWrapper;
use apistos::info::Info;
use apistos::server::Server;
use apistos::spec::Spec;
use apistos::web::{delete, scope};
use apistos::{api_operation, ApiComponent};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::net::Ipv4Addr;
use uuid::Uuid;

#[derive(Serialize, Deserialize, Debug, Clone, ApiComponent, JsonSchema)]
pub struct DeleteMeasurementInput {
    pub disk: Option<MoveMeasurement>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, ApiComponent, JsonSchema)]
pub enum MoveMeasurement {
    /// Move to create/ folder
    ToCreate,
    /// Move to _DELETE/ folder
    Delete,
}

#[derive(Serialize, Deserialize, Debug, Clone, ApiComponent, JsonSchema)]
pub struct DeleteMeasurementOutput {
    pub delete_job_id: Result<Uuid, String>,
    /// Whether the measurement was moved on disk, with eventual error.
    /// Ok(None) means no action was done
    pub disk_result: Result<Option<MoveMeasurement>, String>,
}

#[api_operation(summary = "Get an element from the todo list")]
pub async fn admin_delete_measurement(
    _path: Path<i64>,
    _body: Json<DeleteMeasurementInput>,
) -> Result<Json<DeleteMeasurementOutput>, ActixError> {
    unimplemented!()
}

#[actix_web::main]
async fn main() -> Result<(), impl Error> {
    HttpServer::new(move || {
    let spec = Spec {
      info: Info {
        title: "A well documented API".to_string(),
        description: Some(
          "This is an API documented using Apistos,\na wonderful new tool to document your actix API !".to_string(),
        ),
        ..Default::default()
      },
      servers: vec![Server {
        url: "/api/v3".to_string(),
        ..Default::default()
      }],
      ..Default::default()
    };

    App::new()
      .document(spec)
      .wrap(Logger::default())
      .service(
                scope("/api/v1")
                    .service(scope("/admin").route(
                        "/measurements/{measurement_id}",
                        delete().to(admin_delete_measurement),
                    )),
      )
      .build("/openapi.json")
  })
  .bind((Ipv4Addr::UNSPECIFIED, 8080))?
  .run()
  .await
}

Cargo.toml

[package]
name = "t"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.8.0"
apistos = { version = "0.1", features = ["extras", "qs_query"] }
schemars = { package = "apistos-schemars", version = "0.8", features = ["chrono", "uuid1", "url", "rust_decimal"] }
serde = "1.0.204"
uuid = "1.10.0"

The generated openapi.json:

{"openapi":"3.0.3","info":{"title":"A well documented API","description":"This is an API documented using Apistos,\na wonderful new tool to document your actix API !","version":""},"servers":[{"url":"/api/v3"}],"paths":{"/api/v1/admin/admin":{"delete":{"summary":"Get an element from the todo list","operationId":"delete_api-v1-admin-admin-f8aeb0610e9d768f59fdb7944e4f9ae1","parameters":[{"name":"","in":"path","required":true,"schema":{"title":"int64","type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteMeasurementInput"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteMeasurementOutput"}}}}},"deprecated":false}}},"components":{"schemas":{"DeleteMeasurementInput":{"title":"DeleteMeasurementInput","type":"object","properties":{"disk":{"allOf":[{"$ref":"#/components/schemas/MoveMeasurement"}],"nullable":true}}},"DeleteMeasurementOutput":{"title":"DeleteMeasurementOutput","type":"object","required":["delete_job_id","disk_result"],"properties":{"delete_job_id":{"$ref":"#/components/schemas/Result_of_Uuid_or_String"},"disk_result":{"description":"Whether the measurement was moved on disk, with eventual error. Ok(None) means no action was done","allOf":[{"$ref":"#/components/schemas/Result_of_Nullable_MoveMeasurement_or_String"}]}}},"MoveMeasurement":{"oneOf":[{"title":"ToCreate","description":"Move to create/ folder","type":"string","enum":["ToCreate"]},{"title":"Delete","description":"Move to _DELETE/ folder","type":"string","enum":["Delete"]}]},"Result_of_Nullable_MoveMeasurement_or_String":{"oneOf":[{"title":"Ok","type":"object","required":["Ok"],"properties":{"Ok":{"allOf":[{"$ref":"#/components/schemas/MoveMeasurement"}],"nullable":true}}},{"title":"Err","type":"object","required":["Err"],"properties":{"Err":{"type":"string"}}}]},"Result_of_Uuid_or_String":{"oneOf":[{"title":"Ok","type":"object","required":["Ok"],"properties":{"Ok":{"type":"string","format":"uuid"}}},{"title":"Err","type":"object","required":["Err"],"properties":{"Err":{"type":"string"}}}]}}}}

Try it at https://editor.swagger.io/

AzHicham commented 4 months ago

Hello,

I'm not sure but I think I faced the same issue.

Instead of

.service(
      scope("/admin")
      .route("/measurements/{measurement_id}",
             delete().to(admin_delete_measurement),
   ))

Try this:

.service(
   scope("/admin")
   .service(resource("/measurements/{measurement_id}").route(delete().to(admin_delete_measurement)))
)
Ploppz commented 4 months ago

Thanks a lot, that helped!