Skip to content

Commit

Permalink
Merge pull request #297 from YoshieraHuang/master
Browse files Browse the repository at this point in the history
Add axum example
  • Loading branch information
tyt2y3 authored Nov 8, 2021
2 parents a0cb03c + cb996d8 commit 4120b16
Show file tree
Hide file tree
Showing 17 changed files with 1,475 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
path: [basic, actix_example, actix4_example, rocket_example]
path: [basic, actix_example, actix4_example, axum_example, rocket_example]
steps:
- uses: actions/checkout@v2

Expand Down
3 changes: 3 additions & 0 deletions examples/axum_example/.env
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"
33 changes: 33 additions & 0 deletions examples/axum_example/Cargo.toml
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"]
10 changes: 10 additions & 0 deletions examples/axum_example/README.md
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.
51 changes: 51 additions & 0 deletions examples/axum_example/src/flash.rs
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)
}
221 changes: 221 additions & 0 deletions examples/axum_example/src/main.rs
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))
}
26 changes: 26 additions & 0 deletions examples/axum_example/src/post.rs
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 {}
33 changes: 33 additions & 0 deletions examples/axum_example/src/setup.rs
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
}
Loading

0 comments on commit 4120b16

Please sign in to comment.