Skip to content

Commit

Permalink
Merge branch 'master' into mod-private
Browse files Browse the repository at this point in the history
  • Loading branch information
m4tx authored Sep 22, 2024
2 parents bdbf505 + 7eb06ee commit 4bce021
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 151 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ futures-core = "0.3.30"
glob = "0.3.1"
http = "1.1.0"
http-body = "1.0.1"
http-body-util = "0.1.2"
indexmap = "2.5.0"
itertools = "0.13.0"
log = "0.4.22"
Expand Down
3 changes: 2 additions & 1 deletion examples/hello-world/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use flareon::request::Request;
use flareon::response::{Response, ResponseExt};
use flareon::router::Route;
use flareon::{Body, Error, FlareonApp, FlareonProject, Response, StatusCode};
use flareon::{Body, Error, FlareonApp, FlareonProject, StatusCode};

async fn return_hello(_request: Request) -> Result<Response, Error> {
Ok(Response::new_html(
Expand Down
10 changes: 7 additions & 3 deletions examples/todo-list/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use askama::Template;
use flareon::db::migrations::MigrationEngine;
use flareon::db::{model, query, Database, Model};
use flareon::forms::Form;
use flareon::request::Request;
use flareon::request::{Request, RequestExt};
use flareon::response::{Response, ResponseExt};
use flareon::router::Route;
use flareon::{reverse, Body, Error, FlareonApp, FlareonProject, Response, StatusCode};
use flareon::{reverse, Body, Error, FlareonApp, FlareonProject, StatusCode};
use tokio::sync::OnceCell;

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -64,7 +65,10 @@ async fn add_todo(mut request: Request) -> Result<Response, Error> {
}

async fn remove_todo(request: Request) -> Result<Response, Error> {
let todo_id = request.path_param("todo_id").expect("todo_id not found");
let todo_id = request
.path_params()
.get("todo_id")
.expect("todo_id not found");
let todo_id = todo_id.parse::<i32>().expect("todo_id is not a number");

{
Expand Down
1 change: 1 addition & 0 deletions flareon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ form_urlencoded.workspace = true
futures-core.workspace = true
http.workspace = true
http-body.workspace = true
http-body-util.workspace = true
indexmap.workspace = true
log.workspace = true
regex.workspace = true
Expand Down
5 changes: 4 additions & 1 deletion flareon/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ pub enum Error {
#[error("Could not retrieve request body: {source}")]
ReadRequestBody {
#[from]
source: axum::Error,
source: Box<dyn std::error::Error + Send + Sync>,
},
/// An error occurred while trying to convert a response.
#[error("Could not convert response: {source}")]
ResponseConversion { source: axum::Error },
/// The request body had an invalid `Content-Type` header.
#[error("Invalid content type; expected {expected}, found {actual}")]
InvalidContentType {
Expand Down
6 changes: 3 additions & 3 deletions flareon/src/forms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use async_trait::async_trait;
pub use flareon_macros::Form;
use thiserror::Error;

use crate::request::Request;
use crate::{Html, Render};
use crate::request::{Request, RequestExt};
use crate::{request, Html, Render};

/// Error occurred while processing a form.
#[derive(Debug, Error)]
Expand Down Expand Up @@ -121,7 +121,7 @@ pub trait Form: Sized {
let mut context = Self::Context::new();
let mut has_errors = false;

for (field_id, value) in Request::query_pairs(&form_data) {
for (field_id, value) in request::query_pairs(&form_data) {
let field_id = field_id.as_ref();

if let Err(err) = context.set_value(field_id, value) {
Expand Down
4 changes: 1 addition & 3 deletions flareon/src/headers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
pub(crate) const CONTENT_TYPE_HEADER: &str = "Content-Type";
pub(crate) const HTML_CONTENT_TYPE: &str = "text/html";
pub(crate) const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8";
pub(crate) const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
pub(crate) const LOCATION_HEADER: &str = "Location";
122 changes: 48 additions & 74 deletions flareon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod headers;
#[path = "private.rs"]
pub mod __private;
pub mod request;
pub mod response;
pub mod router;

use std::fmt::{Debug, Formatter};
Expand All @@ -36,13 +37,13 @@ use derive_builder::Builder;
use derive_more::{Deref, From};
pub use error::Error;
use futures_core::Stream;
use headers::{CONTENT_TYPE_HEADER, HTML_CONTENT_TYPE, LOCATION_HEADER};
use http_body::{Frame, SizeHint};
use indexmap::IndexMap;
use log::info;
use request::Request;
use router::{Route, Router};

use crate::response::Response;

/// A type alias for a result that can return a `flareon::Error`.
pub type Result<T> = std::result::Result<T, Error>;

Expand Down Expand Up @@ -100,48 +101,10 @@ impl FlareonAppBuilder {
}
}

type HeadersMap = IndexMap<String, String>;

#[derive(Debug)]
pub struct Response {
status: StatusCode,
headers: HeadersMap,
body: Body,
}

impl Response {
#[must_use]
pub fn new_html(status: StatusCode, body: Body) -> Self {
Self {
status,
headers: Self::html_headers(),
body,
}
}

#[must_use]
pub fn new_redirect<T: Into<String>>(location: T) -> Self {
let mut headers = HeadersMap::new();
headers.insert(LOCATION_HEADER.to_owned(), location.into());
Self {
status: StatusCode::SEE_OTHER,
headers,
body: Body::empty(),
}
}

#[must_use]
fn html_headers() -> HeadersMap {
let mut headers = HeadersMap::new();
headers.insert(CONTENT_TYPE_HEADER.to_owned(), HTML_CONTENT_TYPE.to_owned());
headers
}
}

/// A type that represents an HTTP response body.
/// A type that represents an HTTP request or response body.
///
/// This type is used to represent the body of an HTTP response. It can be
/// either a fixed body (e.g., a string or a byte array) or a streaming body
/// This type is used to represent the body of an HTTP request/response. It can
/// be either a fixed body (e.g., a string or a byte array) or a streaming body
/// (e.g., a large file or a database query result).
///
/// # Examples
Expand All @@ -160,13 +123,15 @@ pub struct Body {
enum BodyInner {
Fixed(Bytes),
Streaming(Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>),
Axum(axum::body::Body),
}

impl Debug for BodyInner {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fixed(data) => f.debug_tuple("Fixed").field(data).finish(),
Self::Streaming(_) => f.debug_tuple("Streaming").field(&"...").finish(),
Self::Axum(axum_body) => f.debug_tuple("Axum").field(axum_body).finish(),
}
}
}
Expand Down Expand Up @@ -223,6 +188,17 @@ impl Body {
pub fn streaming<T: Stream<Item = Result<Bytes>> + Send + 'static>(stream: T) -> Self {
Self::new(BodyInner::Streaming(Box::pin(stream)))
}

#[must_use]
fn axum(inner: axum::body::Body) -> Self {
Self::new(BodyInner::Axum(inner))
}
}

impl Default for Body {
fn default() -> Self {
Self::empty()
}
}

impl http_body::Body for Body {
Expand Down Expand Up @@ -250,20 +226,29 @@ impl http_body::Body for Body {
Poll::Pending => Poll::Pending,
}
}
BodyInner::Axum(ref mut axum_body) => {
Pin::new(axum_body)
.poll_frame(cx)
.map_err(|error| Error::ReadRequestBody {
source: Box::new(error),
})
}
}
}

fn is_end_stream(&self) -> bool {
match &self.inner {
BodyInner::Fixed(data) => data.is_empty(),
BodyInner::Streaming(_) => false,
BodyInner::Axum(axum_body) => axum_body.is_end_stream(),
}
}

fn size_hint(&self) -> SizeHint {
match &self.inner {
BodyInner::Fixed(data) => SizeHint::with_exact(data.len() as u64),
BodyInner::Streaming(_) => SizeHint::new(),
BodyInner::Axum(axum_body) => axum_body.size_hint(),
}
}
}
Expand Down Expand Up @@ -361,8 +346,10 @@ pub async fn run_at(mut project: FlareonProject, listener: tokio::net::TcpListen

let project = Arc::new(project);

let handler = |request: axum::extract::Request| async move {
pass_to_axum(&project, Request::new(request, project.clone()))
let handler = |axum_request: axum::extract::Request| async move {
let request = request_axum_to_flareon(&project, axum_request);

pass_to_axum(&project, request)
.await
.unwrap_or_else(handle_response_error)
};
Expand All @@ -380,22 +367,28 @@ pub async fn run_at(mut project: FlareonProject, listener: tokio::net::TcpListen
Ok(())
}

fn request_axum_to_flareon(
project: &Arc<FlareonProject>,
axum_request: axum::extract::Request,
) -> Request {
let (parts, body) = axum_request.into_parts();
let mut request = Request::from_parts(parts, Body::axum(body));
request.extensions_mut().insert(project.clone());

request
}

async fn pass_to_axum(
project: &Arc<FlareonProject>,
request: Request,
) -> Result<axum::response::Response> {
let response = project.router.handle(request).await?;

let mut builder = http::Response::builder().status(response.status);
for (key, value) in response.headers {
builder = builder.header(key, value);
}
let axum_response = builder.body(axum::body::Body::new(response.body));

match axum_response {
Ok(response) => Ok(response),
Err(error) => Err(Error::ResponseBuilder(error)),
}
let (parts, body) = response.into_parts();
Ok(http::Response::from_parts(
parts,
axum::body::Body::new(body),
))
}

/// A trait for types that can be used to render them as HTML.
Expand Down Expand Up @@ -440,25 +433,6 @@ mod tests {
assert!(app.router.is_empty());
}

#[test]
fn test_response_new_html() {
let body = Body::fixed("<html></html>");
let response = Response::new_html(StatusCode::OK, body);
assert_eq!(response.status, StatusCode::OK);
assert_eq!(
response.headers.get(CONTENT_TYPE_HEADER).unwrap(),
HTML_CONTENT_TYPE
);
}

#[test]
fn test_response_new_redirect() {
let location = "http://example.com";
let response = Response::new_redirect(location);
assert_eq!(response.status, StatusCode::SEE_OTHER);
assert_eq!(response.headers.get(LOCATION_HEADER).unwrap(), location);
}

#[test]
fn test_flareon_project_builder() {
let app = FlareonApp::builder().urls([]).build().unwrap();
Expand Down
Loading

0 comments on commit 4bce021

Please sign in to comment.