1
0
mirror of https://github.com/actix/examples synced 2025-06-26 17:17:42 +02:00

Restructure folders (#411)

This commit is contained in:
Daniel T. Rodrigues
2021-02-25 21:57:58 -03:00
committed by GitHub
parent 9db98162b2
commit c3407627d0
334 changed files with 127 additions and 120 deletions

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,8 @@
[package]
name = "docker_sample"
version = "0.1.0"
authors = ["docker_sample <docker_sample@sample.com>"]
edition = "2018"
[dependencies]
actix-web = "3"

View File

@ -0,0 +1,24 @@
FROM rust:1-slim-buster AS base
ENV USER=root
WORKDIR /code
RUN cargo init
COPY Cargo.toml /code/Cargo.toml
RUN cargo fetch
COPY src /code/src
CMD [ "cargo", "test", "--offline" ]
FROM base AS builder
RUN cargo build --release --offline
FROM rust:1-slim-buster
COPY --from=builder /code/target/release/docker_sample /usr/bin/docker_sample
EXPOSE 5000
ENTRYPOINT [ "/usr/bin/docker_sample" ]

View File

@ -0,0 +1,22 @@
# Docker sample
## Build image
```shell
docker build -t docker_sample .
```
## Run built image
```shell
docker run -d -p 5000:5000 docker_sample
# and the server should start instantly
curl http://localhost:5000
```
## Running unit tests
```shell
docker build -t docker_sample:test --target base .
docker run --rm docker_sample:test
```

View File

@ -0,0 +1,26 @@
#[macro_use]
extern crate actix_web;
use actix_web::{App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn index() -> impl Responder {
println!("GET: /");
HttpResponse::Ok().body("Hello world!")
}
#[get("/again")]
async fn again() -> impl Responder {
println!("GET: /again");
HttpResponse::Ok().body("Hello world again!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Starting actix-web server");
HttpServer::new(|| App::new().service(index).service(again))
.bind("0.0.0.0:5000")?
.run()
.await
}

View File

@ -0,0 +1,11 @@
[package]
name = "error_handling"
version = "1.0.0"
authors = ["dowwie <dkcdkg@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
derive_more = "0.99.2"
rand = "0.7"
env_logger = "0.8"

View File

@ -0,0 +1 @@
This project illustrates custom error propagation through futures in actix-web

View File

@ -0,0 +1,102 @@
/*
The goal of this example is to show how to propagate a custom error type,
to a web handler that will evaluate the type of error that
was raised and return an appropriate HTTPResponse.
This example uses a 50/50 chance of returning 200 Ok, otherwise one of four possible
http errors will be chosen, each with an equal chance of being selected:
1. 403 Forbidden
2. 401 Unauthorized
3. 500 InternalServerError
4. 400 BadRequest
*/
use actix_web::{web, App, Error, HttpResponse, HttpServer, ResponseError};
use derive_more::Display; // naming it clearly for illustration purposes
use rand::{
distributions::{Distribution, Standard},
thread_rng, Rng,
};
#[derive(Debug, Display)]
pub enum CustomError {
#[display(fmt = "Custom Error 1")]
CustomOne,
#[display(fmt = "Custom Error 2")]
CustomTwo,
#[display(fmt = "Custom Error 3")]
CustomThree,
#[display(fmt = "Custom Error 4")]
CustomFour,
}
impl Distribution<CustomError> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> CustomError {
match rng.gen_range(0, 4) {
0 => CustomError::CustomOne,
1 => CustomError::CustomTwo,
2 => CustomError::CustomThree,
_ => CustomError::CustomFour,
}
}
}
/// Actix web uses `ResponseError` for conversion of errors to a response
impl ResponseError for CustomError {
fn error_response(&self) -> HttpResponse {
match self {
CustomError::CustomOne => {
println!("do some stuff related to CustomOne error");
HttpResponse::Forbidden().finish()
}
CustomError::CustomTwo => {
println!("do some stuff related to CustomTwo error");
HttpResponse::Unauthorized().finish()
}
CustomError::CustomThree => {
println!("do some stuff related to CustomThree error");
HttpResponse::InternalServerError().finish()
}
_ => {
println!("do some stuff related to CustomFour error");
HttpResponse::BadRequest().finish()
}
}
}
}
/// randomly returns either () or one of the 4 CustomError variants
async fn do_something_random() -> Result<(), CustomError> {
let mut rng = thread_rng();
// 20% chance that () will be returned by this function
if rng.gen_bool(2.0 / 10.0) {
Ok(())
} else {
Err(rand::random::<CustomError>())
}
}
async fn do_something() -> Result<HttpResponse, Error> {
do_something_random().await?;
Ok(HttpResponse::Ok().body("Nothing interesting happened. Try again."))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(move || {
App::new()
.service(web::resource("/something").route(web::get().to(do_something)))
})
.bind("127.0.0.1:8088")?
.run()
.await
}

View File

@ -0,0 +1,12 @@
[package]
name = "hello-world"
version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
env_logger = "0.8"
[dev-dependencies]
actix-rt = "1"

View File

@ -0,0 +1,50 @@
use actix_web::{middleware, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str {
println!("REQ: {:?}", req);
"Hello world!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// enable logger
.wrap(middleware::Logger::default())
.service(web::resource("/index.html").to(|| async { "Hello world!" }))
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::dev::Service;
use actix_web::{http, test, web, App, Error};
#[actix_rt::test]
async fn test_index() -> Result<(), Error> {
let app = App::new().route("/", web::get().to(index));
let mut app = test::init_service(app).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = app.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let response_body = match resp.response().body().as_ref() {
Some(actix_web::body::Body::Bytes(bytes)) => bytes,
_ => panic!("Response error"),
};
assert_eq!(response_body, r##"Hello world!"##);
Ok(())
}
}

View File

@ -0,0 +1,10 @@
[package]
name = "http-proxy"
version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rotem Yaari <vmalloc@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = { version = "3", features = ["openssl"] }
clap = "2.33"
url = "2.0"

View File

@ -0,0 +1,10 @@
## HTTP Full proxy example
This is a relatively simple HTTP proxy, forwarding HTTP requests to another HTTP server, including
request body, headers, and streaming uploads.
To start:
``` shell
cargo run <listen addr> <listen port> <forward addr> <forward port>
```

View File

@ -0,0 +1,104 @@
use std::net::ToSocketAddrs;
use actix_web::client::Client;
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
use clap::{value_t, Arg};
use url::Url;
async fn forward(
req: HttpRequest,
body: web::Bytes,
url: web::Data<Url>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let mut new_url = url.get_ref().clone();
new_url.set_path(req.uri().path());
new_url.set_query(req.uri().query());
// TODO: This forwarded implementation is incomplete as it only handles the inofficial
// X-Forwarded-For header but not the official Forwarded one.
let forwarded_req = client
.request_from(new_url.as_str(), req.head())
.no_decompress();
let forwarded_req = if let Some(addr) = req.head().peer_addr {
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip()))
} else {
forwarded_req
};
let mut res = forwarded_req.send_body(body).await.map_err(Error::from)?;
let mut client_resp = HttpResponse::build(res.status());
// Remove `Connection` as per
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives
for (header_name, header_value) in
res.headers().iter().filter(|(h, _)| *h != "connection")
{
client_resp.header(header_name.clone(), header_value.clone());
}
Ok(client_resp.body(res.body().await?))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let matches = clap::App::new("HTTP Proxy")
.arg(
Arg::with_name("listen_addr")
.takes_value(true)
.value_name("LISTEN ADDR")
.index(1)
.required(true),
)
.arg(
Arg::with_name("listen_port")
.takes_value(true)
.value_name("LISTEN PORT")
.index(2)
.required(true),
)
.arg(
Arg::with_name("forward_addr")
.takes_value(true)
.value_name("FWD ADDR")
.index(3)
.required(true),
)
.arg(
Arg::with_name("forward_port")
.takes_value(true)
.value_name("FWD PORT")
.index(4)
.required(true),
)
.get_matches();
let listen_addr = matches.value_of("listen_addr").unwrap();
let listen_port = value_t!(matches, "listen_port", u16).unwrap_or_else(|e| e.exit());
let forwarded_addr = matches.value_of("forward_addr").unwrap();
let forwarded_port =
value_t!(matches, "forward_port", u16).unwrap_or_else(|e| e.exit());
let forward_url = Url::parse(&format!(
"http://{}",
(forwarded_addr, forwarded_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap()
))
.unwrap();
HttpServer::new(move || {
App::new()
.data(Client::new())
.data(forward_url.clone())
.wrap(middleware::Logger::default())
.default_service(web::route().to(forward))
})
.bind((listen_addr, listen_port))?
.system_exit()
.run()
.await
}

View File

@ -0,0 +1,14 @@
[package]
name = "awc_examples"
version = "2.0.0"
authors = ["dowwie <dkcdkg@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = { version = "3", features = ["openssl"] }
env_logger = "0.8"
futures = "0.3.1"
serde = { version = "1.0.43", features = ["derive"] }
serde_json = "1.0.16"
validator = "0.10"
validator_derive = "0.10"

View File

@ -0,0 +1,19 @@
This is a contrived example intended to illustrate a few important actix-web features.
*Imagine* that you have a process that involves 3 steps. The steps here
are dumb in that they do nothing other than call an
httpbin endpoint that returns the json that was posted to it. The intent here
is to illustrate how to chain these steps together as futures and return
a final result in a response.
Actix-web features illustrated here include:
1. handling json input param
2. validating user-submitted parameters using the 'validator' crate
2. actix-web client features:
- POSTing json body
3. chaining futures into a single response used by an asynch endpoint
Example query from the command line using httpie:
```echo '{"id":"1", "name": "JohnDoe"}' | http 127.0.0.1:8080/something```

View File

@ -0,0 +1,98 @@
// This is a contrived example intended to illustrate actix-web features.
// *Imagine* that you have a process that involves 3 steps. The steps here
// are dumb in that they do nothing other than call an
// httpbin endpoint that returns the json that was posted to it. The intent
// here is to illustrate how to chain these steps together as futures and return
// a final result in a response.
//
// Actix-web features illustrated here include:
// 1. handling json input param
// 2. validating user-submitted parameters using the 'validator' crate
// 2. actix-web client features:
// - POSTing json body
// 3. chaining futures into a single response used by an async endpoint
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io;
use actix_web::{
client::Client,
error::ErrorBadRequest,
web::{self, BytesMut},
App, Error, HttpResponse, HttpServer,
};
use futures::StreamExt;
use validator::Validate;
use validator_derive::Validate;
#[derive(Debug, Validate, Deserialize, Serialize)]
struct SomeData {
#[validate(length(min = 1, max = 1000000))]
id: String,
#[validate(length(min = 1, max = 100))]
name: String,
}
#[derive(Debug, Deserialize)]
struct HttpBinResponse {
args: HashMap<String, String>,
data: String,
files: HashMap<String, String>,
form: HashMap<String, String>,
headers: HashMap<String, String>,
json: SomeData,
origin: String,
url: String,
}
/// validate data, post json to httpbin, get it back in the response body, return deserialized
async fn step_x(data: SomeData, client: &Client) -> Result<SomeData, Error> {
// validate data
data.validate().map_err(ErrorBadRequest)?;
let mut res = client
.post("https://httpbin.org/post")
.send_json(&data)
.await
.map_err(Error::from)?; // <- convert SendRequestError to an Error
let mut body = BytesMut::new();
while let Some(chunk) = res.next().await {
body.extend_from_slice(&chunk?);
}
let body: HttpBinResponse = serde_json::from_slice(&body).unwrap();
Ok(body.json)
}
async fn create_something(
some_data: web::Json<SomeData>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let some_data_2 = step_x(some_data.into_inner(), &client).await?;
let some_data_3 = step_x(some_data_2, &client).await?;
let d = step_x(some_data_3, &client).await?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::to_string(&d).unwrap()))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
let endpoint = "127.0.0.1:8080";
println!("Starting server at: {:?}", endpoint);
HttpServer::new(|| {
App::new()
.data(Client::default())
.service(web::resource("/something").route(web::post().to(create_something)))
})
.bind(endpoint)?
.run()
.await
}

View File

@ -0,0 +1,13 @@
[package]
name = "middleware-example"
version = "2.0.0"
authors = ["Gorm Casper <gcasper@gmail.com>", "Sven-Hendrik Haase <svenstaro@gmail.com>"]
edition = "2018"
[dependencies]
actix-service = "1"
actix-web = "3"
env_logger = "0.8"
futures = "0.3.1"
pin-project = "0.4"

View File

@ -0,0 +1,33 @@
# middleware examples
This example showcases a bunch of different uses of middlewares. See also the [Middleware guide](https://actix.rs/docs/middleware/).
## Usage
```bash
cd middleware
cargo run
# Started http server: 127.0.0.1:8080
```
Look in `src/main.rs` and comment the different middlewares in/out to see how
they function.
## Middlewares
### redirect::CheckLogin
A middleware implementing a request guard which sketches a rough approximation of what a login could look like.
### read_request_body::Logging
A middleware demonstrating how to read out the incoming request body.
### read_response_body::Logging
A middleware demonstrating how to read out the outgoing response body.
### simple::SayHi
A minimal middleware demonstrating the sequence of operations in an actix middleware.
There is a second version of the same middleware using `wrap_fn` which shows how easily a middleware can be implemented in actix.

View File

@ -0,0 +1,45 @@
#![allow(clippy::type_complexity)]
use actix_service::Service;
use actix_web::{web, App, HttpServer};
use futures::future::FutureExt;
#[allow(dead_code)]
mod read_request_body;
#[allow(dead_code)]
mod read_response_body;
#[allow(dead_code)]
mod redirect;
#[allow(dead_code)]
mod simple;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=debug");
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(redirect::CheckLogin)
.wrap(read_request_body::Logging)
.wrap(read_response_body::Logging)
.wrap(simple::SayHi)
.wrap_fn(|req, srv| {
println!("Hi from start. You requested: {}", req.path());
srv.call(req).map(|res| {
println!("Hi from response");
res
})
})
.service(web::resource("/login").to(|| async {
"You are on /login. Go to src/redirect.rs to change this behavior."
}))
.service(web::resource("/").to(|| async {
"Hello, middleware! Check the console where the server is run."
}))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@ -0,0 +1,72 @@
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::web::BytesMut;
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error, HttpMessage};
use futures::future::{ok, Future, Ready};
use futures::stream::StreamExt;
pub struct Logging;
impl<S: 'static, B> Transform<S> for Logging
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = LoggingMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(LoggingMiddleware {
service: Rc::new(RefCell::new(service)),
})
}
}
pub struct LoggingMiddleware<S> {
// This is special: We need this to avoid lifetime issues.
service: Rc<RefCell<S>>,
}
impl<S, B> Service for LoggingMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>
+ 'static,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
let mut svc = self.service.clone();
Box::pin(async move {
let mut body = BytesMut::new();
let mut stream = req.take_payload();
while let Some(chunk) = stream.next().await {
body.extend_from_slice(&chunk?);
}
println!("request body: {:?}", body);
let res = svc.call(req).await?;
println!("response: {:?}", res.headers());
Ok(res)
})
}
}

View File

@ -0,0 +1,124 @@
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::body::{BodySize, MessageBody, ResponseBody};
use actix_web::web::{Bytes, BytesMut};
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error};
use futures::future::{ok, Ready};
pub struct Logging;
impl<S: 'static, B> Transform<S> for Logging
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<BodyLogger<B>>;
type Error = Error;
type InitError = ();
type Transform = LoggingMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(LoggingMiddleware { service })
}
}
pub struct LoggingMiddleware<S> {
service: S,
}
impl<S, B> Service for LoggingMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{
type Request = ServiceRequest;
type Response = ServiceResponse<BodyLogger<B>>;
type Error = Error;
type Future = WrapperStream<S, B>;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
WrapperStream {
fut: self.service.call(req),
_t: PhantomData,
}
}
}
#[pin_project::pin_project]
pub struct WrapperStream<S, B>
where
B: MessageBody,
S: Service,
{
#[pin]
fut: S::Future,
_t: PhantomData<(B,)>,
}
impl<S, B> Future for WrapperStream<S, B>
where
B: MessageBody,
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Output = Result<ServiceResponse<BodyLogger<B>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = futures::ready!(self.project().fut.poll(cx));
Poll::Ready(res.map(|res| {
res.map_body(move |_, body| {
ResponseBody::Body(BodyLogger {
body,
body_accum: BytesMut::new(),
})
})
}))
}
}
#[pin_project::pin_project(PinnedDrop)]
pub struct BodyLogger<B> {
#[pin]
body: ResponseBody<B>,
body_accum: BytesMut,
}
#[pin_project::pinned_drop]
impl<B> PinnedDrop for BodyLogger<B> {
fn drop(self: Pin<&mut Self>) {
println!("response body: {:?}", self.body_accum);
}
}
impl<B: MessageBody> MessageBody for BodyLogger<B> {
fn size(&self) -> BodySize {
self.body.size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let this = self.project();
match this.body.poll_next(cx) {
Poll::Ready(Some(Ok(chunk))) => {
this.body_accum.extend_from_slice(&chunk);
Poll::Ready(Some(Ok(chunk)))
}
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}

View File

@ -0,0 +1,65 @@
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{http, Error, HttpResponse};
use futures::future::{ok, Either, Ready};
pub struct CheckLogin;
impl<S, B> Transform<S> for CheckLogin
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = CheckLoginMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(CheckLoginMiddleware { service })
}
}
pub struct CheckLoginMiddleware<S> {
service: S,
}
impl<S, B> Service for CheckLoginMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
// We only need to hook into the `start` for this middleware.
let is_logged_in = false; // Change this to see the change in outcome in the browser
if is_logged_in {
Either::Left(self.service.call(req))
} else {
// Don't forward to /login if we are already on /login
if req.path() == "/login" {
Either::Left(self.service.call(req))
} else {
Either::Right(ok(req.into_response(
HttpResponse::Found()
.header(http::header::LOCATION, "/login")
.finish()
.into_body(),
)))
}
}
}
}

View File

@ -0,0 +1,67 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error};
use futures::future::{ok, Ready};
use futures::Future;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct SayHi;
// Middleware factory is `Transform` trait from actix-service crate
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S> for SayHi
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = SayHiMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(SayHiMiddleware { service })
}
}
pub struct SayHiMiddleware<S> {
service: S,
}
impl<S, B> Service for SayHiMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
println!("Hi from start. You requested: {}", req.path());
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
println!("Hi from response");
Ok(res)
})
}
}

View File

@ -0,0 +1,18 @@
[package]
name = "async_ex2"
version = "0.1.0"
authors = ["dowwie <dkcdkg@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = { version = "3", features = ["openssl"] }
actix-service = "1.0.0"
bytes = "0.5.3"
env_logger = "0.8"
futures = "0.3.1"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "1.0.39"
time = "0.1.42"
[dev-dependencies]
actix-rt = "1"

View File

@ -0,0 +1,3 @@
This example illustrates how to use nested resource registration through application-level configuration.
The endpoints do nothing.

View File

@ -0,0 +1,36 @@
use actix_web::web;
use crate::handlers::{parts, products};
pub fn config_app(cfg: &mut web::ServiceConfig) {
// domain includes: /products/{product_id}/parts/{part_id}
cfg.service(
web::scope("/products")
.service(
web::resource("")
.route(web::get().to(products::get_products))
.route(web::post().to(products::add_product)),
)
.service(
web::scope("/{product_id}")
.service(
web::resource("")
.route(web::get().to(products::get_product_detail))
.route(web::delete().to(products::remove_product)),
)
.service(
web::scope("/parts")
.service(
web::resource("")
.route(web::get().to(parts::get_parts))
.route(web::post().to(parts::add_part)),
)
.service(
web::resource("/{part_id}")
.route(web::get().to(parts::get_part_detail))
.route(web::delete().to(parts::remove_part)),
),
),
),
);
}

View File

@ -0,0 +1,18 @@
use actix_web::{middleware, App, HttpServer};
use async_ex2::appconfig::config_app;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
.configure(config_app)
.wrap(middleware::Logger::default())
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Product {
id: Option<i64>,
product_type: Option<String>,
name: Option<String>,
}
#[derive(Deserialize, Serialize)]
pub struct Part {
id: Option<i64>,
part_type: Option<String>,
name: Option<String>,
}

View File

@ -0,0 +1,2 @@
pub mod parts;
pub mod products;

View File

@ -0,0 +1,19 @@
use actix_web::{web, Error, HttpResponse};
use crate::common::{Part, Product};
pub async fn get_parts(_query: web::Query<Option<Part>>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
pub async fn add_part(_new_part: web::Json<Product>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
pub async fn get_part_detail(_id: web::Path<String>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
pub async fn remove_part(_id: web::Path<String>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}

View File

@ -0,0 +1,50 @@
use actix_web::{web, Error, HttpResponse};
use crate::common::{Part, Product};
pub async fn get_products(
_query: web::Query<Option<Part>>,
) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
pub async fn add_product(
_new_product: web::Json<Product>,
) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
pub async fn get_product_detail(_id: web::Path<String>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
pub async fn remove_product(_id: web::Path<String>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().finish())
}
#[cfg(test)]
mod tests {
use crate::appconfig::config_app;
use actix_service::Service;
use actix_web::{
http::{header, StatusCode},
test, App,
};
#[actix_rt::test]
async fn test_add_product() {
let mut app = test::init_service(App::new().configure(config_app)).await;
let payload = r#"{"id":12345,"product_type":"fancy","name":"test"}"#.as_bytes();
let req = test::TestRequest::post()
.uri("/products")
.header(header::CONTENT_TYPE, "application/json")
.set_payload(payload)
.to_request();
let resp = app.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@ -0,0 +1,3 @@
pub mod appconfig;
pub mod common;
pub mod handlers;

View File

@ -0,0 +1,10 @@
[package]
name = "run-in-thread"
version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
edition = "2018"
description = "Run actix-web in separate thread"
[dependencies]
actix-web = "3"
env_logger = "0.8"

View File

@ -0,0 +1,51 @@
use std::sync::mpsc;
use std::{thread, time};
use actix_web::{dev::Server, middleware, rt, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str {
println!("REQ: {:?}", req);
"Hello world!"
}
fn run_app(tx: mpsc::Sender<Server>) -> std::io::Result<()> {
let mut sys = rt::System::new("test");
// srv is server controller type, `dev::Server`
let srv = HttpServer::new(|| {
App::new()
// enable logger
.wrap(middleware::Logger::default())
.service(web::resource("/index.html").to(|| async { "Hello world!" }))
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run();
// send server controller to main thread
let _ = tx.send(srv.clone());
// run future
sys.block_on(srv)
}
fn main() {
std::env::set_var("RUST_LOG", "actix_web=info,actix_server=trace");
env_logger::init();
let (tx, rx) = mpsc::channel();
println!("START SERVER");
thread::spawn(move || {
let _ = run_app(tx);
});
let srv = rx.recv().unwrap();
println!("WAITING 10 SECONDS");
thread::sleep(time::Duration::from_secs(10));
println!("STOPPING SERVER");
// init stop server and wait until server gracefully exit
rt::System::new("").block_on(srv.stop(true));
}

View File

@ -0,0 +1,12 @@
[package]
name = "shutdown-server"
version = "2.0.0"
authors = ["Rob Ede <robjtede@icloud.com>"]
edition = "2018"
description = "Send a request to the server to shut it down"
[dependencies]
actix-web = "3"
env_logger = "0.8"
futures = "0.3"
tokio = { version = "0.2", features = ["signal"] }

View File

@ -0,0 +1,28 @@
# shutdown-server
Demonstrates how to shutdown the web server in a couple of ways:
1. remotely, via http request
- Created in response to actix/actix-web#1315
2. sending a SIGINT signal to the server (control-c)
- actix-server natively supports SIGINT
## Usage
### Running The Server
```bash
cargo run --bin shutdown-server
# Starting 8 workers
# Starting "actix-web-service-127.0.0.1:8080" service on 127.0.0.1:8080
```
### Available Routes
- [GET /hello](http://localhost:8080/hello)
- Regular hello world route
- [POST /stop](http://localhost:8080/stop)
- Calling this will shutdown the server and exit

View File

@ -0,0 +1,52 @@
use actix_web::{get, middleware, post, web, App, HttpResponse, HttpServer};
use futures::executor;
use std::{sync::mpsc, thread};
#[get("/hello")]
async fn hello() -> &'static str {
"Hello world!"
}
#[post("/stop")]
async fn stop(stopper: web::Data<mpsc::Sender<()>>) -> HttpResponse {
// make request that sends message through the Sender
stopper.send(()).unwrap();
HttpResponse::NoContent().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_server=debug,actix_web=debug");
env_logger::init();
// create a channel
let (tx, rx) = mpsc::channel::<()>();
let bind = "127.0.0.1:8080";
// start server as normal but don't .await after .run() yet
let server = HttpServer::new(move || {
// give the server a Sender in .data
App::new()
.data(tx.clone())
.wrap(middleware::Logger::default())
.service(hello)
.service(stop)
})
.bind(&bind)?
.run();
// clone the Server handle
let srv = server.clone();
thread::spawn(move || {
// wait for shutdown signal
rx.recv().unwrap();
// stop server gracefully
executor::block_on(srv.stop(true))
});
// run server
server.await
}

9
basics/state/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "state"
version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
env_logger = "0.8"

15
basics/state/README.md Normal file
View File

@ -0,0 +1,15 @@
# state
## Usage
### server
```bash
cd examples/state
cargo run
# Started http server: 127.0.0.1:8080
```
### web client
- [http://localhost:8080/](http://localhost:8080/)

78
basics/state/src/main.rs Normal file
View File

@ -0,0 +1,78 @@
//! Application may have multiple data objects that are shared across
//! all handlers within same Application.
//!
//! For global shared state, we wrap our state in a `actix_web::web::Data` and move it into
//! the factory closure. The closure is called once-per-thread, and we clone our state
//! and attach to each instance of the `App` with `.app_data(state.clone())`.
//!
//! For thread-local state, we construct our state within the factory closure and attach to
//! the app with `.data(state)`.
//!
//! We retrieve our app state within our handlers with a `state: Data<...>` argument.
//!
//! By default, `actix-web` runs one `App` per logical cpu core.
//! When running on <N> cores, we see that the example will increment `counter1` (global state via
//! Mutex) and `counter3` (global state via Atomic variable) each time the endpoint is called,
//! but only appear to increment `counter2` every Nth time on average (thread-local state). This
//! is because the workload is being shared equally among cores.
//!
//! Check [user guide](https://actix.rs/docs/application/#state) for more info.
use std::cell::Cell;
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use actix_web::{middleware, web, App, HttpRequest, HttpResponse, HttpServer};
/// simple handle
async fn index(
counter1: web::Data<Mutex<usize>>,
counter2: web::Data<Cell<u32>>,
counter3: web::Data<AtomicUsize>,
req: HttpRequest,
) -> HttpResponse {
println!("{:?}", req);
// Increment the counters
*counter1.lock().unwrap() += 1;
counter2.set(counter2.get() + 1);
counter3.fetch_add(1, Ordering::SeqCst);
let body = format!(
"global mutex counter: {}, local counter: {}, global atomic counter: {}",
*counter1.lock().unwrap(),
counter2.get(),
counter3.load(Ordering::SeqCst),
);
HttpResponse::Ok().body(body)
}
#[actix_web::main]
async fn main() -> io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
// Create some global state prior to building the server
#[allow(clippy::mutex_atomic)] // it's intentional.
let counter1 = web::Data::new(Mutex::new(0usize));
let counter3 = web::Data::new(AtomicUsize::new(0usize));
// move is necessary to give closure below ownership of counter1
HttpServer::new(move || {
// Create some thread-local state
let counter2 = Cell::new(0u32);
App::new()
.app_data(counter1.clone()) // add shared state
.app_data(counter3.clone()) // add shared state
.data(counter2) // add thread-local state
// enable logger
.wrap(middleware::Logger::default())
// register simple handler
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

2
basics/static_index/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -0,0 +1,10 @@
[package]
name = "static_index"
version = "2.0.0"
authors = ["Jose Marinez <digeratus@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
actix-files = "0.3"
env_logger = "0.8"

View File

@ -0,0 +1,16 @@
# static_index
Demonstrates how to serve static files. Inside the `./static` folder you will find 2 subfolders:
* `root`: A tree of files that will be served at the web root `/`. This includes the `css` and `js` folders, each
containing an example file.
* `images`: A list of images that will be served at `/images` path, with file listing enabled.
## Usage
```bash
$ cd examples/static_index
$ cargo run
```
This will start the server on port 8080, it can be viewed at [http://localhost:8080](http://localhost:8080).

View File

@ -0,0 +1,24 @@
use actix_files::Files;
use actix_web::{middleware, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// Enable the logger.
.wrap(middleware::Logger::default())
// We allow the visitor to see an index of the images at `/images`.
.service(Files::new("/images", "static/images/").show_files_listing())
// Serve a tree of static files at the web root and specify the index file.
// Note that the root path should always be defined as the last item. The paths are
// resolved in the order they are defined. If this would be placed before the `/images`
// path then the service for the static images would never be reached.
.service(Files::new("/", "./static/root/").index_file("index.html"))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,11 @@
body {
text-align: center;
}
h1 {
font-family: sans-serif;
}
img {
transition: .5s ease-in-out;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<html>
<head>
<link rel="stylesheet" href="css/example.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="/js/example.js"></script>
</head>
<body>
<h1>Actix-web</h1>
<img src="/images/logo.png" alt="Actix logo" />
</body>
</html>

View File

@ -0,0 +1,7 @@
jQuery(document).ready(function () {
let rotation = 0;
jQuery("img").click(function () {
rotation += 360;
jQuery("img").css({'transform': 'rotate(' + rotation + 'deg)'});
});
});

1
basics/todo/.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgres://localhost/actix_todo

22
basics/todo/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
authors = ["Dan Munckton <dangit@munckfish.net>"]
name = "actix-todo"
version = "2.0.0"
edition = "2018"
[dependencies]
actix-web = "3"
actix-files = "0.3"
actix-session = "0.4"
dotenv = "0.15"
env_logger = "0.8"
futures = "0.3.1"
log = "0.4.3"
serde = { version = "1.0.69", features = ["derive"] }
serde_json = "1.0.22"
tera = "1.5.0"
[dependencies.diesel]
features = ["postgres", "r2d2"]
version = "1.3.2"

48
basics/todo/README.md Normal file
View File

@ -0,0 +1,48 @@
# actix-todo
A port of the [Rocket Todo example](https://github.com/SergioBenitez/Rocket/tree/master/examples/todo) into [actix-web](https://actix.rs/). Except this uses PostgreSQL instead of SQLite.
# Usage
## Prerequisites
* Rust >= 1.26
* PostgreSQL >= 9.5
## Change into the project sub-directory
All instructions assume you have changed into this folder:
```bash
cd examples/todo
```
## Set up the database
Install the [diesel](http://diesel.rs) command-line tool including the `postgres` feature:
```bash
cargo install diesel_cli --no-default-features --features postgres
```
Check the contents of the `.env` file. If your database requires a password, update `DATABASE_URL` to be of the form:
```.env
DATABASE_URL=postgres://username:password@localhost/actix_todo
```
Then to create and set-up the database run:
```bash
diesel database setup
```
## Run the application
To run the application execute:
```bash
cargo run
```
Then to view it in your browser navigate to: [http://localhost:8088/](http://localhost:8088/)

5
basics/todo/diesel.toml Normal file
View File

@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1 @@
DROP TABLE tasks

View File

@ -0,0 +1,5 @@
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
description VARCHAR NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 'f'
);

136
basics/todo/src/api.rs Normal file
View 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
View 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
View 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
View 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)
}
}

View File

@ -0,0 +1,7 @@
table! {
tasks (id) {
id -> Int4,
description -> Varchar,
completed -> Bool,
}
}

View 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(),
}
}
}

427
basics/todo/static/css/normalize.css vendored Normal file
View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

421
basics/todo/static/css/skeleton.css vendored Normal file
View File

@ -0,0 +1,421 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
button.primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
button.primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
button.primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@ -0,0 +1,58 @@
.field-error {
border: 1px solid #ff0000 !important;
}
.field-error-msg {
color: #ff0000;
display: block;
margin: -10px 0 10px 0;
}
.field-success {
border: 1px solid #5AB953 !important;
}
.field-success-msg {
color: #5AB953;
display: block;
margin: -10px 0 10px 0;
}
span.completed {
text-decoration: line-through;
}
form.inline {
display: inline;
}
form.link,
button.link {
display: inline;
color: #1EAEDB;
border: none;
outline: none;
background: none;
cursor: pointer;
padding: 0;
margin: 0 0 0 0;
height: inherit;
text-decoration: underline;
font-size: inherit;
text-transform: none;
font-weight: normal;
line-height: inherit;
letter-spacing: inherit;
}
form.link:hover, button.link:hover {
color: #0FA0CE;
}
button.small {
height: 20px;
padding: 0 10px;
font-size: 10px;
line-height: 20px;
margin: 0 2.5px;
}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>The server could not understand the request (400)</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<div class="row">
<h1>The server could not understand the request</h1>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>The page you were looking for doesn't exist (404)</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<div class="row">
<h1>The page you were looking for doesn't exist.</h1>
<p>You may have mistyped the address or the page may have moved.</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Ooops (500)</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<div class="row">
<h1>Ooops ...</h1>
<p>How embarrassing!</p>
<p>Looks like something weird happened while processing your request.</p>
<p>Please try again in a few moments.</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Actix Todo Example</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<p><!-- nothing to see here --></p>
<div class="row">
<h4>Actix Todo</h4>
<form action="/todo" method="post">
<div class="ten columns">
<input type="text" placeholder="enter a task description ..."
name="description" id="description" value="" autofocus
class="u-full-width {% if msg %}field-{{msg.0}}{% endif %}" />
{% if msg %}
<small class="field-{{msg.0}}-msg">
{{msg.1}}
</small>
{% endif %}
</div>
<div class="two columns">
<input type="submit" value="add task">
</div>
</form>
</div>
<div class="row">
<div class="twelve columns">
<ul>
{% for task in tasks %}
<li>
{% if task.completed %}
<span class="completed">{{task.description}}</span>
<form action="/todo/{{task.id}}" class="inline" method="post">
<input type="hidden" name="_method" value="put" />
<button type="submit" class="small">undo</button>
</form>
<form action="/todo/{{task.id}}" method="post" class="inline">
<input type="hidden" name="_method" value="delete" />
<button type="submit" class="primary small">delete</button>
</form>
{% else %}
<form action="/todo/{{task.id}}" class="link" method="post">
<input type="hidden" name="_method" value="put" />
<button type="submit" class="link">{{task.description}}</button>
</form>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</body>
</html>