Repository: ngyn-rs/ngyn (language: rust)

Issue:
Controller path prefix



Discussion / hints:

Fix patch:
diff --git a/crates/core/src/platforms/hyper.rs b/crates/core/src/platforms/hyper.rs
index 0fd9d9a5..2cee336f 100644
--- a/crates/core/src/platforms/hyper.rs
+++ b/crates/core/src/platforms/hyper.rs
@@ -49,16 +49,17 @@ impl HyperApplication {
             let middlewares = Arc::clone(&middlewares);
             async move {
                 let (parts, mut body) = req.into_parts();
-                let req = Request::from_parts(
-                    parts,
-                    body.frame()
-                        .await
-                        .unwrap()
-                        .unwrap()
-                        .into_data()
-                        .unwrap()
-                        .to_vec(),
-                );
+                let body = {
+                    let mut buf = Vec::new();
+                    if let Some(frame) = body.frame().await {
+                        let chunk = frame?.into_data();
+                        if let Ok(data) = chunk {
+                            buf.extend_from_slice(&data)
+                        }
+                    }
+                    buf
+                };
+                let req = Request::from_parts(parts, body);
                 let res = NgynResponse::init(req, routes, middlewares).await;
 
                 Ok::<_, hyper::Error>(res)
diff --git a/crates/macros/src/common/controller_macro.rs b/crates/macros/src/common/controller_macro.rs
index 03f74d70..48215872 100644
--- a/crates/macros/src/common/controller_macro.rs
+++ b/crates/macros/src/common/controller_macro.rs
@@ -4,20 +4,37 @@ use quote::quote;
 use crate::utils::parse_macro_data;
 
 struct ControllerArgs {
+    prefix: Option<syn::LitStr>,
     middlewares: Vec<syn::Path>,
 }
 
 impl syn::parse::Parse for ControllerArgs {
-    /// Parses a string like `routes = "get_location, get_location_weather", middlewares = [WeatherGate]`
+    /// Parses a string like `prefix="/weather", middlewares = [WeatherGate]`
     /// into a `ControllerArgs` struct.
     fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
         let mut middlewares = vec![];
+        let mut prefix = None;
+
+        // match the input with format "/prefix"
+        if !input.is_empty() && input.peek(syn::LitStr) {
+            let path: syn::LitStr = input.parse()?;
+            prefix = Some(path);
+
+            return Ok(ControllerArgs {
+                middlewares,
+                prefix,
+            });
+        }
 
         while !input.is_empty() {
             let ident: syn::Ident = input.parse()?;
             input.parse::<syn::Token![=]>()?;
 
             match ident.to_string().as_str() {
+                "prefix" => {
+                    let path: syn::LitStr = input.parse()?;
+                    prefix = Some(path);
+                }
                 "middlewares" => {
                     let content;
                     syn::bracketed!(content in input);
@@ -41,7 +58,10 @@ impl syn::parse::Parse for ControllerArgs {
             }
         }
 
-        Ok(ControllerArgs { middlewares })
+        Ok(ControllerArgs {
+            middlewares,
+            prefix,
+        })
     }
 }
 
@@ -53,7 +73,10 @@ pub fn controller_macro(args: TokenStream, input: TokenStream) -> TokenStream {
         vis,
         generics,
     } = syn::parse_macro_input!(input as syn::DeriveInput);
-    let args = syn::parse_macro_input!(args as ControllerArgs);
+    let ControllerArgs {
+        middlewares,
+        prefix,
+    } = syn::parse_macro_input!(args as ControllerArgs);
     let (types, keys) = parse_macro_data(data);
 
     let fields: Vec<_> = types
@@ -66,8 +89,7 @@ pub fn controller_macro(args: TokenStream, input: TokenStream) -> TokenStream {
         })
         .collect();
 
-    let add_middlewares: Vec<_> = args
-        .middlewares
+    let add_middlewares: Vec<_> = middlewares
         .iter()
         .map(|m| {
             quote! {
@@ -77,11 +99,23 @@ pub fn controller_macro(args: TokenStream, input: TokenStream) -> TokenStream {
         })
         .collect();
 
+    let path_prefix = {
+        if let Some(prefix) = prefix {
+            quote! {
+                #prefix.to_string() + "/"
+            }
+        } else {
+            quote! {
+                "".to_string()
+            }
+        }
+    };
+
     let expanded = quote! {
         #(#attrs)*
         #[ngyn::macros::dependency]
         #vis struct #ident #generics {
-            middlewares: Vec<std::sync::Arc<dyn ngyn::prelude::NgynMiddleware>>,
+            data: ngyn::prelude::NgynControllerData,
             #(#fields),*
         }
 
@@ -105,7 +139,7 @@ pub fn controller_macro(args: TokenStream, input: TokenStream) -> TokenStream {
             fn new(middlewares: Vec<std::sync::Arc<dyn ngyn::prelude::NgynMiddleware>>) -> Self {
                 #(#add_middlewares)*
                 let mut controller = #ident {
-                    middlewares,
+                    data: ngyn::prelude::NgynControllerData::new(middlewares),
                     #(#keys: ngyn::prelude::NgynProvider.provide()),*
                 };
                 controller
@@ -114,13 +148,13 @@ pub fn controller_macro(args: TokenStream, input: TokenStream) -> TokenStream {
             fn routes(&self) -> Vec<(String, String, String)> {
                 use ngyn::prelude::NgynControllerRoutePlaceholder;
                 Self::routes.iter().map(|(path, method, handler)| {
-                    (path.to_string(), method.to_string(), handler.to_string())
+                    ((#path_prefix + path).replace("//", "/"), method.to_string(), handler.to_string())
                 }).collect()
             }
 
             async fn handle(&self, handler: &str, cx: &mut ngyn::prelude::NgynContext, res: &mut ngyn::prelude::NgynResponse) {
                 use ngyn::prelude::NgynControllerRoutePlaceholder;
-                self.middlewares.iter().for_each(|middleware| {
+                self.data.middlewares().iter().for_each(|middleware| {
                     middleware.handle(cx, res);
                 });
                 self.__handle_route(handler, cx, res).await;
diff --git a/crates/shared/src/server/response.rs b/crates/shared/src/server/response.rs
index 1b33bd87..961fb50a 100644
--- a/crates/shared/src/server/response.rs
+++ b/crates/shared/src/server/response.rs
@@ -104,10 +104,6 @@ impl ResponseBuilder for NgynResponse {
         let mut cx = NgynContext::from_request(req);
         let mut res = Response::new(Full::new(Bytes::default()));
 
-        middlewares
-            .iter()
-            .for_each(|middleware| middleware.handle(&mut cx, &mut res));
-
         let handler = routes
             .iter()
             .filter_map(|(path, method, handler)| {
@@ -119,6 +115,10 @@ impl ResponseBuilder for NgynResponse {
             })
             .next();
 
+        middlewares
+            .iter()
+            .for_each(|middleware| middleware.handle(&mut cx, &mut res));
+
         if let Some(handler) = handler {
             handler(&mut cx, &mut res);
             cx.execute(&mut res).await;
diff --git a/crates/shared/src/traits/controller_trait.rs b/crates/shared/src/traits/controller_trait.rs
index e6d2a5d2..84a2735b 100644
--- a/crates/shared/src/traits/controller_trait.rs
+++ b/crates/shared/src/traits/controller_trait.rs
@@ -32,3 +32,18 @@ pub trait NgynControllerRoutePlaceholder {
         res: &mut crate::NgynResponse,
     );
 }
+
+pub struct NgynControllerData {
+    middlewares: Vec<std::sync::Arc<dyn super::NgynMiddleware>>,
+}
+
+impl NgynControllerData {
+    /// Creates a new instance of `NgynControllerData`.
+    pub fn new(middlewares: Vec<std::sync::Arc<dyn super::NgynMiddleware>>) -> Self {
+        Self { middlewares }
+    }
+
+    pub fn middlewares(&self) -> &Vec<std::sync::Arc<dyn super::NgynMiddleware>> {
+        &self.middlewares
+    }
+}
diff --git a/examples/weather_app/src/modules/weather/weather_controller.rs b/examples/weather_app/src/modules/weather/weather_controller.rs
index e75b59ae..42f51a29 100644
--- a/examples/weather_app/src/modules/weather/weather_controller.rs
+++ b/examples/weather_app/src/modules/weather/weather_controller.rs
@@ -12,14 +12,14 @@ pub struct WeatherDto {
     pub humidity: f32,
 }
 
-#[controller(middlewares = [])]
+#[controller("/weather")]
 pub struct WeatherController {
     weather_service: WeatherService,
 }
 
 #[routes]
 impl WeatherController {
-    #[get("/weather/<location>/<city>")]
+    #[get("/<location>/<city>")]
     #[check(WeatherGate)]
     async fn get_location(&self, params: Param) -> String {
         self.weather_service
@@ -27,7 +27,7 @@ impl WeatherController {
             .await
     }
 
-    #[post("/weather")]
+    #[post("/")]
     async fn post_location(&self, weather: WeatherDto) -> String {
         let location = weather.location;
         self.weather_service.get_location_weather(&location).await


Test patch: