From c41122fc5bba8fb465ffd6d52011a78b97a90693 Mon Sep 17 00:00:00 2001 From: Sam Bonill Date: Fri, 14 Oct 2022 17:57:53 -0500 Subject: [PATCH] parse json request into Type --- Cargo.toml | 3 ++ README.md | 78 ++++++++++++++++++++++++++++- src/http/request.rs | 117 +++++++++++++++++++++++++++++++------------ src/http/resource.rs | 25 ++++----- src/lib.rs | 7 ++- 5 files changed, 178 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index be50e13..3292c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,7 @@ axum = { git= "https://github.com/tokio-rs/axum", version = "0.6.0-rc.2" } futures = "0.3.19" num_cpus = "1.13.1" async-trait = "0.1.52" +serde_path_to_error = "0.1.8" serde_json = "1.0.79" +serde = "1.0" +serde_urlencoded = "0.7" diff --git a/README.md b/README.md index 55bd498..07a701f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ use graphul::{Json, Graphul, http::{StatusCode, resource::Resource, response::Re use async_trait::async_trait; use serde_json::json; +type ResValue = HashMap; struct Article; @@ -97,7 +98,13 @@ impl Resource for Article { } async fn post(_req: Request) -> Response { - (StatusCode::CREATED, "post handler").into_response() + async fn post(ctx: Context) -> Response { + let value: Json = match ctx.payload().await { + Ok(data) => data, + Err(err) => return err.into_response(), + }; + + (StatusCode::CREATED, value).into_response() } // you can use put, delete, head, patch and trace @@ -168,6 +175,75 @@ async fn main() { Json(json!({"message": "hello world!"})) }); + app.run("127.0.0.1:8000").await; +} +``` + +## Share state + +```rust +use graphul::{http::Methods, extract::State, Graphul}; + +#[tokio::main] +async fn main() { + #[derive(Clone)] + struct AppState { + data: String + } + + let state = AppState { data: "Hello, World 👋!".to_string() }; + let mut app = Graphul::share_state(state); + + app.get("/", |State(state): State| async { + state.data + }); // .middelware(); + + app.run("127.0.0.1:8000").await; +} +``` + +## Share state with Resource + +```rust +use async_trait::async_trait; +use graphul::{ + http::{resource::Resource, response::Response, StatusCode}, + Context, Graphul, IntoResponse, +}; +use serde_json::json; + +struct Article; + +#[derive(Clone)] +struct AppState { + data: Vec<&str>, +} + +#[async_trait] +impl Resource for Article { + + async fn get(ctx: Context) -> Response { + let article = ctx.state(); + + let posts = json!({ + "posts": article.data, + }); + (StatusCode::OK, ctx.json(posts)).into_response() + } + + // you can use post, put, delete, head, patch and trace +} + +#[tokio::main] +async fn main() { + let state = AppState { + data: vec!["Article 1", "Article 2", "Article 3"], + }; + let mut app = Graphul::share_state(state); + + app.resource("/article", Article); + + app.run("127.0.0.1:8000").await; } ``` diff --git a/src/http/request.rs b/src/http/request.rs index 3437d61..bef0bd9 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,72 +1,123 @@ use std::collections::HashMap; use async_trait::async_trait; -use axum::extract::{ - rejection::QueryRejection, - FromRef, FromRequest, -}; pub use axum::http::Request; +use axum::{ + body::{Bytes, HttpBody}, + extract::{rejection::{JsonRejection, FailedToDeserializeQueryString}, FromRef, FromRequest, Query, Path}, + BoxError, Json, http::Extensions, +}; +use hyper::{HeaderMap, Method, Uri, Version}; +use serde::de::DeserializeOwned; +use serde_json::Value; use crate::Body; type HashMapRequest = HashMap; #[derive(Debug)] -pub struct Context { - params_map: HashMapRequest, - query_map: HashMapRequest, - req: Request, - state: InnerState, +pub struct Context { + params_map: Option, + query_map: Option, + bytes: Bytes, + inner_state: InnerState, + headers: HeaderMap, + method: Method, + uri: Uri, + version: Version, } -// update context to get params and query +// update context to get params and query implementar params y query genericos que no solo soporte maps si no tambien otros structs +// Json -impl Context { - pub fn params(&self, key: &'static str) -> String { - match self.params_map.get(key) { - Some(value) => value.clone(), - None => String::new(), - } +impl Context { + async fn parse_query(&mut self) { + // get query + let query = self.uri.query().unwrap_or_default(); + match serde_urlencoded::from_str(query) { + Ok(value) => self.query_map = Some(value), + Err(_) => (), + }; } - pub fn request(&self) -> &Request { - &self.req + async fn parse_params(&mut self) { + todo!() + } + pub fn params(&self, _key: &'static str) -> String { + todo!() + } + + pub fn body(&self) -> String { + String::from_utf8(self.bytes.to_vec()).expect("") + } + pub fn bytes(&self) -> &Bytes { + &self.bytes } pub fn state(&self) -> &InnerState { - &self.state + &self.inner_state } - pub fn all_params(&self) -> &HashMapRequest { + pub fn all_params(&self) -> &Option { &self.params_map } pub fn query(&self, key: &'static str) -> String { - match self.query_map.get(key) { - Some(value) => value.clone(), - None => String::new(), - } + todo!() } - pub fn all_query(&self) -> &HashMapRequest { + pub fn all_query(&self) -> &Option { &self.query_map } + + pub async fn payload(&self) -> Result, JsonRejection> { + // forse parsing + let request = Request::builder() + .header("Content-Type", "application/json") + .body(Body::from(self.bytes.clone())); + + let request = match request { + Ok(value) => value, + Err(_) => Request::default(), + }; + + Json::from_request(request, &()).await + } + + pub fn json(&self, payload: Value) -> Json { + Json(payload) + } + + pub fn send(value: &str) -> &str { + value + } } #[async_trait] -impl FromRequest for Context +impl FromRequest for Context where OuterState: Send + Sync + 'static, - InnerState: FromRef, - B: Send + 'static + InnerState: FromRef + Send + Sync, + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, { - type Rejection = QueryRejection; + type Rejection = JsonRejection; async fn from_request( req: axum::http::Request, state: &OuterState, ) -> Result { let inner_state = InnerState::from_ref(state); + let headers = req.headers().clone(); + let method = req.method().clone(); + let uri = req.uri().clone(); + let version = req.version(); + let bytes = Bytes::from_request(req, state).await?; Ok(Context { - req: req, - state: inner_state, - params_map: HashMap::new(), - query_map: HashMap::new(), + version, + headers, + method, + uri, + bytes, + inner_state, + params_map: None, + query_map: None, }) } } diff --git a/src/http/resource.rs b/src/http/resource.rs index 4b75a31..99c5834 100644 --- a/src/http/resource.rs +++ b/src/http/resource.rs @@ -1,10 +1,8 @@ use async_trait::async_trait; -use axum::{ - response::{IntoResponse, Response}, -}; +use axum::response::{IntoResponse, Response}; use hyper::StatusCode; -use crate::{Body, Context}; +use crate::Context; /* @@ -22,40 +20,39 @@ expected to be called millions of times a second. */ #[async_trait] -pub trait Resource +pub trait Resource where S: Clone + Send + Sync + 'static, - B: Send + 'static { - async fn get(_ctx: Context) -> Response { + async fn get(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn post(_ctx: Context) -> Response { + async fn post(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn put(_ctx: Context) -> Response { + async fn put(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn delete(_ctx: Context) -> Response { + async fn delete(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn patch(_ctx: Context) -> Response { + async fn patch(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn options(_ctx: Context) -> Response { + async fn options(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn trace(_ctx: Context) -> Response { + async fn trace(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } - async fn head(_ctx: Context) -> Response { + async fn head(_ctx: Context) -> Response { (StatusCode::NOT_IMPLEMENTED, "Method Not Allowed").into_response() } } diff --git a/src/lib.rs b/src/lib.rs index 69cd433..3e5a745 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,6 @@ use axum::Router; pub use http::request::Context; use http::resource::Resource; - pub type Body = axum::body::Body; pub struct Group<'a, S = ()> { @@ -30,7 +29,7 @@ impl<'a, S> Group<'a, S> where S: Clone + Send + Sync + 'static, { - pub fn resource(&mut self, path: &str, res: impl Resource + 'static) { + pub fn resource(&mut self, path: &str, res: impl Resource + 'static) { let route_name = self.get_route_name(path); self.app.resource(route_name.as_str(), res); } @@ -143,7 +142,7 @@ impl Graphul where S: Clone + Send + Sync + 'static, { - pub fn resource + 'static>(&mut self, path: &str, _res: T) { + pub fn resource + 'static>(&mut self, path: &str, _res: T) { // get self.increase_route_counter(); self.routes = self.routes.clone().route(path, get(T::get)); @@ -261,7 +260,7 @@ impl Graphul where S: Clone + Send + Sync + 'static, { - pub fn with_state(state: S) -> Self { + pub fn share_state(state: S) -> Self { Self { routes: Router::with_state(state), count_routes: 0,