From 450cad1faecf72e00d57d309e8fb0f729a8129f2 Mon Sep 17 00:00:00 2001 From: Antoine Romero-Romero Date: Sun, 19 Mar 2023 13:35:44 +0000 Subject: [PATCH] fix: introduce PyRequest and PyResponse struct --- Cargo.lock | 32 +-- Cargo.toml | 2 +- integration_tests/base_routes.py | 50 ++-- integration_tests/test_middlewares.py | 2 + integration_tests/views/async_view.py | 7 +- integration_tests/views/sync_view.py | 7 +- robyn/robyn.pyi | 23 +- robyn/router.py | 2 +- src/executors/mod.rs | 40 ++-- src/lib.rs | 8 +- src/routers/const_router.rs | 5 +- src/routers/http_router.rs | 5 +- src/routers/middleware_router.rs | 6 +- src/routers/mod.rs | 2 +- src/routers/web_socket_router.rs | 6 +- src/server.rs | 5 +- src/types.rs | 325 -------------------------- src/types/function_info.rs | 24 ++ src/types/mod.rs | 51 ++++ src/types/request.rs | 104 +++++++++ src/types/response.rs | 130 +++++++++++ src/web_socket_connection.rs | 2 +- 22 files changed, 411 insertions(+), 427 deletions(-) delete mode 100644 src/types.rs create mode 100644 src/types/function_info.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/request.rs create mode 100644 src/types/response.rs diff --git a/Cargo.lock b/Cargo.lock index 6365455ec..47d3be731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,11 +991,11 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.38" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1096,9 +1096,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -1219,9 +1219,9 @@ checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" [[package]] name = "serde" -version = "1.0.155" +version = "1.0.157" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" +checksum = "707de5fcf5df2b5788fca98dd7eab490bc2fd9b7ef1404defc462833b83f25ca" [[package]] name = "serde_json" @@ -1300,13 +1300,13 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.92" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1461,6 +1461,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -1470,12 +1476,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-xid" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" - [[package]] name = "unindent" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index ba23b681a..0d502b4cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ futures-util = "0.3.27" matchit = "0.7.0" socket2 = { version = "0.5.1", features = ["all"] } uuid = { version = "1.3.0", features = ["serde", "v4"] } -serde = "1.0.155" +serde = "1.0.157" serde_json = "1.0.94" log = "0.4.17" diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 375d603f7..78732edf9 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -57,17 +57,13 @@ def shutdown_handler(): @app.before_request("/sync/middlewares") def sync_before_request(request: Request): - new_headers = request.headers - new_headers["before"] = "sync_before_request" - request.headers = new_headers + request.headers["before"] = "sync_before_request" return request @app.after_request("/sync/middlewares") def sync_after_request(response: Response): - new_headers = response.headers - new_headers["after"] = "sync_after_request" - response.headers = new_headers + response.headers["after"] = "sync_after_request" response.body = response.body + " after" return response @@ -81,17 +77,13 @@ def sync_middlewares(request: Request): @app.before_request("/async/middlewares") async def async_before_request(request: Request): - new_headers = request.headers - new_headers["before"] = "async_before_request" - request.headers = new_headers + request.headers["before"] = "async_before_request" return request @app.after_request("/async/middlewares") async def async_after_request(response: Response): - new_headers = response.headers - new_headers["after"] = "async_after_request" - response.headers = new_headers + response.headers["after"] = "async_after_request" response.body = response.body + " after" return response @@ -274,14 +266,14 @@ async def async_param(request: Request): @app.get("/sync/extra/*extra") -def sync_param_extra(request): - extra = request["params"]["extra"] +def sync_param_extra(request: Request): + extra = request.params["extra"] return extra @app.get("/async/extra/*extra") -async def async_param_extra(request): - extra = request["params"]["extra"] +async def async_param_extra(request: Request): + extra = request.params["extra"] return extra @@ -289,13 +281,31 @@ async def async_param_extra(request): @app.get("/sync/http/param") -def sync_http_param(request): - return jsonify({"url": request["url"], "method": request["method"]}) +def sync_http_param(request: Request): + return jsonify( + { + "url": { + "scheme": request.url.scheme, + "host": request.url.host, + "path": request.url.path, + }, + "method": request.method, + } + ) @app.get("/async/http/param") -async def async_http_param(request): - return jsonify({"url": request["url"], "method": request["method"]}) +async def async_http_param(request: Request): + return jsonify( + { + "url": { + "scheme": request.url.scheme, + "host": request.url.host, + "path": request.url.path, + }, + "method": request.method, + } + ) # HTML serving diff --git a/integration_tests/test_middlewares.py b/integration_tests/test_middlewares.py index 24a236537..1bd2957e4 100644 --- a/integration_tests/test_middlewares.py +++ b/integration_tests/test_middlewares.py @@ -7,6 +7,8 @@ @pytest.mark.parametrize("function_type", ["sync", "async"]) def test_middlewares(function_type: str, session): r = get(f"/{function_type}/middlewares") + # We do not want the request headers to be in the response + assert "before" not in r.headers assert "after" in r.headers assert r.headers["after"] == f"{function_type}_after_request" assert r.text == f"{function_type} middlewares after" diff --git a/integration_tests/views/async_view.py b/integration_tests/views/async_view.py index b3e068c2f..02d4cf7ff 100644 --- a/integration_tests/views/async_view.py +++ b/integration_tests/views/async_view.py @@ -6,9 +6,4 @@ async def get(): return "Hello, world!" async def post(request: Request): - body = request.body - return { - "status": 200, - "body": body, - "headers": {"Content-Type": "text/json"}, - } + return request.body diff --git a/integration_tests/views/sync_view.py b/integration_tests/views/sync_view.py index e6bfdbce1..3b78f82a1 100644 --- a/integration_tests/views/sync_view.py +++ b/integration_tests/views/sync_view.py @@ -6,9 +6,4 @@ def get(): return "Hello, world!" def post(request: Request): - body = request.body - return { - "status": 200, - "body": body, - "headers": {"Content-Type": "text/json"}, - } + return request.body diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index 770e9e920..1004a0839 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -15,30 +15,27 @@ class FunctionInfo: is_async: bool number_of_params: int -@dataclass -class Body: - content: Union[str, bytes] - - def as_bytes(self) -> bytes: - pass - def set(self, content: Union[str, bytes]): - pass +class Url: + scheme: str + host: str + path: str @dataclass class Request: queries: dict[str, str] headers: dict[str, str] params: dict[str, str] - body: Body + body: Union[str, bytes] + method: str + url: Url @dataclass class Response: status_code: int + response_type: str headers: dict[str, str] - body: Body - - def set_file_path(self, file_path: str): - pass + body: Union[str, bytes] + file_path: str class Server: def __init__(self) -> None: diff --git a/robyn/router.py b/robyn/router.py index 8ca37b813..e415f7c9b 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -37,7 +37,7 @@ def _format_response(self, res): response = Response(status_code=status_code, headers=headers, body=body) file_path = res.get("file_path") if file_path is not None: - response.set_file_path(file_path) + response.file_path = file_path elif isinstance(res, Response): response = res elif isinstance(res, bytes): diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 67ec4da8c..3725c6efe 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1,14 +1,13 @@ /// This is the module that has all the executor functions /// i.e. the functions that have the responsibility of parsing and executing functions. -use crate::types::{FunctionInfo, Request, Response}; - use std::sync::Arc; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use log::debug; +use pyo3::prelude::*; use pyo3_asyncio::TaskLocals; -// pyO3 module -use pyo3::{prelude::*, PyClass}; + +use crate::types::{function_info::FunctionInfo, request::Request, response::Response}; fn get_function_output<'a, T>( function: &'a FunctionInfo, @@ -16,40 +15,42 @@ fn get_function_output<'a, T>( input: &T, ) -> Result<&'a PyAny, PyErr> where - T: Clone + IntoPy>, + T: ToPyObject, { let handler = function.handler.as_ref(py); // this makes the request object accessible across every route match function.number_of_params { 0 => handler.call0(), - 1 => handler.call1((input.clone(),)), + 1 => handler.call1((input.to_object(py),)), // this is done to accommodate any future params - 2_u8..=u8::MAX => handler.call1((input.clone(),)), + 2_u8..=u8::MAX => handler.call1((input.to_object(py),)), } } -pub async fn execute_middleware_function<'a, T>(input: &T, function: FunctionInfo) -> Result +pub async fn execute_middleware_function(input: &T, function: FunctionInfo) -> Result where - T: Clone + PyClass + IntoPy>, + T: for<'a> FromPyObject<'a> + ToPyObject, { if function.is_async { - let output = Python::with_gil(|py| { + let output: Py = Python::with_gil(|py| { pyo3_asyncio::tokio::into_future(get_function_output(&function, py, input)?) })? .await?; - Python::with_gil(|py| -> PyResult { - let output: (T,) = output.extract(py)?; + Python::with_gil(|py| -> Result { + let output: (T,) = output + .extract(py) + .context("Failed to get middleware response")?; Ok(output.0) }) - .map_err(|e| anyhow!(e)) } else { - Python::with_gil(|py| -> PyResult { - let output: (T,) = get_function_output(&function, py, input)?.extract()?; + Python::with_gil(|py| -> Result { + let output: (T,) = get_function_output(&function, py, input)? + .extract() + .context("Failed to get middleware response")?; Ok(output.0) }) - .map_err(|e| anyhow!(e)) } } @@ -67,8 +68,9 @@ pub async fn execute_http_function(request: &Request, function: FunctionInfo) -> }) } else { Python::with_gil(|py| -> Result { - let output = get_function_output(&function, py, request)?; - output.extract().context("Failed to get route response") + get_function_output(&function, py, request)? + .extract() + .context("Failed to get route response") }) } } diff --git a/src/lib.rs b/src/lib.rs index 582937e05..1ca7e0fcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ use shared_socket::SocketHeld; // pyO3 module use pyo3::prelude::*; -use types::{ActixBytesWrapper, FunctionInfo, Request, Response}; +use types::{function_info::FunctionInfo, request::PyRequest, response::PyResponse, Url}; #[pymodule] pub fn robyn(_py: Python<'_>, m: &PyModule) -> PyResult<()> { @@ -19,9 +19,9 @@ pub fn robyn(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; pyo3::prepare_freethreaded_python(); Ok(()) } diff --git a/src/routers/const_router.rs b/src/routers/const_router.rs index f86a129d3..e118d9cb1 100644 --- a/src/routers/const_router.rs +++ b/src/routers/const_router.rs @@ -4,8 +4,9 @@ use std::sync::Arc; use std::sync::RwLock; use crate::executors::execute_http_function; -use crate::types::Response; -use crate::types::{FunctionInfo, Request}; +use crate::types::function_info::FunctionInfo; +use crate::types::request::Request; +use crate::types::response::Response; use anyhow::Context; use log::debug; use matchit::Router as MatchItRouter; diff --git a/src/routers/http_router.rs b/src/routers/http_router.rs index f4c400f90..673f22bc1 100644 --- a/src/routers/http_router.rs +++ b/src/routers/http_router.rs @@ -1,15 +1,14 @@ use std::sync::RwLock; use std::{collections::HashMap, str::FromStr}; -// pyo3 modules -use crate::types::FunctionInfo; -use pyo3::types::PyAny; use actix_web::http::Method; use matchit::Router as MatchItRouter; +use pyo3::types::PyAny; use anyhow::{Context, Result}; use super::Router; +use crate::types::function_info::FunctionInfo; type RouteMap = RwLock>; diff --git a/src/routers/middleware_router.rs b/src/routers/middleware_router.rs index dba4243fd..d687071dd 100644 --- a/src/routers/middleware_router.rs +++ b/src/routers/middleware_router.rs @@ -1,14 +1,12 @@ use std::collections::HashMap; use std::sync::RwLock; -// pyo3 modules -use crate::types::FunctionInfo; + use anyhow::{Context, Error, Result}; use matchit::Router as MatchItRouter; use pyo3::types::PyAny; -use crate::routers::types::MiddlewareRoute; - use super::Router; +use crate::{routers::types::MiddlewareRoute, types::function_info::FunctionInfo}; type RouteMap = RwLock>; diff --git a/src/routers/mod.rs b/src/routers/mod.rs index f31d4bd3b..903acff2c 100644 --- a/src/routers/mod.rs +++ b/src/routers/mod.rs @@ -1,7 +1,7 @@ use anyhow::Result; use pyo3::PyAny; -use crate::types::FunctionInfo; +use crate::types::function_info::FunctionInfo; pub mod const_router; pub mod http_router; diff --git a/src/routers/web_socket_router.rs b/src/routers/web_socket_router.rs index 9f170ed47..4e7e231b1 100644 --- a/src/routers/web_socket_router.rs +++ b/src/routers/web_socket_router.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use std::sync::RwLock; -// pyo3 modules -use crate::types::FunctionInfo; + use log::debug; -/// Contains the thread safe hashmaps of different routes +use crate::types::function_info::FunctionInfo; +/// Contains the thread safe hashmaps of different routes type WebSocketRoutes = RwLock>>; pub struct WebSocketRouter { diff --git a/src/server.rs b/src/server.rs index a6010a155..c799f7901 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,4 @@ use crate::executors::{execute_event_handler, execute_http_function, execute_middleware_function}; -use crate::types::{Request, Response}; use crate::routers::const_router::ConstRouter; use crate::routers::Router; @@ -8,7 +7,9 @@ use crate::routers::http_router::HttpRouter; use crate::routers::types::MiddlewareRoute; use crate::routers::{middleware_router::MiddlewareRouter, web_socket_router::WebSocketRouter}; use crate::shared_socket::SocketHeld; -use crate::types::FunctionInfo; +use crate::types::function_info::FunctionInfo; +use crate::types::request::Request; +use crate::types::response::Response; use crate::web_socket_connection::start_web_socket; use std::convert::TryInto; diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index e25e1ffdf..000000000 --- a/src/types.rs +++ /dev/null @@ -1,325 +0,0 @@ -use core::mem; -use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::{ - convert::Infallible, - pin::Pin, - task::{Context, Poll}, -}; - -use actix_http::body::MessageBody; -use actix_http::body::{BodySize, BoxBody}; -use actix_http::StatusCode; -use actix_web::web::Bytes; -use actix_web::{http::Method, HttpRequest}; -use actix_web::{HttpResponse, HttpResponseBuilder, Responder}; -use anyhow::Result; -use dashmap::DashMap; -use pyo3::exceptions::PyValueError; -use pyo3::types::{PyBytes, PyDict, PyString}; -use pyo3::{exceptions, intern, prelude::*}; - -use crate::io_helpers::{apply_hashmap_headers, read_file}; - -fn type_of(_: &T) -> String { - std::any::type_name::().to_string() -} - -#[derive(Debug, Clone, Default)] -#[pyclass(name = "Body")] -pub struct ActixBytesWrapper { - content: Bytes, -} - -#[pymethods] -impl ActixBytesWrapper { - pub fn as_bytes(&self) -> PyResult> { - Ok(self.content.to_vec()) - } - - pub fn set(&mut self, content: &PyAny) -> PyResult<()> { - let value = if let Ok(v) = content.downcast::() { - v.to_string().into_bytes() - } else if let Ok(v) = content.downcast::() { - v.as_bytes().to_vec() - } else { - return Err(PyValueError::new_err(format!( - "Could not convert body of type {} to bytes", - type_of(content) - ))); - }; - self.content = Bytes::from(value); - Ok(()) - } -} - -// provides an interface between pyo3::types::{PyString, PyBytes} and actix_web::web::Bytes -impl ActixBytesWrapper { - pub fn new(raw_body: &PyAny) -> PyResult { - let value = if let Ok(v) = raw_body.downcast::() { - v.to_string().into_bytes() - } else if let Ok(v) = raw_body.downcast::() { - v.as_bytes().to_vec() - } else { - return Err(PyValueError::new_err(format!( - "Could not convert {} specified body to bytes", - type_of(raw_body) - ))); - }; - Ok(Self { - content: Bytes::from(value), - }) - } - - pub fn from_str(raw_body: &str) -> Self { - Self { - content: Bytes::from(raw_body.to_string()), - } - } -} - -impl Deref for ActixBytesWrapper { - type Target = Bytes; - - fn deref(&self) -> &Self::Target { - &self.content - } -} - -impl DerefMut for ActixBytesWrapper { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.content - } -} - -impl MessageBody for ActixBytesWrapper { - type Error = Infallible; - - #[inline] - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - #[inline] - fn poll_next( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut())))) - } - } - - #[inline] - fn try_into_bytes(self) -> Result { - Ok(self.content) - } -} - -#[pyclass] -#[derive(Debug, Clone)] -pub struct FunctionInfo { - #[pyo3(get, set)] - pub handler: Py, - #[pyo3(get, set)] - pub is_async: bool, - #[pyo3(get, set)] - pub number_of_params: u8, -} - -#[pymethods] -impl FunctionInfo { - #[new] - pub fn new(handler: Py, is_async: bool, number_of_params: u8) -> Self { - Self { - handler, - is_async, - number_of_params, - } - } -} - -#[derive(Default, Clone)] -pub struct Url { - pub scheme: String, - pub host: String, - pub path: String, -} - -impl Url { - fn new(scheme: &str, host: &str, path: &str) -> Self { - Self { - scheme: scheme.to_string(), - host: host.to_string(), - path: path.to_string(), - } - } - - pub fn to_object(&self, py: Python<'_>) -> PyResult { - let dict = PyDict::new(py); - dict.set_item(intern!(py, "scheme"), self.scheme.as_str())?; - dict.set_item(intern!(py, "host"), self.host.as_str())?; - dict.set_item(intern!(py, "path"), self.path.as_str())?; - Ok(dict.into_py(py)) - } -} - -#[pyclass] -#[derive(Default, Clone)] -pub struct Request { - #[pyo3(get, set)] - pub queries: HashMap, - #[pyo3(get, set)] - pub headers: HashMap, - pub method: Method, - #[pyo3(get, set)] - pub params: HashMap, - pub body: Bytes, - pub url: Url, -} - -#[pymethods] -impl Request { - #[getter] - pub fn get_body(&self) -> PyResult { - Ok(String::from_utf8(self.body.to_vec())?) - } - - #[setter] - pub fn set_body(&mut self, content: &PyAny) -> PyResult<()> { - self.body.set(content) - } - - pub fn get_body_as_bytes(&self) -> PyResult> { - self.body.as_bytes() - } -} - -impl Request { - pub fn from_actix_request( - req: &HttpRequest, - body: Bytes, - global_headers: &DashMap, - ) -> Self { - let mut queries = HashMap::new(); - if !req.query_string().is_empty() { - let split = req.query_string().split('&'); - for s in split { - let params = s.split_once('=').unwrap_or((s, "")); - queries.insert(params.0.to_string(), params.1.to_string()); - } - } - let headers = req - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string())) - .chain( - global_headers - .iter() - .map(|h| (h.key().clone(), h.value().clone())), - ) - .collect(); - - Self { - queries, - headers, - method: req.method().clone(), - params: HashMap::new(), - body, - url: Url::new( - req.connection_info().scheme(), - req.connection_info().host(), - req.path(), - ), - } - } -} - -#[pyclass] -#[derive(Debug, Clone)] -pub struct Response { - pub status_code: u16, - pub response_type: String, - #[pyo3(get, set)] - pub headers: HashMap, - pub body: ActixBytesWrapper, - pub file_path: Option, -} - -impl Responder for Response { - type Body = BoxBody; - - fn respond_to(self, _req: &HttpRequest) -> HttpResponse { - let mut response_builder = - HttpResponseBuilder::new(StatusCode::from_u16(self.status_code).unwrap()); - apply_hashmap_headers(&mut response_builder, &self.headers); - response_builder.body(self.body) - } -} - -impl Response { - pub fn not_found(headers: &HashMap) -> Self { - Self { - status_code: 404, - response_type: "text".to_string(), - headers: headers.clone(), - body: ActixBytesWrapper::from_str("Not found"), - file_path: None, - } - } - - pub fn internal_server_error(headers: &HashMap) -> Self { - Self { - status_code: 500, - response_type: "text".to_string(), - headers: headers.clone(), - body: ActixBytesWrapper::from_str("Internal server error"), - file_path: None, - } - } -} - -#[pymethods] -impl Response { - // To do: Add check for content-type in header and change response_type accordingly - #[new] - pub fn new(status_code: u16, headers: HashMap, body: &PyAny) -> PyResult { - Ok(Self { - status_code, - // we should be handling based on headers but works for now - response_type: "text".to_string(), - headers, - body: ActixBytesWrapper::new(body)?, - file_path: None, - }) - } - - #[getter] - pub fn get_body(&self) -> PyResult { - Ok(String::from_utf8(self.body.to_vec())?) - } - - #[setter] - pub fn set_body(&mut self, content: &PyAny) -> PyResult<()> { - self.body.set(content) - } - - pub fn get_body_as_bytes(&self) -> PyResult> { - self.body.as_bytes() - } - - pub fn set_file_path(&mut self, file_path: &str) -> PyResult<()> { - // we should be handling based on headers but works for now - self.response_type = "static_file".to_string(); - self.file_path = Some(file_path.to_string()); - self.body = ActixBytesWrapper { - content: Bytes::from( - read_file(file_path) - .map_err(|e| PyErr::new::(e.to_string()))?, - ), - }; - Ok(()) - } -} diff --git a/src/types/function_info.rs b/src/types/function_info.rs new file mode 100644 index 000000000..948c74adf --- /dev/null +++ b/src/types/function_info.rs @@ -0,0 +1,24 @@ +use pyo3::prelude::*; + +#[pyclass] +#[derive(Debug, Clone)] +pub struct FunctionInfo { + #[pyo3(get, set)] + pub handler: Py, + #[pyo3(get, set)] + pub is_async: bool, + #[pyo3(get, set)] + pub number_of_params: u8, +} + +#[pymethods] +impl FunctionInfo { + #[new] + pub fn new(handler: Py, is_async: bool, number_of_params: u8) -> Self { + Self { + handler, + is_async, + number_of_params, + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 000000000..4ba9039e1 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,51 @@ +use pyo3::{ + exceptions::PyValueError, + prelude::*, + types::{PyBytes, PyString}, +}; + +pub mod function_info; +pub mod request; +pub mod response; + +#[pyclass] +#[derive(Default, Clone)] +pub struct Url { + #[pyo3(get)] + pub scheme: String, + #[pyo3(get)] + pub host: String, + #[pyo3(get)] + pub path: String, +} + +impl Url { + fn new(scheme: &str, host: &str, path: &str) -> Self { + Self { + scheme: scheme.to_string(), + host: host.to_string(), + path: path.to_string(), + } + } +} + +pub fn get_body_from_pyobject(body: &PyAny) -> PyResult> { + if let Ok(s) = body.downcast::() { + Ok(s.to_string().into_bytes()) + } else if let Ok(b) = body.downcast::() { + Ok(b.as_bytes().to_vec()) + } else { + Err(PyValueError::new_err( + "Could not convert specified body to bytes", + )) + } +} + +pub fn check_body_type(py: Python, body: Py) -> PyResult<()> { + if body.downcast::(py).is_err() && body.downcast::(py).is_err() { + return Err(PyValueError::new_err( + "Could not convert specified body to bytes", + )); + }; + Ok(()) +} diff --git a/src/types/request.rs b/src/types/request.rs new file mode 100644 index 000000000..990d97acd --- /dev/null +++ b/src/types/request.rs @@ -0,0 +1,104 @@ +use actix_web::{web::Bytes, HttpRequest}; +use dashmap::DashMap; +use pyo3::{prelude::*, types::PyDict}; +use std::collections::HashMap; + +use super::{check_body_type, get_body_from_pyobject, Url}; + +#[derive(Default, Clone, FromPyObject)] +pub struct Request { + pub queries: HashMap, + pub headers: HashMap, + pub method: String, + pub params: HashMap, + #[pyo3(from_py_with = "get_body_from_pyobject")] + pub body: Vec, + pub url: Url, +} + +impl ToPyObject for Request { + fn to_object(&self, py: Python) -> PyObject { + let queries = self.queries.clone().into_py(py).extract(py).unwrap(); + let headers = self.headers.clone().into_py(py).extract(py).unwrap(); + let params = self.params.clone().into_py(py).extract(py).unwrap(); + let body = String::from_utf8(self.body.to_vec()).unwrap().to_object(py); + let request = PyRequest { + queries, + params, + headers, + body, + method: self.method.clone(), + url: self.url.clone(), + }; + Py::new(py, request).unwrap().as_ref(py).into() + } +} + +impl Request { + pub fn from_actix_request( + req: &HttpRequest, + body: Bytes, + global_headers: &DashMap, + ) -> Self { + let mut queries = HashMap::new(); + if !req.query_string().is_empty() { + let split = req.query_string().split('&'); + for s in split { + let params = s.split_once('=').unwrap_or((s, "")); + queries.insert(params.0.to_string(), params.1.to_string()); + } + } + let headers = req + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string())) + .chain( + global_headers + .iter() + .map(|h| (h.key().clone(), h.value().clone())), + ) + .collect(); + + let url = Url::new( + req.connection_info().scheme(), + req.connection_info().host(), + req.path(), + ); + + Self { + queries, + headers, + method: req.method().as_str().to_owned(), + params: HashMap::new(), + body: body.to_vec(), + url, + } + } +} + +#[pyclass(name = "Request")] +#[derive(Clone)] +pub struct PyRequest { + #[pyo3(get, set)] + pub queries: Py, + #[pyo3(get, set)] + pub headers: Py, + #[pyo3(get, set)] + pub params: Py, + #[pyo3(get)] + pub body: Py, + #[pyo3(get)] + pub method: String, + #[pyo3(get)] + pub url: Url, +} + +#[pymethods] +impl PyRequest { + #[setter] + pub fn set_body(&mut self, py: Python, body: Py) -> PyResult<()> { + check_body_type(py, body.clone())?; + self.body = body; + Ok(()) + } +} diff --git a/src/types/response.rs b/src/types/response.rs new file mode 100644 index 000000000..5e63aac25 --- /dev/null +++ b/src/types/response.rs @@ -0,0 +1,130 @@ +use std::collections::HashMap; + +use actix_http::{body::BoxBody, StatusCode}; +use actix_web::{HttpRequest, HttpResponse, HttpResponseBuilder, Responder}; +use pyo3::{ + exceptions::{PyIOError, PyValueError}, + prelude::*, + types::{PyBytes, PyDict, PyString}, +}; + +use crate::io_helpers::{apply_hashmap_headers, read_file}; + +use super::{check_body_type, get_body_from_pyobject}; + +#[derive(Debug, Clone, FromPyObject)] +pub struct Response { + pub status_code: u16, + pub response_type: String, + pub headers: HashMap, + #[pyo3(from_py_with = "get_body_from_pyobject")] + pub body: Vec, + pub file_path: Option, +} + +impl Responder for Response { + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let mut response_builder = + HttpResponseBuilder::new(StatusCode::from_u16(self.status_code).unwrap()); + apply_hashmap_headers(&mut response_builder, &self.headers); + response_builder.body(self.body) + } +} + +impl Response { + pub fn not_found(headers: &HashMap) -> Self { + Self { + status_code: 404, + response_type: "text".to_string(), + headers: headers.clone(), + body: "Not found".to_owned().into_bytes(), + file_path: None, + } + } + + pub fn internal_server_error(headers: &HashMap) -> Self { + Self { + status_code: 500, + response_type: "text".to_string(), + headers: headers.clone(), + body: "Internal server error".to_owned().into_bytes(), + file_path: None, + } + } +} + +impl ToPyObject for Response { + fn to_object(&self, py: Python) -> PyObject { + let headers = self.headers.clone().into_py(py).extract(py).unwrap(); + let body = String::from_utf8(self.body.to_vec()).unwrap().to_object(py); + let response = PyResponse { + status_code: self.status_code, + response_type: self.response_type.clone(), + headers, + body, + file_path: self.file_path.clone(), + }; + Py::new(py, response).unwrap().as_ref(py).into() + } +} + +#[pyclass(name = "Response")] +#[derive(Debug, Clone)] +pub struct PyResponse { + #[pyo3(get)] + pub status_code: u16, + #[pyo3(get)] + pub response_type: String, + #[pyo3(get, set)] + pub headers: Py, + #[pyo3(get)] + pub body: Py, + #[pyo3(get)] + pub file_path: Option, +} + +#[pymethods] +impl PyResponse { + // To do: Add check for content-type in header and change response_type accordingly + #[new] + pub fn new( + py: Python, + status_code: u16, + headers: Py, + body: Py, + ) -> PyResult { + if body.downcast::(py).is_err() && body.downcast::(py).is_err() { + return Err(PyValueError::new_err( + "Could not convert specified body to bytes", + )); + }; + Ok(Self { + status_code, + // we should be handling based on headers but works for now + response_type: "text".to_string(), + headers, + body, + file_path: None, + }) + } + + #[setter] + pub fn set_body(&mut self, py: Python, body: Py) -> PyResult<()> { + check_body_type(py, body.clone())?; + self.body = body; + Ok(()) + } + + #[setter] + pub fn set_file_path(&mut self, py: Python, file_path: &str) -> PyResult<()> { + // we should be handling based on headers but works for now + self.response_type = "static_file".to_string(); + self.file_path = Some(file_path.to_string()); + self.body = read_file(file_path) + .map_err(|e| PyErr::new::(e.to_string()))? + .into_py(py); + Ok(()) + } +} diff --git a/src/web_socket_connection.rs b/src/web_socket_connection.rs index 0c7ec7fde..25be49524 100644 --- a/src/web_socket_connection.rs +++ b/src/web_socket_connection.rs @@ -1,4 +1,4 @@ -use crate::types::FunctionInfo; +use crate::types::function_info::FunctionInfo; use actix::prelude::*; use actix::{Actor, AsyncContext, StreamHandler};