mirror of
https://github.com/actix/examples
synced 2025-06-27 01:27:43 +02:00
Restructure folders (#411)
This commit is contained in:
committed by
GitHub
parent
9db98162b2
commit
c3407627d0
136
basics/todo/src/api.rs
Normal file
136
basics/todo/src/api.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use actix_files::NamedFile;
|
||||
use actix_session::Session;
|
||||
use actix_web::middleware::errhandlers::ErrorHandlerResponse;
|
||||
use actix_web::{dev, error, http, web, Error, HttpResponse, Result};
|
||||
use serde::Deserialize;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
use crate::db;
|
||||
use crate::session::{self, FlashMessage};
|
||||
|
||||
pub async fn index(
|
||||
pool: web::Data<db::PgPool>,
|
||||
tmpl: web::Data<Tera>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let tasks = web::block(move || db::get_all_tasks(&pool)).await?;
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("tasks", &tasks);
|
||||
|
||||
//Session is set during operations on other endpoints
|
||||
//that can redirect to index
|
||||
if let Some(flash) = session::get_flash(&session)? {
|
||||
context.insert("msg", &(flash.kind, flash.message));
|
||||
session::clear_flash(&session);
|
||||
}
|
||||
|
||||
let rendered = tmpl
|
||||
.render("index.html.tera", &context)
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(rendered))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateForm {
|
||||
description: String,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
params: web::Form<CreateForm>,
|
||||
pool: web::Data<db::PgPool>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if params.description.is_empty() {
|
||||
session::set_flash(
|
||||
&session,
|
||||
FlashMessage::error("Description cannot be empty"),
|
||||
)?;
|
||||
Ok(redirect_to("/"))
|
||||
} else {
|
||||
web::block(move || db::create_task(params.into_inner().description, &pool))
|
||||
.await?;
|
||||
session::set_flash(&session, FlashMessage::success("Task successfully added"))?;
|
||||
Ok(redirect_to("/"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateParams {
|
||||
id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateForm {
|
||||
_method: String,
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
db: web::Data<db::PgPool>,
|
||||
params: web::Path<UpdateParams>,
|
||||
form: web::Form<UpdateForm>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
match form._method.as_ref() {
|
||||
"put" => toggle(db, params).await,
|
||||
"delete" => delete(db, params, session).await,
|
||||
unsupported_method => {
|
||||
let msg = format!("Unsupported HTTP method: {}", unsupported_method);
|
||||
Err(error::ErrorBadRequest(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn toggle(
|
||||
pool: web::Data<db::PgPool>,
|
||||
params: web::Path<UpdateParams>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
web::block(move || db::toggle_task(params.id, &pool)).await?;
|
||||
Ok(redirect_to("/"))
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
pool: web::Data<db::PgPool>,
|
||||
params: web::Path<UpdateParams>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
web::block(move || db::delete_task(params.id, &pool)).await?;
|
||||
session::set_flash(&session, FlashMessage::success("Task was deleted."))?;
|
||||
Ok(redirect_to("/"))
|
||||
}
|
||||
|
||||
fn redirect_to(location: &str) -> HttpResponse {
|
||||
HttpResponse::Found()
|
||||
.header(http::header::LOCATION, location)
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn bad_request<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
let new_resp = NamedFile::open("static/errors/400.html")?
|
||||
.set_status_code(res.status())
|
||||
.into_response(res.request())?;
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
res.into_response(new_resp.into_body()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn not_found<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
let new_resp = NamedFile::open("static/errors/404.html")?
|
||||
.set_status_code(res.status())
|
||||
.into_response(res.request())?;
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
res.into_response(new_resp.into_body()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn internal_server_error<B>(
|
||||
res: dev::ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
let new_resp = NamedFile::open("static/errors/500.html")?
|
||||
.set_status_code(res.status())
|
||||
.into_response(res.request())?;
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
res.into_response(new_resp.into_body()),
|
||||
))
|
||||
}
|
41
basics/todo/src/db.rs
Normal file
41
basics/todo/src/db.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::r2d2::{ConnectionManager, Pool, PoolError, PooledConnection};
|
||||
|
||||
use crate::model::{NewTask, Task};
|
||||
|
||||
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
|
||||
type PgPooledConnection = PooledConnection<ConnectionManager<PgConnection>>;
|
||||
|
||||
pub fn init_pool(database_url: &str) -> Result<PgPool, PoolError> {
|
||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||
Pool::builder().build(manager)
|
||||
}
|
||||
|
||||
fn get_conn(pool: &PgPool) -> Result<PgPooledConnection, &'static str> {
|
||||
pool.get().map_err(|_| "Can't get connection")
|
||||
}
|
||||
|
||||
pub fn get_all_tasks(pool: &PgPool) -> Result<Vec<Task>, &'static str> {
|
||||
Task::all(get_conn(pool)?.deref()).map_err(|_| "Error retrieving tasks")
|
||||
}
|
||||
|
||||
pub fn create_task(todo: String, pool: &PgPool) -> Result<(), &'static str> {
|
||||
let new_task = NewTask { description: todo };
|
||||
Task::insert(new_task, get_conn(pool)?.deref())
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Error inserting task")
|
||||
}
|
||||
|
||||
pub fn toggle_task(id: i32, pool: &PgPool) -> Result<(), &'static str> {
|
||||
Task::toggle_with_id(id, get_conn(pool)?.deref())
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Error toggling task completion")
|
||||
}
|
||||
|
||||
pub fn delete_task(id: i32, pool: &PgPool) -> Result<(), &'static str> {
|
||||
Task::delete_with_id(id, get_conn(pool)?.deref())
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Error deleting task")
|
||||
}
|
69
basics/todo/src/main.rs
Normal file
69
basics/todo/src/main.rs
Normal file
@ -0,0 +1,69 @@
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use std::{env, io};
|
||||
|
||||
use actix_files as fs;
|
||||
use actix_session::CookieSession;
|
||||
use actix_web::middleware::{errhandlers::ErrorHandlers, Logger};
|
||||
use actix_web::{http, web, App, HttpServer};
|
||||
use dotenv::dotenv;
|
||||
use tera::Tera;
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod model;
|
||||
mod schema;
|
||||
mod session;
|
||||
|
||||
static SESSION_SIGNING_KEY: &[u8] = &[0; 32];
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
dotenv().ok();
|
||||
|
||||
env::set_var("RUST_LOG", "actix_todo=debug,actix_web=info");
|
||||
env_logger::init();
|
||||
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let pool = db::init_pool(&database_url).expect("Failed to create pool");
|
||||
|
||||
let app = move || {
|
||||
debug!("Constructing the App");
|
||||
|
||||
let mut templates = match Tera::new("templates/**/*") {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
println!("Parsing error(s): {}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
templates.autoescape_on(vec!["tera"]);
|
||||
|
||||
let session_store = CookieSession::signed(SESSION_SIGNING_KEY).secure(false);
|
||||
|
||||
let error_handlers = ErrorHandlers::new()
|
||||
.handler(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
api::internal_server_error,
|
||||
)
|
||||
.handler(http::StatusCode::BAD_REQUEST, api::bad_request)
|
||||
.handler(http::StatusCode::NOT_FOUND, api::not_found);
|
||||
|
||||
App::new()
|
||||
.data(templates)
|
||||
.data(pool.clone())
|
||||
.wrap(Logger::default())
|
||||
.wrap(session_store)
|
||||
.wrap(error_handlers)
|
||||
.service(web::resource("/").route(web::get().to(api::index)))
|
||||
.service(web::resource("/todo").route(web::post().to(api::create)))
|
||||
.service(web::resource("/todo/{id}").route(web::post().to(api::update)))
|
||||
.service(fs::Files::new("/static", "static/"))
|
||||
};
|
||||
|
||||
debug!("Starting server");
|
||||
HttpServer::new(app).bind("localhost:8088")?.run().await
|
||||
}
|
47
basics/todo/src/model.rs
Normal file
47
basics/todo/src/model.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::schema::{
|
||||
tasks,
|
||||
tasks::dsl::{completed as task_completed, tasks as all_tasks},
|
||||
};
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[table_name = "tasks"]
|
||||
pub struct NewTask {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Queryable, Serialize)]
|
||||
pub struct Task {
|
||||
pub id: i32,
|
||||
pub description: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn all(conn: &PgConnection) -> QueryResult<Vec<Task>> {
|
||||
all_tasks.order(tasks::id.desc()).load::<Task>(conn)
|
||||
}
|
||||
|
||||
pub fn insert(todo: NewTask, conn: &PgConnection) -> QueryResult<usize> {
|
||||
diesel::insert_into(tasks::table)
|
||||
.values(&todo)
|
||||
.execute(conn)
|
||||
}
|
||||
|
||||
pub fn toggle_with_id(id: i32, conn: &PgConnection) -> QueryResult<usize> {
|
||||
let task = all_tasks.find(id).get_result::<Task>(conn)?;
|
||||
|
||||
let new_status = !task.completed;
|
||||
let updated_task = diesel::update(all_tasks.find(id));
|
||||
updated_task
|
||||
.set(task_completed.eq(new_status))
|
||||
.execute(conn)
|
||||
}
|
||||
|
||||
pub fn delete_with_id(id: i32, conn: &PgConnection) -> QueryResult<usize> {
|
||||
diesel::delete(all_tasks.find(id)).execute(conn)
|
||||
}
|
||||
}
|
7
basics/todo/src/schema.rs
Normal file
7
basics/todo/src/schema.rs
Normal file
@ -0,0 +1,7 @@
|
||||
table! {
|
||||
tasks (id) {
|
||||
id -> Int4,
|
||||
description -> Varchar,
|
||||
completed -> Bool,
|
||||
}
|
||||
}
|
39
basics/todo/src/session.rs
Normal file
39
basics/todo/src/session.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use actix_session::Session;
|
||||
use actix_web::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const FLASH_KEY: &str = "flash";
|
||||
|
||||
pub fn set_flash(session: &Session, flash: FlashMessage) -> Result<()> {
|
||||
session.set(FLASH_KEY, flash)
|
||||
}
|
||||
|
||||
pub fn get_flash(session: &Session) -> Result<Option<FlashMessage>> {
|
||||
session.get::<FlashMessage>(FLASH_KEY)
|
||||
}
|
||||
|
||||
pub fn clear_flash(session: &Session) {
|
||||
session.remove(FLASH_KEY);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct FlashMessage {
|
||||
pub kind: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl FlashMessage {
|
||||
pub fn success(message: &str) -> Self {
|
||||
Self {
|
||||
kind: "success".to_owned(),
|
||||
message: message.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(message: &str) -> Self {
|
||||
Self {
|
||||
kind: "error".to_owned(),
|
||||
message: message.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user