Repository: netwo-io/apistos (rust)

Issue:
Only documents one endpoint for each path
`main.rs`
```rust
use actix_web::middleware::Logger;
use actix_web::web::Json;
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;
use apistos::{api_operation, ApiComponent};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::net::Ipv4Addr;

#[derive(Serialize, Deserialize, Debug, Clone, ApiComponent, JsonSchema)]
pub struct Experiment {}

#[api_operation(summary = "Get")]
pub async fn get() -> Result<Json<Experiment>, ActixError> {
    unimplemented!()
}
#[api_operation(summary = "Post")]
pub async fn post(_data: Json<Experiment>) -> Result<Json<i64>, 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(
            web::scope("/api/v1")
                .service(
                    web::scope("/experiments")
                        .route("", web::post().to(post))
                        .route("", web::get().to(get))
                    )
      )
      .build("/openapi.json")
  })
  .bind((Ipv4Addr::UNSPECIFIED, 8080))?
  .run()
  .await
}
```

`Cargo.toml`
```toml
[package]
name = "t"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.8.0"
apistos = "0.3.5"
schemars = { package = "apistos-schemars", version = "0.8" }
serde = "1.0.204"
uuid = "1.10.0"
```

Generated schema, converted to YAML for readability
```yaml
openapi: 3.0.3
info:
  title: A well documented API
  description: |-
    This is an API documented using Apistos,
    a wonderful new tool to document your actix API !
  version: ''
servers:
  - url: /api/v3
paths:
  /api/v1/experiments:
    get:
      summary: Get
      operationId: get_api-v1-experiments-703fc88c4982422c0d38cfc3589f324c
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Experiment'
      deprecated: false
components:
  schemas:
    Experiment:
      title: Experiment
      type: object

```

As you may see it does not document the `POST` endpoint. If I change the order, it only documents the `POST` endpoint and not the `GET` endpoint.


Pull request description:
Fix scope overriding same path operations
Fix #127 

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 2be0cf8..8f5bc74 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,7 +22,7 @@ documentation = "https://docs.rs/apistos/"
 license = "MIT OR Apache-2.0"
 rust-version = "1.75"
 publish = true
-version = "0.3.5"
+version = "0.3.6"
 
 [workspace.dependencies]
 actix-service = "2"
diff --git a/apistos-core/Cargo.toml b/apistos-core/Cargo.toml
index 66b166e..a62a4eb 100644
--- a/apistos-core/Cargo.toml
+++ b/apistos-core/Cargo.toml
@@ -29,7 +29,7 @@ serde_qs = { workspace = true, features = ["actix4"], optional = true }
 uuid = { workspace = true, optional = true }
 url = { workspace = true, optional = true }
 
-apistos-models = { path = "../apistos-models", version = "0.3.5", features = ["deserialize"] }
+apistos-models = { path = "../apistos-models", version = "0.3.6", features = ["deserialize"] }
 
 [dev-dependencies]
 assert-json-diff = { workspace = true }
diff --git a/apistos-rapidoc/Cargo.toml b/apistos-rapidoc/Cargo.toml
index a3ef041..a7ddfd1 100644
--- a/apistos-rapidoc/Cargo.toml
+++ b/apistos-rapidoc/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.5" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.6" }
 
 [lints]
 workspace = true
diff --git a/apistos-redoc/Cargo.toml b/apistos-redoc/Cargo.toml
index 86fbd04..1977f68 100644
--- a/apistos-redoc/Cargo.toml
+++ b/apistos-redoc/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.5" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.6" }
 
 [lints]
 workspace = true
diff --git a/apistos-scalar/Cargo.toml b/apistos-scalar/Cargo.toml
index 44a99ff..23a924e 100644
--- a/apistos-scalar/Cargo.toml
+++ b/apistos-scalar/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.5" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.6" }
 
 [lints]
 workspace = true
diff --git a/apistos-shuttle/Cargo.toml b/apistos-shuttle/Cargo.toml
index adca916..bb12c25 100644
--- a/apistos-shuttle/Cargo.toml
+++ b/apistos-shuttle/Cargo.toml
@@ -18,7 +18,7 @@ actix-web = { workspace = true }
 num_cpus = { workspace = true }
 shuttle-runtime = { workspace = true }
 
-apistos = { path = "../apistos", version = "0.3.5" }
+apistos = { path = "../apistos", version = "0.3.6" }
 
 [lints]
 workspace = true
diff --git a/apistos-swagger-ui/Cargo.toml b/apistos-swagger-ui/Cargo.toml
index 2eddfe7..7f9b12f 100644
--- a/apistos-swagger-ui/Cargo.toml
+++ b/apistos-swagger-ui/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.5" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.6" }
 
 [lints]
 workspace = true
diff --git a/apistos/Cargo.toml b/apistos/Cargo.toml
index b39011c..6fa003e 100644
--- a/apistos/Cargo.toml
+++ b/apistos/Cargo.toml
@@ -25,25 +25,25 @@ schemars = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 
-apistos-core = { path = "../apistos-core", version = "0.3.5" }
-apistos-gen = { path = "../apistos-gen", version = "0.3.5" }
-apistos-models = { path = "../apistos-models", version = "0.3.5" }
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.5" }
-apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.5", optional = true }
-apistos-redoc = { path = "../apistos-redoc", version = "0.3.5", optional = true }
-apistos-scalar = { path = "../apistos-scalar", version = "0.3.5", optional = true }
-apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.5", optional = true }
+apistos-core = { path = "../apistos-core", version = "0.3.6" }
+apistos-gen = { path = "../apistos-gen", version = "0.3.6" }
+apistos-models = { path = "../apistos-models", version = "0.3.6" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.6" }
+apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.6", optional = true }
+apistos-redoc = { path = "../apistos-redoc", version = "0.3.6", optional = true }
+apistos-scalar = { path = "../apistos-scalar", version = "0.3.6", optional = true }
+apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.6", optional = true }
 
 
 [dev-dependencies]
 actix-web-lab = { workspace = true }
 garde-actix-web = { workspace = true }
 
-apistos-models = { path = "../apistos-models", version = "0.3.5", features = ["deserialize"] }
-apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.5" }
-apistos-redoc = { path = "../apistos-redoc", version = "0.3.5" }
-apistos-scalar = { path = "../apistos-scalar", version = "0.3.5" }
-apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.5" }
+apistos-models = { path = "../apistos-models", version = "0.3.6", features = ["deserialize"] }
+apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.6" }
+apistos-redoc = { path = "../apistos-redoc", version = "0.3.6" }
+apistos-scalar = { path = "../apistos-scalar", version = "0.3.6" }
+apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.6" }
 
 [lints]
 workspace = true
diff --git a/apistos/src/internal/actix/scope.rs b/apistos/src/internal/actix/scope.rs
index abe0c30..4feddfb 100644
--- a/apistos/src/internal/actix/scope.rs
+++ b/apistos/src/internal/actix/scope.rs
@@ -10,12 +10,11 @@ use actix_web::Error;
 use apistos_models::components::Components;
 use apistos_models::paths::PathItem;
 use indexmap::IndexMap;
-use std::collections::BTreeMap;
 use std::fmt::Debug;
 use std::future::Future;
 
 pub struct Scope<S = actix_web::Scope> {
-  pub(crate) item_map: BTreeMap<String, PathItem>,
+  pub(crate) item_map: IndexMap<String, PathItem>,
   pub(crate) components: Vec<Components>,
   tags: Vec<String>,
   path: String,
@@ -175,9 +174,11 @@ where
   }
 
   fn update_from_def_holder<D: DefinitionHolder>(&mut self, dh: &mut D) {
-    self.components.extend(dh.components());
-    let mut item_map = IndexMap::new();
+    let mut item_map: IndexMap<String, PathItem> = IndexMap::new();
     dh.update_path_items(&mut item_map);
+
+    self.components.extend(dh.components());
+
     for (path, mut path_item) in item_map {
       let p = [self.path.clone(), path]
         .iter()
@@ -191,7 +192,9 @@ where
         operation.tags.append(&mut self.tags.clone());
       }
 
-      self.item_map.insert(p, path_item);
+      let op_map = self.item_map.entry(p).or_default();
+      //@todo we should probably merge the path items together instead but for now only operations can be defined using apistos here
+      op_map.operations.extend(path_item.operations);
     }
   }
 }


Test patch:
diff --git a/apistos-gen-test/Cargo.toml b/apistos-gen-test/Cargo.toml
index 92e4d7f..9062819 100644
--- a/apistos-gen-test/Cargo.toml
+++ b/apistos-gen-test/Cargo.toml
@@ -21,8 +21,8 @@ assert-json-diff = { workspace = true }
 chrono = { workspace = true, features = ["serde"] }
 futures-core = { workspace = true }
 apistos = { path = "../apistos", features = ["multipart", "uuid"] }
-apistos-core = { path = "../apistos-core", version = "0.3.5", features = ["actix-web-grants"] }
-apistos-gen = { path = "../apistos-gen", version = "0.3.5" }
+apistos-core = { path = "../apistos-core", version = "0.3.6", features = ["actix-web-grants"] }
+apistos-gen = { path = "../apistos-gen", version = "0.3.6" }
 # we use the "preserve_order" feature from schemars here following https://github.com/netwo-io/apistos/pull/78
 schemars = { workspace = true, features = ["preserve_order"] }
 serde = { workspace = true, features = ["derive"] }
diff --git a/apistos/tests/routing.rs b/apistos/tests/routing.rs
index 8e21332..90a4480 100644
--- a/apistos/tests/routing.rs
+++ b/apistos/tests/routing.rs
@@ -62,7 +62,10 @@ async fn actix_routing() {
         .route("test3", patch().to(test))
         .route("test4/{test_id}", patch().to(test))
         .app_data("")
-        .configure(my_routes),
+        .configure(my_routes)
+        .service(scope("test5").route("", post().to(test)).route("", get().to(test)))
+        .service(resource("test6").route(post().to(test)))
+        .service(resource("test6").route(get().to(test))),
     )
     .build("/openapi.json");
   let app = init_service(app).await;
@@ -80,11 +83,18 @@ async fn actix_routing() {
     "/test/test2/",
     "/test/test3",
     "/test/test4/{test_id}",
+    "/test/test5",
+    "/test/test6",
     "/test/users/{user_id}",
     "/test/{plop_id}/{clap_name}",
   ];
 
-  assert_eq!(paths, expected_paths)
+  assert_eq!(paths, expected_paths);
+
+  assert_eq!(
+    body.paths.paths.values().flat_map(|v| v.operations.values()).count(),
+    10
+  );
 }
 
 // Imports bellow aim at making clippy happy. Those dependencies are necessary for integration-test.