mirror of
https://github.com/actix/examples
synced 2025-06-26 17:17:42 +02:00
Restructure folders (#411)
This commit is contained in:
committed by
GitHub
parent
9db98162b2
commit
c3407627d0
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
8
basics/docker_sample/Cargo.toml
Normal file
8
basics/docker_sample/Cargo.toml
Normal 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"
|
24
basics/docker_sample/Dockerfile
Normal file
24
basics/docker_sample/Dockerfile
Normal 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" ]
|
22
basics/docker_sample/README.MD
Normal file
22
basics/docker_sample/README.MD
Normal 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
|
||||
```
|
26
basics/docker_sample/src/main.rs
Normal file
26
basics/docker_sample/src/main.rs
Normal 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
|
||||
}
|
11
basics/error_handling/Cargo.toml
Normal file
11
basics/error_handling/Cargo.toml
Normal 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"
|
1
basics/error_handling/README.md
Normal file
1
basics/error_handling/README.md
Normal file
@ -0,0 +1 @@
|
||||
This project illustrates custom error propagation through futures in actix-web
|
102
basics/error_handling/src/main.rs
Normal file
102
basics/error_handling/src/main.rs
Normal 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
|
||||
}
|
12
basics/hello-world/Cargo.toml
Normal file
12
basics/hello-world/Cargo.toml
Normal 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"
|
50
basics/hello-world/src/main.rs
Normal file
50
basics/hello-world/src/main.rs
Normal 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(())
|
||||
}
|
||||
}
|
10
basics/http-proxy/Cargo.toml
Normal file
10
basics/http-proxy/Cargo.toml
Normal 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"
|
10
basics/http-proxy/README.md
Normal file
10
basics/http-proxy/README.md
Normal 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>
|
||||
```
|
104
basics/http-proxy/src/main.rs
Normal file
104
basics/http-proxy/src/main.rs
Normal 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
|
||||
}
|
14
basics/json-validation/Cargo.toml
Normal file
14
basics/json-validation/Cargo.toml
Normal 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"
|
19
basics/json-validation/README.md
Normal file
19
basics/json-validation/README.md
Normal 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```
|
98
basics/json-validation/src/main.rs
Normal file
98
basics/json-validation/src/main.rs
Normal 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
|
||||
}
|
13
basics/middleware/Cargo.toml
Normal file
13
basics/middleware/Cargo.toml
Normal 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"
|
33
basics/middleware/README.md
Normal file
33
basics/middleware/README.md
Normal 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.
|
45
basics/middleware/src/main.rs
Normal file
45
basics/middleware/src/main.rs
Normal 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
|
||||
}
|
72
basics/middleware/src/read_request_body.rs
Normal file
72
basics/middleware/src/read_request_body.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
124
basics/middleware/src/read_response_body.rs
Normal file
124
basics/middleware/src/read_response_body.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
65
basics/middleware/src/redirect.rs
Normal file
65
basics/middleware/src/redirect.rs
Normal 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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
basics/middleware/src/simple.rs
Normal file
67
basics/middleware/src/simple.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
18
basics/nested-routing/Cargo.toml
Normal file
18
basics/nested-routing/Cargo.toml
Normal 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"
|
3
basics/nested-routing/README.md
Normal file
3
basics/nested-routing/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
This example illustrates how to use nested resource registration through application-level configuration.
|
||||
The endpoints do nothing.
|
||||
|
36
basics/nested-routing/src/appconfig.rs
Normal file
36
basics/nested-routing/src/appconfig.rs
Normal 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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
18
basics/nested-routing/src/bin/main.rs
Normal file
18
basics/nested-routing/src/bin/main.rs
Normal 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
|
||||
}
|
15
basics/nested-routing/src/common.rs
Normal file
15
basics/nested-routing/src/common.rs
Normal 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>,
|
||||
}
|
2
basics/nested-routing/src/handlers/mod.rs
Normal file
2
basics/nested-routing/src/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod parts;
|
||||
pub mod products;
|
19
basics/nested-routing/src/handlers/parts.rs
Normal file
19
basics/nested-routing/src/handlers/parts.rs
Normal 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())
|
||||
}
|
50
basics/nested-routing/src/handlers/products.rs
Normal file
50
basics/nested-routing/src/handlers/products.rs
Normal 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);
|
||||
}
|
||||
}
|
3
basics/nested-routing/src/lib.rs
Normal file
3
basics/nested-routing/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod appconfig;
|
||||
pub mod common;
|
||||
pub mod handlers;
|
10
basics/run-in-thread/Cargo.toml
Normal file
10
basics/run-in-thread/Cargo.toml
Normal 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"
|
51
basics/run-in-thread/src/main.rs
Normal file
51
basics/run-in-thread/src/main.rs
Normal 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));
|
||||
}
|
12
basics/shutdown-server/Cargo.toml
Normal file
12
basics/shutdown-server/Cargo.toml
Normal 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"] }
|
28
basics/shutdown-server/README.md
Normal file
28
basics/shutdown-server/README.md
Normal 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
|
52
basics/shutdown-server/src/main.rs
Normal file
52
basics/shutdown-server/src/main.rs
Normal 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
9
basics/state/Cargo.toml
Normal 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
15
basics/state/README.md
Normal 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
78
basics/state/src/main.rs
Normal 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
2
basics/static_index/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
10
basics/static_index/Cargo.toml
Normal file
10
basics/static_index/Cargo.toml
Normal 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"
|
16
basics/static_index/README.md
Normal file
16
basics/static_index/README.md
Normal 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).
|
24
basics/static_index/src/main.rs
Normal file
24
basics/static_index/src/main.rs
Normal 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
|
||||
}
|
BIN
basics/static_index/static/images/logo.png
Normal file
BIN
basics/static_index/static/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
11
basics/static_index/static/root/css/example.css
Normal file
11
basics/static_index/static/root/css/example.css
Normal file
@ -0,0 +1,11 @@
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
img {
|
||||
transition: .5s ease-in-out;
|
||||
}
|
BIN
basics/static_index/static/root/favicon.ico
Normal file
BIN
basics/static_index/static/root/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
13
basics/static_index/static/root/index.html
Normal file
13
basics/static_index/static/root/index.html
Normal 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>
|
7
basics/static_index/static/root/js/example.js
Normal file
7
basics/static_index/static/root/js/example.js
Normal 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
1
basics/todo/.env
Normal file
@ -0,0 +1 @@
|
||||
DATABASE_URL=postgres://localhost/actix_todo
|
22
basics/todo/Cargo.toml
Normal file
22
basics/todo/Cargo.toml
Normal 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
48
basics/todo/README.md
Normal 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
5
basics/todo/diesel.toml
Normal 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"
|
0
basics/todo/migrations/.gitkeep
Normal file
0
basics/todo/migrations/.gitkeep
Normal 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();
|
@ -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;
|
@ -0,0 +1 @@
|
||||
DROP TABLE tasks
|
@ -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
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(),
|
||||
}
|
||||
}
|
||||
}
|
427
basics/todo/static/css/normalize.css
vendored
Normal file
427
basics/todo/static/css/normalize.css
vendored
Normal 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
421
basics/todo/static/css/skeleton.css
vendored
Normal 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) {}
|
58
basics/todo/static/css/style.css
Normal file
58
basics/todo/static/css/style.css
Normal 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;
|
||||
}
|
21
basics/todo/static/errors/400.html
Normal file
21
basics/todo/static/errors/400.html
Normal 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>
|
22
basics/todo/static/errors/404.html
Normal file
22
basics/todo/static/errors/404.html
Normal 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>
|
24
basics/todo/static/errors/500.html
Normal file
24
basics/todo/static/errors/500.html
Normal 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>
|
65
basics/todo/templates/index.html.tera
Normal file
65
basics/todo/templates/index.html.tera
Normal 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>
|
Reference in New Issue
Block a user