Skip to content

Commit

Permalink
feat: add form support and better routing; implement TODO app example
Browse files Browse the repository at this point in the history
  • Loading branch information
m4tx committed Aug 16, 2024
1 parent bd6ae75 commit a654112
Show file tree
Hide file tree
Showing 27 changed files with 1,996 additions and 125 deletions.
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ members = [
"flareon-orm",
# Examples
"examples/hello-world",
"examples/todo-list",
"examples/link-shortener",
]
resolver = "2"

Expand All @@ -15,19 +17,27 @@ edition = "2021"
license = "MIT OR Apache-2.0"

[workspace.dependencies]
askama = "0.12.1"
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"
derive_more = { version = "1.0.0", features = ["full"] }
env_logger = "0.11.3"
flareon = { path = "flareon" }
flareon_macros = { path = "flareon-macros" }
flareon_orm = { path = "flareon-orm" }
form_urlencoded = "1.2.1"
indexmap = "2.2.6"
itertools = "0.13.0"
log = "0.4.22"
regex = "1.10.5"
sea-query = "0.32.0-rc.1"
sea-query-binder = { version = "0.7.0-rc.1", features = ["sqlx-any", "runtime-tokio"] }
serde = "1.0.203"
slug = "0.1.5"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
tower = "0.4.13"
sqlx = { version = "0.8.0", features = ["runtime-tokio", "sqlite"] }
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
Binary file added db.sqlite3
Binary file not shown.
8 changes: 4 additions & 4 deletions examples/hello-world/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::sync::Arc;

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

fn return_hello(_request: Request) -> Result<Response, Error> {
async fn return_hello(_request: Request) -> Result<Response, Error> {
Ok(Response::new_html(
StatusCode::OK,
Body::fixed("<h1>Hello Flareon!</h1>".as_bytes().to_vec()),
Expand Down
13 changes: 13 additions & 0 deletions examples/link-shortener/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "example-link-shortener"
version = "0.1.0"
publish = false
description = "Link Shortener - Flareon example."
edition = "2021"

[dependencies]
askama = "0.12.1"
env_logger = "0.11.5"
flareon = { path = "../../flareon" }
sea-query = "0.32.0-rc.1"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
124 changes: 124 additions & 0 deletions examples/link-shortener/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
mod models;

use std::sync::Arc;

use askama::Template;
use flareon::db::{Database, Model};
use flareon::forms::Form;
use flareon::prelude::{Body, Error, FlareonApp, FlareonProject, Response, Route, StatusCode};
use flareon::request::Request;
use flareon::reverse;
use sea_query::Table;
use tokio::sync::RwLock;

use crate::models::Link;
// #[derive(Debug, Clone)]
// struct TodoItem {
// title: String,
// }
//
// #[derive(Debug, Template)]
// #[template(path = "index.html")]
// struct IndexTemplate<'a> {
// request: &'a Request,
// todo_items: Vec<TodoItem>,
// }

// static TODOS: RwLock<Vec<TodoItem>> = RwLock::const_new(Vec::new());

// async fn index(request: Request) -> Result<Response, Error> {
// let todo_items = (*TODOS.read().await).clone();
// let index_template = IndexTemplate {
// request: &request,
// todo_items,
// };
// let rendered = index_template.render().unwrap();
//
// Ok(Response::new_html(
// StatusCode::OK,
// Body::fixed(rendered.as_bytes().to_vec()),
// ))
// }
//
// #[derive(Debug, Form)]
// struct TodoForm {
// #[form(opt(max_length = 100))]
// title: String,
// }
//
// async fn add_todo(mut request: Request) -> Result<Response, Error> {
// let todo_form = TodoForm::from_request(&mut request).await.unwrap();
//
// {
// let mut todos = TODOS.write().await;
// todos.push(TodoItem {
// title: todo_form.title,
// });
// }
//
// Ok(reverse!(request, "index"))
// }
//
// 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 = todo_id.parse::<usize>().expect("todo_id is not a number");
//
// {
// let mut todos = TODOS.write().await;
// todos.remove(todo_id);
// }
//
// Ok(reverse!(request, "index"))
// }

#[tokio::main]
async fn main() {
env_logger::init();

// let todo_app = FlareonApp::builder()
// .urls([
// Route::with_handler_and_name("/", Arc::new(Box::new(index)),
// "index"), Route::with_handler_and_name("/todos/add",
// Arc::new(Box::new(add_todo)), "add-todo"),
// Route::with_handler_and_name(
// "/todos/:todo_id/remove",
// Arc::new(Box::new(remove_todo)),
// "remove-todo",
// ),
// ])
// .build()
// .unwrap();
//
// let todo_project = FlareonProject::builder()
// .register_app_with_views(todo_app, "")
// .build()
// .unwrap();
//
// flareon::run(todo_project, "127.0.0.1:8000").await.unwrap();

let db = Database::new("sqlite:db.sqlite3?mode=rwc&cache=shared")
.await
.unwrap();

db.execute(
r"CREATE TABLE IF NOT EXISTS link (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL,
url TEXT NOT NULL,
visits INTEGER NOT NULL DEFAULT 0
)",
)
.await
.unwrap();

let mut link = Link {
id: 1,
slug: "test2".to_string(),
url: "https://example.com".to_string(),
visits: 0,
};
link.save(&db).await.unwrap();
link.save(&db).await.unwrap();

db.close().await.unwrap();
}
38 changes: 38 additions & 0 deletions examples/link-shortener/src/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
pub struct Link {
pub id: i32,
pub slug: String,
pub url: String,
pub visits: i32,
}

impl ::flareon::db::Model for Link {
const COLUMNS: &'static [::flareon::db::Column] = &[
::flareon::db::Column::new(::flareon::db::Identifier::new("id")).auto(true),
::flareon::db::Column::new(::flareon::db::Identifier::new("slug")),
::flareon::db::Column::new(::flareon::db::Identifier::new("url")),
::flareon::db::Column::new(::flareon::db::Identifier::new("visits")),
];
const TABLE_NAME: &'static str = "link";

fn from_db(db_row: ::flareon::db::Row) -> Self {
Self {
id: db_row.get::<i32>(0),
slug: db_row.get::<String>(1),
url: db_row.get::<String>(2),
visits: db_row.get::<i32>(3),
}
}

fn get_values(&self, columns: &[usize]) -> Vec<&dyn ::flareon::db::ValueRef> {
columns
.iter()
.map(|&column| match column {
0 => &self.id as &dyn ::flareon::db::ValueRef,
1 => &self.slug as &dyn ::flareon::db::ValueRef,
2 => &self.url as &dyn ::flareon::db::ValueRef,
3 => &self.visits as &dyn ::flareon::db::ValueRef,
_ => panic!("Unknown column index: {}", column),
})
.collect()
}
}
28 changes: 28 additions & 0 deletions examples/link-shortener/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% let request = request %}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TODO List</title>
</head>
<body>
<h1>TODO List</h1>
<form id="todo-form" action="{{ flareon::reverse_str!(request, "add-todo") }}" method="post">
<input type="text" id="title" name="title" placeholder="Enter a new TODO" required>
<button type="submit">Add TODO</button>
</form>
<ul id="todo-list">
{% for todo in todo_items %}
<li>
{% let todo_id = loop.index0 %}
<form action="{{ flareon::reverse_str!(request, "remove-todo", "todo_id" => todo_id) }}" method="post">
<span>{{ todo.title }}</span>
<button type="submit">Remove</button>
</form>
</li>
{% endfor %}
</ul>
</body>
</html>
12 changes: 12 additions & 0 deletions examples/todo-list/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "example-todo-list"
version = "0.1.0"
publish = false
description = "TODO List - Flareon example."
edition = "2021"

[dependencies]
askama = "0.12.1"
flareon = { path = "../../flareon" }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
env_logger = "0.11.5"
92 changes: 92 additions & 0 deletions examples/todo-list/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::sync::Arc;

use askama::Template;
use flareon::forms::Form;
use flareon::prelude::{Body, Error, FlareonApp, FlareonProject, Response, Route, StatusCode};
use flareon::request::Request;
use flareon::reverse;
use tokio::sync::RwLock;

#[derive(Debug, Clone)]
struct TodoItem {
title: String,
}

#[derive(Debug, Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
request: &'a Request,
todo_items: Vec<TodoItem>,
}

static TODOS: RwLock<Vec<TodoItem>> = RwLock::const_new(Vec::new());

async fn index(request: Request) -> Result<Response, Error> {
let todo_items = (*TODOS.read().await).clone();
let index_template = IndexTemplate {
request: &request,
todo_items,
};
let rendered = index_template.render().unwrap();

Ok(Response::new_html(
StatusCode::OK,
Body::fixed(rendered.as_bytes().to_vec()),
))
}

#[derive(Debug, Form)]
struct TodoForm {
#[form(opt(max_length = 100))]
title: String,
}

async fn add_todo(mut request: Request) -> Result<Response, Error> {
let todo_form = TodoForm::from_request(&mut request).await.unwrap();

{
let mut todos = TODOS.write().await;
todos.push(TodoItem {
title: todo_form.title,
});
}

Ok(reverse!(request, "index"))
}

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 = todo_id.parse::<usize>().expect("todo_id is not a number");

{
let mut todos = TODOS.write().await;
todos.remove(todo_id);
}

Ok(reverse!(request, "index"))
}

#[tokio::main]
async fn main() {
env_logger::init();

let todo_app = FlareonApp::builder()
.urls([
Route::with_handler_and_name("/", Arc::new(Box::new(index)), "index"),
Route::with_handler_and_name("/todos/add", Arc::new(Box::new(add_todo)), "add-todo"),
Route::with_handler_and_name(
"/todos/:todo_id/remove",
Arc::new(Box::new(remove_todo)),
"remove-todo",
),
])
.build()
.unwrap();

let todo_project = FlareonProject::builder()
.register_app_with_views(todo_app, "")
.build()
.unwrap();

flareon::run(todo_project, "127.0.0.1:8000").await.unwrap();
}
28 changes: 28 additions & 0 deletions examples/todo-list/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% let request = request %}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TODO List</title>
</head>
<body>
<h1>TODO List</h1>
<form id="todo-form" action="{{ flareon::reverse_str!(request, "add-todo") }}" method="post">
<input type="text" id="title" name="title" placeholder="Enter a new TODO" required>
<button type="submit">Add TODO</button>
</form>
<ul id="todo-list">
{% for todo in todo_items %}
<li>
{% let todo_id = loop.index0 %}
<form action="{{ flareon::reverse_str!(request, "remove-todo", "todo_id" => todo_id) }}" method="post">
<span>{{ todo.title }}</span>
<button type="submit">Remove</button>
</form>
</li>
{% endfor %}
</ul>
</body>
</html>
Loading

0 comments on commit a654112

Please sign in to comment.