Repository: netwo-io/apistos (rust)

Issue:
Generates wrong path
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/

Pull request:
Fix wrong generated path with route function
Fixes #122 

Patch:
diff --git a/apistos/src/internal/actix/scope.rs b/apistos/src/internal/actix/scope.rs
index 55c4fff..abe0c30 100644
--- a/apistos/src/internal/actix/scope.rs
+++ b/apistos/src/internal/actix/scope.rs
@@ -107,7 +107,7 @@ where
 
   /// Wrapper for [`actix_web::Scope::route`](https://docs.rs/actix-web/*/actix_web/struct.Scope.html#method.route).
   pub fn route(mut self, path: &str, route: Route) -> Self {
-    let mut w = RouteWrapper::new(&self.path, route);
+    let mut w = RouteWrapper::new(path, route);
     self.update_from_def_holder(&mut w);
     self.inner = self.inner.take().map(|s| s.route(path, w.inner));
     self


Test patch:
diff --git a/apistos/tests/routing.rs b/apistos/tests/routing.rs
index f3cb1bd..8e21332 100644
--- a/apistos/tests/routing.rs
+++ b/apistos/tests/routing.rs
@@ -9,8 +9,8 @@ use apistos_gen::api_operation;
 use apistos_models::info::Info;
 use apistos_models::tag::Tag;
 
-#[test]
-fn actix_routing() {
+#[actix_web::test]
+async fn actix_routing() {
   fn my_routes(cfg: &mut ServiceConfig) {
     cfg.service(
       resource("/users/{user_id}")
@@ -50,7 +50,7 @@ fn actix_routing() {
     ..Default::default()
   };
 
-  App::new()
+  let app = App::new()
     .document(spec)
     .service(
       scope("test")
@@ -60,16 +60,39 @@ fn actix_routing() {
           tagged_scope("test2", vec!["Another super tag".to_string()]).service(resource("/").route(post().to(test))),
         )
         .route("test3", patch().to(test))
+        .route("test4/{test_id}", patch().to(test))
         .app_data("")
         .configure(my_routes),
     )
     .build("/openapi.json");
+  let app = init_service(app).await;
+
+  let req = TestRequest::get().uri("/openapi.json").to_request();
+  let resp = call_service(&app, req).await;
+  assert!(resp.status().is_success());
+
+  let body: OpenApi = try_read_body_json(resp).await.expect("Unable to read body");
+  let mut paths: Vec<&String> = body.paths.paths.keys().collect();
+  paths.sort();
+
+  let expected_paths = vec![
+    "/test/line/{plop_id}",
+    "/test/test2/",
+    "/test/test3",
+    "/test/test4/{test_id}",
+    "/test/users/{user_id}",
+    "/test/{plop_id}/{clap_name}",
+  ];
+
+  assert_eq!(paths, expected_paths)
 }
 
 // Imports bellow aim at making clippy happy. Those dependencies are necessary for integration-test.
 use actix_service as _;
+use actix_web::test::{call_service, init_service, try_read_body_json, TestRequest};
 use actix_web_lab as _;
 use apistos_core as _;
+use apistos_models::OpenApi;
 use apistos_plugins as _;
 use apistos_rapidoc as _;
 use apistos_redoc as _;