Skip to content

Commit

Permalink
Merge pull request #12 from render-examples/update
Browse files Browse the repository at this point in the history
update to actix.rs 4.9; updated todo example with sqlx
  • Loading branch information
lexpierce authored Oct 14, 2024
2 parents da0a3fb + 8ad8d76 commit 32afd45
Show file tree
Hide file tree
Showing 23 changed files with 1,945 additions and 1,627 deletions.
3,047 changes: 1,717 additions & 1,330 deletions Cargo.lock

Large diffs are not rendered by default.

30 changes: 12 additions & 18 deletions Cargo.toml
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
[package]
authors = ["Render Developers <[email protected]>"]
name = "actix-todo"
version = "0.1.1"
edition = "2018"
name = "todo"
version = "1.0.0"
edition = "2021"

[dependencies]
actix-web = "1.0.0"
actix-files = "0.1.1"
actix-session = "0.1.1"
dotenv = "0.14.1"
env_logger = "0.6.1"
futures = "0.1.27"
log = "0.4.6"
serde = "1.0.92"
serde_derive = "1.0.92"
serde_json = "1.0.39"
tera = "0.11.20"
actix-files = "0.6"
actix-session = { version = "0.10", features = ["cookie-session"] }
actix-web = "4.9"

[dependencies.diesel]
features = ["postgres", "r2d2"]
version = "1.4.2"
dotenvy = "0.15"
env_logger = "0.11"
log = "0.4"
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "tls-rustls"] }
tera = "1.5"
2 changes: 1 addition & 1 deletion README.md
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This repo can be used as a starting point to deploy [Actix](https://actix.rs) web applications written in [Rust](https://www.rust-lang.org) on Render.

It uses a PostgreSQL database and is based on the official [actix_todo](https://github.com/actix/examples/tree/master/actix_todo) example.
It uses a PostgreSQL database and is based on the official [todo](https://github.com/actix/examples/tree/master/basics/todo) example.

The sample app is up at https://actix-todo.onrender.com.

Expand Down
6 changes: 3 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cargo install sqlx-cli@^0.7 --no-default-features --features=postgres,rustls
sqlx migrate run
cargo build --release
cargo install diesel_cli --no-default-features --features postgres
diesel database setup
5 changes: 0 additions & 5 deletions diesel.toml

This file was deleted.

6 changes: 0 additions & 6 deletions migrations/00000000000000_diesel_initial_setup/down.sql

This file was deleted.

36 changes: 0 additions & 36 deletions migrations/00000000000000_diesel_initial_setup/up.sql

This file was deleted.

1 change: 0 additions & 1 deletion migrations/2018-07-05-163612_create_tasks_table/down.sql

This file was deleted.

1 change: 1 addition & 0 deletions migrations/20220205163207_create_tasks_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE tasks;
File renamed without changes.
220 changes: 98 additions & 122 deletions src/api.rs
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,172 +1,148 @@
use actix_files::NamedFile;
use actix_session::Session;
use actix_web::middleware::errhandlers::ErrorHandlerResponse;
use actix_web::{dev, error, http, web, Error, HttpResponse, Responder, Result};
use futures::future::{err, Either, Future, IntoFuture};
use tera::{escape_html, Context, Tera};

use crate::db;
use crate::session::{self, FlashMessage};

pub fn index(
pool: web::Data<db::PgPool>,
use actix_web::{
dev, error, http::StatusCode, middleware::ErrorHandlerResponse, web, Error, HttpResponse,
Responder, Result,
};
use serde::Deserialize;
use sqlx::PgPool;
use tera::{Context, Tera};

use crate::{
db,
session::{self, FlashMessage},
};

pub async fn index(
pool: web::Data<PgPool>,
tmpl: web::Data<Tera>,
session: Session,
) -> impl Future<Item = HttpResponse, Error = Error> {
web::block(move || db::get_all_tasks(&pool))
.from_err()
.then(move |res| match res {
Ok(tasks) => {
let mut context = Context::new();
let escaped_tasks = tasks
.iter()
.map(|task| task.html_escaped())
.collect::<Vec<_>>();
context.insert("tasks", &escaped_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, escape_html(&flash.message)));
session::clear_flash(&session);
}

let rendered =
tmpl.render("index.html.tera", &context).map_err(|e| {
error::ErrorInternalServerError(e.description().to_owned())
})?;

Ok(HttpResponse::Ok().body(rendered))
}
Err(e) => Err(e),
})
) -> Result<HttpResponse, Error> {
let tasks = db::get_all_tasks(&pool)
.await
.map_err(error::ErrorInternalServerError)?;

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)]
#[derive(Debug, Deserialize)]
pub struct CreateForm {
description: String,
}

pub fn create(
pub async fn create(
params: web::Form<CreateForm>,
pool: web::Data<db::PgPool>,
pool: web::Data<PgPool>,
session: Session,
) -> impl Future<Item = HttpResponse, Error = Error> {
) -> Result<impl Responder, Error> {
if params.description.is_empty() {
Either::A(
session::set_flash(
&session,
FlashMessage::error("Description cannot be empty"),
)
.map(|_| redirect_to("/"))
.into_future(),
)
session::set_flash(&session, FlashMessage::error("Description cannot be empty"))?;
Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND))
} else {
Either::B(
web::block(move || db::create_task(params.into_inner().description, &pool))
.from_err()
.then(move |res| match res {
Ok(_) => {
session::set_flash(
&session,
FlashMessage::success("Task successfully added"),
)?;
Ok(redirect_to("/"))
}
Err(e) => Err(e),
}),
)
db::create_task(params.into_inner().description, &pool)
.await
.map_err(error::ErrorInternalServerError)?;

session::set_flash(&session, FlashMessage::success("Task successfully added"))?;

Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND))
}
}

#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub struct UpdateParams {
id: i32,
}

#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub struct UpdateForm {
_method: String,
}

pub fn update(
db: web::Data<db::PgPool>,
pub async fn update(
db: web::Data<PgPool>,
params: web::Path<UpdateParams>,
form: web::Form<UpdateForm>,
session: Session,
) -> impl Future<Item = HttpResponse, Error = Error> {
match form._method.as_ref() {
"put" => Either::A(Either::A(toggle(db, params))),
"delete" => Either::A(Either::B(delete(db, params, session))),
) -> Result<impl Responder, Error> {
Ok(web::Redirect::to(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);
Either::B(err(error::ErrorBadRequest(msg)))
let msg = format!("Unsupported HTTP method: {unsupported_method}");
return Err(error::ErrorBadRequest(msg));
}
}
})
.using_status_code(StatusCode::FOUND))
}

fn toggle(
pool: web::Data<db::PgPool>,
async fn toggle(
pool: web::Data<PgPool>,
params: web::Path<UpdateParams>,
) -> impl Future<Item = HttpResponse, Error = Error> {
web::block(move || db::toggle_task(params.id, &pool))
.from_err()
.then(move |res| match res {
Ok(_) => Ok(redirect_to("/")),
Err(e) => Err(e),
})
) -> Result<&'static str, Error> {
db::toggle_task(params.id, &pool)
.await
.map_err(error::ErrorInternalServerError)?;

Ok("/")
}

fn delete(
pool: web::Data<db::PgPool>,
async fn delete(
pool: web::Data<PgPool>,
params: web::Path<UpdateParams>,
session: Session,
) -> impl Future<Item = HttpResponse, Error = Error> {
web::block(move || db::delete_task(params.id, &pool))
.from_err()
.then(move |res| match res {
Ok(_) => {
session::set_flash(
&session,
FlashMessage::success("Task was deleted."),
)?;
Ok(redirect_to("/"))
}
Err(e) => Err(e),
})
}
) -> Result<&'static str, Error> {
db::delete_task(params.id, &pool)
.await
.map_err(error::ErrorInternalServerError)?;

session::set_flash(&session, FlashMessage::success("Task was deleted."))?;

fn redirect_to(location: &str) -> HttpResponse {
HttpResponse::Found()
.header(http::header::LOCATION, location)
.finish()
Ok("/")
}

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())
.respond_to(res.request())?;
Ok(ErrorHandlerResponse::Response(
res.into_response(new_resp.into_body()),
))
.customize()
.with_status(res.status())
.respond_to(res.request())
.map_into_boxed_body()
.map_into_right_body();

Ok(ErrorHandlerResponse::Response(res.into_response(new_resp)))
}

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())
.respond_to(res.request())?;
Ok(ErrorHandlerResponse::Response(
res.into_response(new_resp.into_body()),
))
.customize()
.with_status(res.status())
.respond_to(res.request())
.map_into_boxed_body()
.map_into_right_body();

Ok(ErrorHandlerResponse::Response(res.into_response(new_resp)))
}

pub fn internal_server_error<B>(
res: dev::ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>> {
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())
.respond_to(res.request())?;
Ok(ErrorHandlerResponse::Response(
res.into_response(new_resp.into_body()),
))
.customize()
.with_status(res.status())
.respond_to(res.request())
.map_into_boxed_body()
.map_into_right_body();

Ok(ErrorHandlerResponse::Response(res.into_response(new_resp)))
}
Loading

0 comments on commit 32afd45

Please sign in to comment.