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

Issue:
Add support for `multipart/formdata`
There are a few options that are possible. One good one is multer


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6e31451b..b0d87718 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1663,6 +1663,23 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "multer"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 1.1.0",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
+]
+
 [[package]]
 name = "ngyn"
 version = "0.4.5"
@@ -1816,8 +1833,10 @@ version = "0.4.5"
 dependencies = [
  "async-trait",
  "bytes",
+ "futures-util",
  "http 1.1.0",
  "http-body-util",
+ "multer",
  "regex",
  "serde",
  "serde_json",
diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml
index 9deceedf..541a31dd 100644
--- a/crates/shared/Cargo.toml
+++ b/crates/shared/Cargo.toml
@@ -14,8 +14,10 @@ rust-version = "1.75"
 [dependencies]
 async-trait = { workspace = true }
 bytes = { workspace = true }
+futures-util = { version = "0.3", default-features = false }
 http-body-util = { workspace = true }
 http = { workspace = true }
+multer = "3.1.0"
 regex = "1.10"
 serde = { workspace = true }
 serde_json = { workspace = true }
diff --git a/crates/shared/src/server/transformer.rs b/crates/shared/src/server/transformer.rs
index ce4e00ac..3a4c0322 100644
--- a/crates/shared/src/server/transformer.rs
+++ b/crates/shared/src/server/transformer.rs
@@ -1,3 +1,8 @@
+use bytes::Bytes;
+use futures_util::StreamExt;
+use http::{header::CONTENT_TYPE, HeaderValue};
+use http_body_util::{BodyStream, Full};
+use multer::Multipart;
 use serde::Deserialize;
 
 use crate::server::NgynContext;
@@ -54,10 +59,9 @@ impl<'a> Transducer {
     ///     }
     /// }
     ///
-    /// let mut cx = NgynContext::new();
-    /// let mut res = NgynResponse::new();
+    /// let mut cx = NgynContext::default();
     ///
-    /// let result: MyTransformer = Transducer::reduce(&mut cx, &mut res);
+    /// let result: MyTransformer = Transducer::reduce(&mut cx);
     /// ```
     #[must_use]
     pub fn reduce<S: Transformer<'a>>(cx: &'a mut NgynContext) -> S {
@@ -123,9 +127,8 @@ impl Transformer<'_> for Param {
     /// use crate::{context::NgynContext, NgynResponse};
     ///
     /// let mut cx = NgynContext::new();
-    /// let mut res = NgynResponse::new();
     ///
-    /// let param: Param = Param::transform(&mut cx, &mut res);
+    /// let param: Param = Param::transform(&mut cx);
     /// ```
     fn transform(cx: &mut NgynContext) -> Self {
         let data: Vec<(Cow<'static, str>, Cow<'static, str>)> = cx
@@ -196,13 +199,12 @@ impl Transformer<'_> for Query {
     /// use crate::{context::NgynContext, NgynResponse};
     /// use hyper::Uri;
     ///
-    /// let mut cx = NgynContext::new();
-    /// let mut res = NgynResponse::new();
+    /// let mut cx = NgynContext::default();
     ///
     /// let uri: Uri = "https://example.com/?id=123&name=John".parse().unwrap();
     /// cx.request.set_uri(uri);
     ///
-    /// let query: Query = Query::transform(&mut cx, &mut res);
+    /// let query: Query = Query::transform(&mut cx);
     /// ```
     fn transform(cx: &mut NgynContext) -> Self {
         Query {
@@ -212,12 +214,14 @@ impl Transformer<'_> for Query {
 }
 
 /// Represents a data transfer object struct.
-pub struct Body {
-    data: String,
+pub struct Body<'b> {
+    content_type: Option<&'b HeaderValue>,
+    data: &'b Vec<u8>,
 }
 
-impl Body {
+impl<'b> Body<'b> {
     /// Parses the data into the specified type using serde deserialization.
+    /// Once read, the body data is consumed and cannot be read again.
     ///
     /// ### Arguments
     ///
@@ -233,7 +237,7 @@ impl Body {
     /// use serde::Deserialize;
     ///
     /// let body = Body {
-    ///     data: r#"{"name": "John", "age": 30}"#.to_string(),
+    ///     data: r#"{"name": "John", "age": 30}"#.to_string().into_bytes(),
     /// };
     ///
     /// #[derive(Deserialize)]
@@ -244,16 +248,65 @@ impl Body {
     ///
     /// let result: Result<Person, serde_json::Error> = body.json();
     /// ```
-    pub fn json<S: for<'a> Deserialize<'a>>(&self) -> Result<S, serde_json::Error> {
-        serde_json::from_str(self.text())
+    pub fn json<S: for<'a> Deserialize<'a>>(self) -> Result<S, serde_json::Error> {
+        serde_json::from_str(&self.text())
+    }
+
+    /// Reads the body data as a string.
+    /// Once read, the body data is consumed and cannot be read again.
+    ///
+    /// ### Returns
+    ///
+    /// * `String` - The body data as a string.
+    ///
+    /// ### Examples
+    ///
+    /// ```rust ignore
+    /// let body = Body {
+    ///    data: r#"{"name": "John", "age": 30}"#.to_string().into_bytes(),
+    /// };
+    ///
+    /// assert_eq!(body.text(), r#"{"name": "John", "age": 30}"#);
+    /// ```
+    pub fn text(self) -> String {
+        String::from_utf8_lossy(self.data).to_string()
     }
 
-    pub fn text(&self) -> &str {
-        &self.data
+    /// Parses the data into a `multipart/form-data` stream.
+    /// Once read, the body data is consumed and cannot be read again.
+    ///
+    /// ### Returns
+    ///
+    /// * `Multipart<'static>` - The body data as a `multipart/form-data` stream.
+    ///
+    /// ### Examples
+    ///
+    /// ```rust ignore
+    /// let body = Body {
+    ///    data: r#"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="file"; filename="example.txt"\r\nContent-Type: text/plain\r\n\r\nHello World\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n"#.to_string().into_bytes(),
+    /// };
+    ///
+    /// let stream = body.form_data();
+    /// ```
+    pub fn form_data(self) -> Result<Multipart<'b>, multer::Error> {
+        if let Some(content_type) = self.content_type {
+            let boundary = multer::parse_boundary(
+                content_type
+                    .to_str()
+                    .expect("Content Type header contains invalid ASCII value"),
+            )?;
+            let body: Full<Bytes> = Full::new(Bytes::from(self.data.to_owned()));
+            let stream = BodyStream::new(body).filter_map(|result| async move {
+                result.map(|frame| frame.into_data().ok()).transpose()
+            });
+            Ok(Multipart::new(stream, boundary))
+        } else {
+            Err(multer::Error::NoBoundary)
+        }
     }
 }
 
-impl Transformer<'_> for Body {
+impl<'a: 'b, 'b> Transformer<'a> for Body<'b> {
     /// Transforms the given `NgynContext` into a `Body` instance.
     ///
     /// ### Arguments
@@ -270,12 +323,12 @@ impl Transformer<'_> for Body {
     /// use crate::{context::NgynContext, NgynResponse};
     ///
     /// let mut cx = NgynContext::new();
-    /// let mut res = NgynResponse::new();
     ///
-    /// let dto: Body = Body::transform(&mut cx, &mut res);
+    /// let dto: Body = Body::transform(&mut cx);
     /// ```
-    fn transform(cx: &mut NgynContext) -> Self {
-        let data = String::from_utf8_lossy(cx.request().body()).to_string();
-        Body { data }
+    fn transform(cx: &'a mut NgynContext) -> Self {
+        let data = cx.request().body();
+        let content_type = cx.request().headers().get(CONTENT_TYPE);
+        Body { data, content_type }
     }
 }
diff --git a/examples/weather_app/src/main.rs b/examples/weather_app/src/main.rs
index f426bbd2..f4a70ae5 100644
--- a/examples/weather_app/src/main.rs
+++ b/examples/weather_app/src/main.rs
@@ -1,11 +1,11 @@
 mod middlewares;
-mod modules;
 mod shared;
+mod weather;
 
 use dotenv::dotenv;
-use modules::{get_location, post_location};
 use ngyn::prelude::*;
 use ngyn_shuttle::{ShuttleApplication, ShuttleNgyn};
+use weather::{get_location, post_location};
 
 use crate::middlewares::notfound_middleware::NotFoundMiddleware;
 
diff --git a/examples/weather_app/src/modules/weather/mod.rs b/examples/weather_app/src/modules/weather/mod.rs
deleted file mode 100644
index 166d5490..00000000
--- a/examples/weather_app/src/modules/weather/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod weather_controller;
-pub mod weather_gate;
-pub mod weather_module;
-pub mod weather_repository;
-pub mod weather_service;
diff --git a/examples/weather_app/src/modules/weather/weather_repository.rs b/examples/weather_app/src/modules/weather/weather_repository.rs
deleted file mode 100644
index 75eaea72..00000000
--- a/examples/weather_app/src/modules/weather/weather_repository.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-use ngyn::prelude::*;
-use std::env;
-
-
diff --git a/examples/weather_app/src/modules/weather/weather_service.rs b/examples/weather_app/src/modules/weather/weather_service.rs
deleted file mode 100644
index e69de29b..00000000
diff --git a/examples/weather_app/src/shared/mod.rs b/examples/weather_app/src/shared.rs
similarity index 100%
rename from examples/weather_app/src/shared/mod.rs
rename to examples/weather_app/src/shared.rs
diff --git a/examples/weather_app/src/modules/mod.rs b/examples/weather_app/src/weather.rs
similarity index 96%
rename from examples/weather_app/src/modules/mod.rs
rename to examples/weather_app/src/weather.rs
index 38637d0e..0d01e914 100644
--- a/examples/weather_app/src/modules/mod.rs
+++ b/examples/weather_app/src/weather.rs
@@ -25,7 +25,7 @@ pub struct WeatherRepository;
 
 impl WeatherRepository {
     fn build_url(&self, api_type: &str, location: &str) -> String {
-        let api_key = std::env::var("WEATHER_API_KEY").unwrap();
+        let api_key = std::env::var("WEATHER_API_KEY").expect("WEATHER_API_KEY must be set");
         format!(
             "https://api.weatherapi.com/v1/{}.json?key={}&q={}",
             api_type, api_key, location


Test patch: