Skip to content

feat: initial work on formfields #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub mod prelude {
handler::*,
},
server::{
Body, JsonResponse, JsonResult, NgynContext, NgynRequest, NgynResponse, Param, Query,
ToBytes, Transducer,
Body, FormFields, JsonResponse, JsonResult, NgynContext, NgynRequest, NgynResponse,
Param, Query, ToBytes, Transducer,
},
NgynGate, NgynMiddleware,
};
Expand Down
2 changes: 1 addition & 1 deletion crates/macros/src/common/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub fn handler_macro(args: TokenStream, raw_input: TokenStream) -> TokenStream {
});

quote! {
#vis #constness #unsafety #fn_token #ident <#generics_stream>(cx: &'_cx_lifetime mut ngyn::prelude::NgynContext) #output {
#vis #constness #unsafety #fn_token #ident <#generics_stream>(cx: &'_cx_lifetime mut ngyn::prelude::NgynContext<'_>) #output {
#body
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/shared/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub use bytes::Bytes;
pub use context::NgynContext;
pub use http::Method;
use http_body_util::Full;
pub use transformer::{Body, Param, Query, Transducer, Transformer};
pub use transformer::{Body, FormFields, FromField, Param, Query, Transducer, Transformer};

pub type NgynRequest = http::Request<Vec<u8>>;
pub type NgynResponse = http::Response<Full<Bytes>>;
158 changes: 156 additions & 2 deletions crates/shared/src/server/transformer.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use std::{borrow::Cow, str::FromStr};
use std::{
borrow::Cow,
collections::HashMap,
future::Future,
pin::Pin,
str::{FromStr, ParseBoolError},
};

use bytes::Bytes;
use futures_util::StreamExt;
use http::{header::CONTENT_TYPE, HeaderValue};
use http_body_util::{BodyStream, Full};
use multer::Multipart;
use multer::{Field, Multipart};
use serde::Deserialize;

use crate::server::NgynContext;
Expand Down Expand Up @@ -305,6 +311,154 @@ impl<'b> Body<'b> {
Err(multer::Error::NoBoundary)
}
}

/// Parses the data into a `multipart/form-data` stream and returns a HashMap of form fields.
///
/// ### Returns
///
/// * `HashMap<String, Field<'b>>` - The form fields as a HashMap.
pub async fn form_fields(self) -> Option<HashMap<String, Field<'b>>> {
if let Ok(mut stream) = self.form_data() {
let mut fields = HashMap::new();
while let Ok(Some(field)) = stream.next_field().await {
if let Some(name) = field.name() {
// Clone the name to store it as the key in the HashMap
fields.insert(name.to_string(), field);
}
}
return Some(fields);
}
None
}
}

pub struct FormFields<'b>(
Pin<Box<dyn Future<Output = Option<HashMap<String, Field<'b>>>> + Send + 'b>>,
);

impl<'b> Transformer<'b> for FormFields<'b> {
fn transform(cx: &'b mut NgynContext) -> Self {
let body = Body::transform(cx);
let fields = body.form_fields();
FormFields::<'b>(Box::pin(fields))
}
}

impl<'b> Future for FormFields<'b> {
type Output = Option<HashMap<String, Field<'b>>>;

fn poll(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
self.get_mut().0.as_mut().poll(cx)
}
}

pub trait FromField {
#[allow(async_fn_in_trait)]
async fn from_field(field: Field<'_>) -> Result<Self, multer::Error>
where
Self: Sized;
}

pub trait ParseField {
#[allow(async_fn_in_trait)]
async fn parse<T>(self) -> Result<T, multer::Error>
where
T: FromField,
Self: Sized;
}

impl ParseField for Field<'_> {
async fn parse<T>(self) -> Result<T, multer::Error>
where
T: FromField,
{
T::from_field(self).await
}
}

impl FromField for String {
async fn from_field(field: Field<'_>) -> Result<String, multer::Error> {
field.text().await
}
}

impl FromField for f32 {
async fn from_field(field: Field<'_>) -> Result<f32, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: std::num::ParseFloatError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for f64 {
async fn from_field(field: Field<'_>) -> Result<f64, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: std::num::ParseFloatError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for i32 {
async fn from_field(field: Field<'_>) -> Result<i32, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: std::num::ParseIntError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for i64 {
async fn from_field(field: Field<'_>) -> Result<i64, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: std::num::ParseIntError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for u32 {
async fn from_field(field: Field<'_>) -> Result<u32, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: std::num::ParseIntError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for u64 {
async fn from_field(field: Field<'_>) -> Result<u64, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: std::num::ParseIntError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for bool {
async fn from_field(field: Field<'_>) -> Result<bool, multer::Error> {
field
.text()
.await?
.parse()
.map_err(|e: ParseBoolError| multer::Error::StreamReadFailed(e.into()))
}
}

impl FromField for Bytes {
async fn from_field(field: Field<'_>) -> Result<Bytes, multer::Error> {
field.bytes().await
}
}

impl<'a: 'b, 'b> Transformer<'a> for Body<'b> {
Expand Down
18 changes: 17 additions & 1 deletion examples/weather_app/src/weather.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use ngyn::{http::StatusCode, prelude::*, shared::server::Transformer};
use ngyn::{
http::StatusCode,
prelude::*,
shared::server::{transformer::ParseField, Transformer},
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use validator::Validate;
Expand Down Expand Up @@ -96,3 +100,15 @@ pub async fn post_location(
Err(e) => Err(json!({ "status": 501, "message": e.to_string() })),
}
}

#[handler]
async fn handle_form(fields: FormFields<'_cx_lifetime>) -> JsonResult {
let mut fields = fields.await.unwrap();

if let Some(name) = fields.remove("name") {
let name: String = name.parse().await.unwrap();
Ok(json!({ "name": name }))
} else {
Err("Name field is required".into())
}
}
28 changes: 8 additions & 20 deletions sites/docs/versioned_docs/version-0.5.x/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,21 @@ Ngyn provides a simple way to handle form data in your applications. This guide

## Parsing Form Data

To parse form data in your route handlers, you can use the `Body` struct. The `Body` struct provides methods to access form data sent in the request body.
To parse form data in your route handlers, you can use the `FormFields` struct. The `FormFields` struct provides methods to access form data sent in the request body.

Here's an example of a route handler that parses form data:

```rust
use ngyn::prelude::*;

#[handler]
async fn handle_form(body: Body) -> JsonResult {
let mut name = None
let mut email = None;
let mut data = body.form_data().unwrap();

while let Ok(Some(field)) = data.next_field().await {
let key = field.name().unwrap();
let value = field.text().await.unwrap();

if key == "name" {
name = Some(value);
} else if key == "email" {
email = Some(value);
}
}
async fn handle_form(fields: FormFields<'_cx_lifetime>) -> JsonResult {
let mut fields = fields.await;

Ok(json!({
"name": name,
"email": email,
}))
if let Some(name) = fields.remove("name") {
Ok(json!({ "name": name }))
} else {
Err("Name field is required".into())
}
}
```