Repository: netwo-io/apistos (rust)
Issue: The end position of the URL path is missing "/"
I define the route in the following way.

`web::scope("/ticket").service(resource("/").route(put().to(update_ticket)))`

apistos: `/ticket`
actix-web: `/ticket/`

Pull request description: Fix #90: do not remove trailing slash when unnecessary

Patch:
diff --git a/apistos/src/internal/actix/scope.rs b/apistos/src/internal/actix/scope.rs
index e96a3c4..55c4fff 100644
--- a/apistos/src/internal/actix/scope.rs
+++ b/apistos/src/internal/actix/scope.rs
@@ -181,7 +181,8 @@ where
     for (path, mut path_item) in item_map {
       let p = [self.path.clone(), path]
         .iter()
-        .map(|p| p.trim_matches('/'))
+        .filter(|p| !p.is_empty())
+        .map(|p| p.trim_start_matches('/'))
         .collect::<Vec<&str>>()
         .join("/");
 
diff --git a/deny.toml b/deny.toml
index 28981cd..2f7cb6e 100644
--- a/deny.toml
+++ b/deny.toml
@@ -9,24 +9,6 @@
 # The values provided in this template are the default values that will be used
 # when any section or field is not specified in your own configuration
 
-# If 1 or more target triples (and optionally, target_features) are specified,
-# only the specified targets will be checked when running `cargo deny check`.
-# This means, if a particular package is only ever used as a target specific
-# dependency, such as, for example, the `nix` crate only being used via the
-# `target_family = "unix"` configuration, that only having windows targets in
-# this list would mean the nix crate, as well as any of its exclusive
-# dependencies not shared by any other crates, would be ignored, as the target
-# list here is effectively saying which targets you are building for.
-targets = [
-    # The triple can be any string, but only the target triples built in to
-    # rustc (as of 1.40) can be checked against actual config expressions
-    #{ triple = "x86_64-unknown-linux-musl" },
-    # You can also specify which target_features you promise are enabled for a
-    # particular target. target_features are currently not validated against
-    # the actual valid features supported by the target architecture.
-    #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
-]
-
 # This section is considered when running `cargo deny check advisories`
 # More documentation for the advisories section can be found here:
 # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
@@ -35,16 +17,8 @@ targets = [
 db-path = "~/.cargo/advisory-db"
 # The url(s) of the advisory databases to use
 db-urls = ["https://github.com/rustsec/advisory-db"]
-# The lint level for security vulnerabilities
-vulnerability = "deny"
-# The lint level for unmaintained crates
-unmaintained = "deny"
 # The lint level for crates that have been yanked from their source registry
 yanked = "deny"
-# The lint level for crates with security notices. Note that as of
-# 2019-12-17 there are no security notice advisories in
-# https://github.com/rustsec/advisory-db
-notice = "warn"
 # A list of advisory IDs to ignore. Note that ignored advisories will still
 # output a note when they are encountered.
 ignore = []
@@ -68,8 +42,6 @@ ignore = []
 # More documentation for the licenses section can be found here:
 # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
 [licenses]
-# The lint level for crates which do not have a detectable license
-unlicensed = "deny"
 # List of explicitly allowed licenses
 # See https://spdx.org/licenses/ for list of possible licenses
 # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
@@ -78,27 +50,8 @@ allow = [
     "Apache-2.0",
     "BSD-3-Clause",
     "Unicode-DFS-2016",
+    "Unicode-3.0"
 ]
-# List of explicitly disallowed licenses
-# See https://spdx.org/licenses/ for list of possible licenses
-# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
-deny = [
-    #"Nokia",
-]
-# Lint level for licenses considered copyleft
-copyleft = "deny"
-# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
-# * both - The license will be approved if it is both OSI-approved *AND* FSF
-# * either - The license will be approved if it is either OSI-approved *OR* FSF
-# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
-# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
-# * neither - This predicate is ignored and the default lint level is used
-allow-osi-fsf-free = "neither"
-# Lint level used when no other predicates are matched
-# 1. License isn't in the allow or deny lists
-# 2. License isn't copyleft
-# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
-default = "deny"
 # The confidence threshold for detecting a license from license text.
 # The higher the value, the more closely the license text must be to the
 # canonical license text of a valid SPDX license file.


Test patch:
diff --git a/apistos/tests/operations.rs b/apistos/tests/operations.rs
new file mode 100644
index 0000000..014da33
--- /dev/null
+++ b/apistos/tests/operations.rs
@@ -0,0 +1,89 @@
+#![allow(clippy::expect_used)]
+#![allow(clippy::panic)]
+
+use actix_web::test::{call_service, init_service, try_read_body_json, TestRequest};
+use actix_web::{App, Error};
+use apistos::app::OpenApiWrapper;
+use apistos::spec::Spec;
+use apistos::web::{put, resource, scope, Scope};
+use apistos_gen::api_operation;
+use apistos_models::info::Info;
+use apistos_models::OpenApi;
+
+#[actix_web::test]
+async fn operations() {
+  const OAS_PATH: &str = "/api-docs/openapi.json";
+
+  const PATH_PREFIX: &str = "/api/rest/v1/finance";
+
+  fn router_api_v1() -> Scope {
+    scope(PATH_PREFIX)
+      .service(api_ticket())
+      .service(api_account_no_trailing_slash())
+  }
+
+  const TICKET_SUB_PATH_PREFIX: &str = "/ticket";
+  const ACCOUNT_SUB_PATH_PREFIX: &str = "/account";
+
+  pub(crate) fn api_ticket() -> Scope {
+    scope(TICKET_SUB_PATH_PREFIX).service(resource("/").route(put().to(update_ticket)))
+  }
+
+  pub(crate) fn api_account_no_trailing_slash() -> Scope {
+    scope(ACCOUNT_SUB_PATH_PREFIX).service(resource("").route(put().to(update_account)))
+  }
+
+  #[api_operation(tag = "ticket")]
+  pub(crate) async fn update_ticket() -> Result<String, Error> {
+    Ok("success".to_string())
+  }
+
+  #[api_operation(tag = "account")]
+  pub(crate) async fn update_account() -> Result<String, Error> {
+    Ok("success".to_string())
+  }
+
+  let spec = Spec {
+    info: Info {
+      title: "An API".to_string(),
+      version: "1.0.0".to_string(),
+      ..Default::default()
+    },
+    ..Default::default()
+  };
+  let app = App::new().document(spec).service(router_api_v1()).build(OAS_PATH);
+  let app = init_service(app).await;
+
+  let req = TestRequest::get().uri(OAS_PATH).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 paths = body.paths.paths;
+  assert_eq!(paths.len(), 2);
+
+  let (path, _path_item) = paths.iter().find(|(p, _)| p.contains("ticket")).unwrap();
+  assert_eq!(path, "/api/rest/v1/finance/ticket/");
+
+  let (path, _path_item) = paths.iter().find(|(p, _)| p.contains("account")).unwrap();
+  assert_eq!(path, "/api/rest/v1/finance/account");
+}
+
+// Imports bellow aim at making clippy happy. Those dependencies are necessary for integration-test.
+use actix_service as _;
+use actix_web_lab as _;
+use apistos_core as _;
+use apistos_plugins as _;
+use apistos_rapidoc as _;
+use apistos_redoc as _;
+use apistos_scalar as _;
+use apistos_swagger_ui as _;
+use garde_actix_web as _;
+use indexmap as _;
+use log as _;
+use md5 as _;
+use once_cell as _;
+use regex as _;
+use schemars as _;
+use serde as _;
+use serde_json as _;