Skip to content

Commit

Permalink
feat: initial API implementation
Browse files Browse the repository at this point in the history
This is an early vision of the basic concepts and how the API overall
might look like in the future. There's also a fully working Hello world
example that implements a single endpoint.
  • Loading branch information
m4tx committed Jul 28, 2024
1 parent 15694a1 commit 089fd6e
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 10 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ license = "MIT OR Apache-2.0"
[workspace.dependencies]
async-trait = "0.1.80"
axum = "0.7.5"
bytes = "1.6.1"
chrono = { version = "0.4.38", features = ["serde"] }
clap = { version = "4.5.8", features = ["derive", "env"] }
derive_builder = "0.20.0"
env_logger = "0.11.3"
indexmap = "2.2.6"
itertools = "0.13.0"
log = "0.4.22"
regex = "1.10.5"
serde = "1.0.203"
slug = "0.1.5"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
tower = "0.4.13"
thiserror = "1.0.61"
3 changes: 3 additions & 0 deletions examples/hello-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ name = "example-hello-world"
version = "0.1.0"
publish = false
description = "Hello World - Flareon example."
edition = "2021"

[dependencies]
flareon = { path = "../../flareon" }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
30 changes: 29 additions & 1 deletion examples/hello-world/src/main.rs
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
fn main() {}
use std::sync::Arc;

use flareon::prelude::{
Body, Error, FlareonApp, FlareonProject, Request, Response, Route, StatusCode,
};

fn return_hello(_request: Request) -> Result<Response, Error> {
Ok(Response::new_html(
StatusCode::OK,
Body::fixed("<h1>Hello Flareon!</h1>".as_bytes().to_vec()),
))
}

Check warning on line 12 in examples/hello-world/src/main.rs

View check run for this annotation

Codecov / codecov/patch

examples/hello-world/src/main.rs#L7-L12

Added lines #L7 - L12 were not covered by tests

#[tokio::main]
async fn main() {
let hello_app = FlareonApp::builder()
.urls([Route::new("/", Arc::new(Box::new(return_hello)))])
.build()
.unwrap();

let flareon_project = FlareonProject::builder()
.register_app_with_views(hello_app, "/")
.build()
.unwrap();

flareon::run(flareon_project, "127.0.0.1:8000")
.await
.unwrap();
}

Check warning on line 29 in examples/hello-world/src/main.rs

View check run for this annotation

Codecov / codecov/patch

examples/hello-world/src/main.rs#L15-L29

Added lines #L15 - L29 were not covered by tests
8 changes: 8 additions & 0 deletions flareon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ license.workspace = true
description = "Modern web framework focused on speed and ease of use."

[dependencies]
async-trait.workspace = true
axum.workspace = true
bytes.workspace = true
derive_builder.workspace = true
indexmap.workspace = true
log.workspace = true
thiserror.workspace = true
tokio.workspace = true
283 changes: 274 additions & 9 deletions flareon/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,279 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
pub mod prelude;

use std::fmt::{Debug, Formatter};
use std::io::Read;
use std::sync::Arc;

use async_trait::async_trait;
use axum::handler::HandlerWithoutStateExt;
use bytes::Bytes;
use derive_builder::Builder;
use indexmap::IndexMap;
use log::info;
use thiserror::Error;

pub type StatusCode = axum::http::StatusCode;

#[async_trait]
pub trait RequestHandler {
async fn handle(&self, request: Request) -> Result<Response, Error>;
}

#[derive(Clone, Debug)]
pub struct Router {
urls: Vec<Route>,
}

impl Router {
#[must_use]
pub fn with_urls<T: Into<Vec<Route>>>(urls: T) -> Self {
Self { urls: urls.into() }
}

Check warning on line 31 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L29-L31

Added lines #L29 - L31 were not covered by tests
}

#[async_trait]
impl RequestHandler for Router {
async fn handle(&self, request: Request) -> Result<Response, Error> {
let path = request.uri().path();

Check warning on line 37 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L36-L37

Added lines #L36 - L37 were not covered by tests

for route in &self.urls {
if path.starts_with(&route.url) {
return route.view.handle(request).await;
}

Check warning on line 42 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L39-L42

Added lines #L39 - L42 were not covered by tests
}

unimplemented!("404 handler is not implemented yet")
}

Check warning on line 46 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L45-L46

Added lines #L45 - L46 were not covered by tests
}

#[async_trait]
impl<T> RequestHandler for T
where
T: Fn(Request) -> Result<Response, Error> + Send + Sync,
{
async fn handle(&self, request: Request) -> Result<Response, Error> {
self(request)
}

Check warning on line 56 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L54-L56

Added lines #L54 - L56 were not covered by tests
}

/// A building block for a Flareon project.
///
/// A Flareon app is a part (ideally, reusable) of a Flareon project that is
/// responsible for its own set of functionalities. Examples of apps could be:
/// * admin panel
/// * user authentication
/// * blog
/// * message board
/// * session management
/// * etc.
///
/// Each app can have its own set of URLs that it can handle which can be
/// mounted on the project's router, its own set of middleware, database
/// migrations (which can depend on other apps), etc.
#[derive(Clone, Debug, Builder)]

Check warning on line 73 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L73

Added line #L73 was not covered by tests
#[builder(setter(into))]
pub struct FlareonApp {
router: Router,
}

impl FlareonApp {
#[must_use]
pub fn builder() -> FlareonAppBuilder {
FlareonAppBuilder::default()
}

Check warning on line 83 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L81-L83

Added lines #L81 - L83 were not covered by tests
}

impl FlareonAppBuilder {
#[allow(unused_mut)]
pub fn urls<T: Into<Vec<Route>>>(&mut self, urls: T) -> &mut Self {
let mut new = self;
new.router = Some(Router::with_urls(urls.into()));
new
}

Check warning on line 92 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L88-L92

Added lines #L88 - L92 were not covered by tests
}

#[derive(Clone)]
pub struct Route {
url: String,
view: Arc<Box<dyn RequestHandler + Send + Sync>>,
}

impl Debug for Route {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Route")
.field("url", &self.url)
.field("view", &"...")
.finish()
}

Check warning on line 107 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L102-L107

Added lines #L102 - L107 were not covered by tests
}

impl Route {
#[must_use]
pub fn new<T: Into<String>>(url: T, view: Arc<Box<dyn RequestHandler + Send + Sync>>) -> Self {
Self {
url: url.into(),
view,
}
}

Check warning on line 117 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L112-L117

Added lines #L112 - L117 were not covered by tests
}

pub type Request = axum::extract::Request;

type HeadersMap = IndexMap<String, String>;

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

const CONTENT_TYPE_HEADER: &str = "Content-Type";
const HTML_CONTENT_TYPE: &str = "text/html";

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

Check warning on line 142 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L136-L142

Added lines #L136 - L142 were not covered by tests

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

Check warning on line 149 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L145-L149

Added lines #L145 - L149 were not covered by tests
}

pub enum Body {
Fixed(Bytes),
Streaming(Box<dyn Read>),
}

#[cfg(test)]
mod tests {
use super::*;
impl Debug for Body {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Body::Fixed(data) => f.debug_tuple("Fixed").field(data).finish(),
Body::Streaming(_) => f.debug_tuple("Streaming").field(&"...").finish(),

Check warning on line 161 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L158-L161

Added lines #L158 - L161 were not covered by tests
}
}

Check warning on line 163 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L163

Added line #L163 was not covered by tests
}

impl Body {
#[must_use]
pub fn empty() -> Self {
Self::Fixed(Bytes::new())
}

Check warning on line 170 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L168-L170

Added lines #L168 - L170 were not covered by tests

#[must_use]
pub fn fixed<T: Into<Bytes>>(data: T) -> Self {
Self::Fixed(data.into())
}

Check warning on line 175 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L173-L175

Added lines #L173 - L175 were not covered by tests
}

#[derive(Debug, thiserror::Error)]

Check warning on line 178 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L178

Added line #L178 was not covered by tests
pub enum Error {
#[error("Could not create a response object: {0}")]
ResponseBuilder(#[from] axum::http::Error),
}

#[derive(Clone, Debug)]
pub struct FlareonProject {
apps: Vec<FlareonApp>,
router: Router,
}

#[derive(Debug)]
pub struct FlareonProjectBuilder {
apps: Vec<FlareonApp>,
urls: Vec<Route>,
}

impl FlareonProjectBuilder {
#[must_use]
pub fn new() -> Self {
Self {
apps: Vec::new(),
urls: Vec::new(),
}
}

Check warning on line 203 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L198-L203

Added lines #L198 - L203 were not covered by tests

#[must_use]
pub fn register_app_with_views(&mut self, app: FlareonApp, url_prefix: &str) -> &mut Self {
let new = self;
new.urls.push(Route::new(
url_prefix,
Arc::new(Box::new(app.router.clone())),
));
new.apps.push(app);
new
}

Check warning on line 214 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L206-L214

Added lines #L206 - L214 were not covered by tests

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
pub fn build(&self) -> Result<FlareonProject, Error> {
Ok(FlareonProject {
apps: self.apps.clone(),
router: Router::with_urls(self.urls.clone()),
})

Check warning on line 220 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L216-L220

Added lines #L216 - L220 were not covered by tests
}
}

impl Default for FlareonProjectBuilder {
fn default() -> Self {
Self::new()
}

Check warning on line 227 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L225-L227

Added lines #L225 - L227 were not covered by tests
}

impl FlareonProject {
#[must_use]
pub fn builder() -> FlareonProjectBuilder {
FlareonProjectBuilder::default()
}

Check warning on line 234 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L232-L234

Added lines #L232 - L234 were not covered by tests
}

pub async fn run(mut project: FlareonProject, address_str: &str) -> Result<(), Error> {
for app in &mut project.apps {
info!("Initializing app: {:?}", app);

Check warning on line 239 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L237-L239

Added lines #L237 - L239 were not covered by tests
}

let listener = tokio::net::TcpListener::bind(address_str).await.unwrap();

let handler = |request: axum::extract::Request| async move {
pass_to_axum(&project, request)
.await
.unwrap_or_else(handle_response_error)
};
axum::serve(listener, handler.into_make_service())
.await
.unwrap();

Ok(())
}

Check warning on line 254 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L242-L254

Added lines #L242 - L254 were not covered by tests

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

Check warning on line 260 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L256-L260

Added lines #L256 - L260 were not covered by tests

let mut builder = axum::http::Response::builder().status(response.status);
for (key, value) in response.headers {
builder = builder.header(key, value);
}
let axum_response = builder.body(match response.body {
Body::Fixed(data) => axum::body::Body::from(data),
Body::Streaming(_) => unimplemented!(),

Check warning on line 268 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L262-L268

Added lines #L262 - L268 were not covered by tests
});

match axum_response {
Ok(response) => Ok(response),
Err(error) => Err(Error::ResponseBuilder(error)),

Check warning on line 273 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L271-L273

Added lines #L271 - L273 were not covered by tests
}
}

Check warning on line 275 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L275

Added line #L275 was not covered by tests

fn handle_response_error(_error: Error) -> axum::response::Response {
unimplemented!("500 error handler is not implemented yet")

Check warning on line 278 in flareon/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

flareon/src/lib.rs#L277-L278

Added lines #L277 - L278 were not covered by tests
}
3 changes: 3 additions & 0 deletions flareon/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub use crate::{
Body, Error, FlareonApp, FlareonProject, Request, RequestHandler, Response, Route, StatusCode,
};

0 comments on commit 089fd6e

Please sign in to comment.