-
-
Notifications
You must be signed in to change notification settings - Fork 501
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #297 from YoshieraHuang/master
Add axum example
- Loading branch information
Showing
17 changed files
with
1,475 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
HOST=127.0.0.1 | ||
PORT=8000 | ||
DATABASE_URL="postgres://postgres:password@localhost/axum_exmaple" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
[package] | ||
name = "sea-orm-axum-example" | ||
version = "0.1.0" | ||
authors = ["Yoshiera Huang <[email protected]>"] | ||
edition = "2021" | ||
publish = false | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
[workspace] | ||
|
||
[dependencies] | ||
tokio = { version = "1.5", features = ["full"] } | ||
axum = { version = "0.3.0" } | ||
tower = "0.4.10" | ||
tower-http = { version = "0.1", features = ["fs"] } | ||
tower-cookies = { git = "https://github.com/imbolc/tower-cookies" } | ||
anyhow = "1" | ||
dotenv = "0.15" | ||
env_logger = "0.9" | ||
serde = "1" | ||
serde_json = "1" | ||
tera = "1" | ||
|
||
[dependencies.sea-orm] | ||
path = "../../" # remove this line in your own project | ||
version = "^0.3.0" | ||
features = ["macros", "runtime-tokio-native-tls"] | ||
default-features = false | ||
|
||
[features] | ||
default = ["sqlx-postgres"] | ||
sqlx-mysql = ["sea-orm/sqlx-mysql"] | ||
sqlx-postgres = ["sea-orm/sqlx-postgres"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Axum with SeaORM example app | ||
|
||
Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`. | ||
|
||
```toml | ||
[features] | ||
default = ["sqlx-$DATABASE"] | ||
``` | ||
|
||
Edit `.env` to point to your database. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
use axum::http::{header, HeaderMap, HeaderValue, StatusCode}; | ||
use serde::{de::DeserializeOwned, Deserialize, Serialize}; | ||
use tower_cookies::{Cookie, Cookies}; | ||
|
||
#[derive(Deserialize)] | ||
struct ValuedMessage<T> { | ||
#[serde(rename = "_")] | ||
value: T, | ||
} | ||
|
||
#[derive(Serialize)] | ||
struct ValuedMessageRef<'a, T> { | ||
#[serde(rename = "_")] | ||
value: &'a T, | ||
} | ||
|
||
const FLASH_COOKIE_NAME: &str = "_flash"; | ||
|
||
pub fn get_flash_cookie<T>(cookies: &Cookies) -> Option<T> | ||
where | ||
T: DeserializeOwned, | ||
{ | ||
cookies.get(FLASH_COOKIE_NAME).and_then(|flash_cookie| { | ||
if let Ok(ValuedMessage::<T> { value }) = serde_json::from_str(flash_cookie.value()) { | ||
Some(value) | ||
} else { | ||
None | ||
} | ||
}) | ||
} | ||
|
||
pub type PostResponse = (StatusCode, HeaderMap); | ||
|
||
pub fn post_response<T>(cookies: &mut Cookies, data: T) -> PostResponse | ||
where | ||
T: Serialize, | ||
{ | ||
let valued_message_ref = ValuedMessageRef { value: &data }; | ||
|
||
let mut cookie = Cookie::new( | ||
FLASH_COOKIE_NAME, | ||
serde_json::to_string(&valued_message_ref).unwrap(), | ||
); | ||
cookie.set_path("/"); | ||
cookies.add(cookie); | ||
|
||
let mut header = HeaderMap::new(); | ||
header.insert(header::LOCATION, HeaderValue::from_static("/")); | ||
|
||
(StatusCode::SEE_OTHER, header) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
mod flash; | ||
mod post; | ||
mod setup; | ||
|
||
use axum::{ | ||
error_handling::HandleErrorExt, | ||
extract::{Extension, Form, Path, Query}, | ||
http::StatusCode, | ||
response::Html, | ||
routing::{get, post, service_method_routing}, | ||
AddExtensionLayer, Router, Server, | ||
}; | ||
use flash::{get_flash_cookie, post_response, PostResponse}; | ||
use post::Entity as Post; | ||
use sea_orm::{prelude::*, Database, QueryOrder, Set}; | ||
use serde::{Deserialize, Serialize}; | ||
use std::str::FromStr; | ||
use std::{env, net::SocketAddr}; | ||
use tera::Tera; | ||
use tower::ServiceBuilder; | ||
use tower_cookies::{CookieManagerLayer, Cookies}; | ||
use tower_http::services::ServeDir; | ||
|
||
#[tokio::main] | ||
async fn main() -> anyhow::Result<()> { | ||
env::set_var("RUST_LOG", "debug"); | ||
env_logger::init(); | ||
|
||
dotenv::dotenv().ok(); | ||
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); | ||
let host = env::var("HOST").expect("HOST is not set in .env file"); | ||
let port = env::var("PORT").expect("PORT is not set in .env file"); | ||
let server_url = format!("{}:{}", host, port); | ||
|
||
let conn = Database::connect(db_url) | ||
.await | ||
.expect("Database connection failed"); | ||
let _ = setup::create_post_table(&conn).await; | ||
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")) | ||
.expect("Tera initialization failed"); | ||
// let state = AppState { templates, conn }; | ||
|
||
let app = Router::new() | ||
.route("/", get(list_posts).post(create_post)) | ||
.route("/:id", get(edit_post).post(update_post)) | ||
.route("/new", get(new_post)) | ||
.route("/delete/:id", post(delete_post)) | ||
.nest( | ||
"/static", | ||
service_method_routing::get(ServeDir::new(concat!( | ||
env!("CARGO_MANIFEST_DIR"), | ||
"/static" | ||
))) | ||
.handle_error(|error: std::io::Error| { | ||
( | ||
StatusCode::INTERNAL_SERVER_ERROR, | ||
format!("Unhandled internal error: {}", error), | ||
) | ||
}), | ||
) | ||
.layer( | ||
ServiceBuilder::new() | ||
.layer(CookieManagerLayer::new()) | ||
.layer(AddExtensionLayer::new(conn)) | ||
.layer(AddExtensionLayer::new(templates)), | ||
); | ||
|
||
let addr = SocketAddr::from_str(&server_url).unwrap(); | ||
Server::bind(&addr).serve(app.into_make_service()).await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
#[derive(Deserialize)] | ||
struct Params { | ||
page: Option<usize>, | ||
posts_per_page: Option<usize>, | ||
} | ||
|
||
#[derive(Deserialize, Serialize, Debug, Clone)] | ||
struct FlashData { | ||
kind: String, | ||
message: String, | ||
} | ||
|
||
async fn list_posts( | ||
Extension(ref templates): Extension<Tera>, | ||
Extension(ref conn): Extension<DatabaseConnection>, | ||
Query(params): Query<Params>, | ||
cookies: Cookies, | ||
) -> Result<Html<String>, (StatusCode, &'static str)> { | ||
let page = params.page.unwrap_or(1); | ||
let posts_per_page = params.posts_per_page.unwrap_or(5); | ||
let paginator = Post::find() | ||
.order_by_asc(post::Column::Id) | ||
.paginate(conn, posts_per_page); | ||
let num_pages = paginator.num_pages().await.ok().unwrap(); | ||
let posts = paginator | ||
.fetch_page(page - 1) | ||
.await | ||
.expect("could not retrieve posts"); | ||
|
||
let mut ctx = tera::Context::new(); | ||
ctx.insert("posts", &posts); | ||
ctx.insert("page", &page); | ||
ctx.insert("posts_per_page", &posts_per_page); | ||
ctx.insert("num_pages", &num_pages); | ||
|
||
if let Some(value) = get_flash_cookie::<FlashData>(&cookies) { | ||
ctx.insert("flash", &value); | ||
} | ||
|
||
let body = templates | ||
.render("index.html.tera", &ctx) | ||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; | ||
|
||
Ok(Html(body)) | ||
} | ||
|
||
async fn new_post( | ||
Extension(ref templates): Extension<Tera>, | ||
) -> Result<Html<String>, (StatusCode, &'static str)> { | ||
let ctx = tera::Context::new(); | ||
let body = templates | ||
.render("new.html.tera", &ctx) | ||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; | ||
|
||
Ok(Html(body)) | ||
} | ||
|
||
async fn create_post( | ||
Extension(ref conn): Extension<DatabaseConnection>, | ||
form: Form<post::Model>, | ||
mut cookies: Cookies, | ||
) -> Result<PostResponse, (StatusCode, &'static str)> { | ||
let model = form.0; | ||
|
||
post::ActiveModel { | ||
title: Set(model.title.to_owned()), | ||
text: Set(model.text.to_owned()), | ||
..Default::default() | ||
} | ||
.save(conn) | ||
.await | ||
.expect("could not insert post"); | ||
|
||
let data = FlashData { | ||
kind: "success".to_owned(), | ||
message: "Post succcessfully added".to_owned(), | ||
}; | ||
|
||
Ok(post_response(&mut cookies, data)) | ||
} | ||
|
||
async fn edit_post( | ||
Extension(ref templates): Extension<Tera>, | ||
Extension(ref conn): Extension<DatabaseConnection>, | ||
Path(id): Path<i32>, | ||
) -> Result<Html<String>, (StatusCode, &'static str)> { | ||
let post: post::Model = Post::find_by_id(id) | ||
.one(conn) | ||
.await | ||
.expect("could not find post") | ||
.unwrap(); | ||
|
||
let mut ctx = tera::Context::new(); | ||
ctx.insert("post", &post); | ||
|
||
let body = templates | ||
.render("edit.html.tera", &ctx) | ||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; | ||
|
||
Ok(Html(body)) | ||
} | ||
|
||
async fn update_post( | ||
Extension(ref conn): Extension<DatabaseConnection>, | ||
Path(id): Path<i32>, | ||
form: Form<post::Model>, | ||
mut cookies: Cookies, | ||
) -> Result<PostResponse, (StatusCode, String)> { | ||
let model = form.0; | ||
|
||
post::ActiveModel { | ||
id: Set(id), | ||
title: Set(model.title.to_owned()), | ||
text: Set(model.text.to_owned()), | ||
} | ||
.save(conn) | ||
.await | ||
.expect("could not edit post"); | ||
|
||
let data = FlashData { | ||
kind: "success".to_owned(), | ||
message: "Post succcessfully updated".to_owned(), | ||
}; | ||
|
||
Ok(post_response(&mut cookies, data)) | ||
} | ||
|
||
async fn delete_post( | ||
Extension(ref conn): Extension<DatabaseConnection>, | ||
Path(id): Path<i32>, | ||
mut cookies: Cookies, | ||
) -> Result<PostResponse, (StatusCode, &'static str)> { | ||
let post: post::ActiveModel = Post::find_by_id(id) | ||
.one(conn) | ||
.await | ||
.unwrap() | ||
.unwrap() | ||
.into(); | ||
|
||
post.delete(conn).await.unwrap(); | ||
|
||
let data = FlashData { | ||
kind: "success".to_owned(), | ||
message: "Post succcessfully deleted".to_owned(), | ||
}; | ||
|
||
Ok(post_response(&mut cookies, data)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
//! SeaORM Entity. Generated by sea-orm-codegen 0.3.2 | ||
|
||
use sea_orm::entity::prelude::*; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] | ||
#[sea_orm(table_name = "posts")] | ||
pub struct Model { | ||
#[sea_orm(primary_key)] | ||
#[serde(skip_deserializing)] | ||
pub id: i32, | ||
pub title: String, | ||
#[sea_orm(column_type = "Text")] | ||
pub text: String, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug, EnumIter)] | ||
pub enum Relation {} | ||
|
||
impl RelationTrait for Relation { | ||
fn def(&self) -> RelationDef { | ||
panic!("No RelationDef") | ||
} | ||
} | ||
|
||
impl ActiveModelBehavior for ActiveModel {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; | ||
use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult}; | ||
|
||
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> { | ||
let builder = db.get_database_backend(); | ||
db.execute(builder.build(stmt)).await | ||
} | ||
|
||
pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> { | ||
let stmt = sea_query::Table::create() | ||
.table(super::post::Entity) | ||
.if_not_exists() | ||
.col( | ||
ColumnDef::new(super::post::Column::Id) | ||
.integer() | ||
.not_null() | ||
.auto_increment() | ||
.primary_key(), | ||
) | ||
.col( | ||
ColumnDef::new(super::post::Column::Title) | ||
.string() | ||
.not_null(), | ||
) | ||
.col( | ||
ColumnDef::new(super::post::Column::Text) | ||
.string() | ||
.not_null(), | ||
) | ||
.to_owned(); | ||
|
||
create_table(db, &stmt).await | ||
} |
Oops, something went wrong.