Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add axum example #297

Merged
merged 4 commits into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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