sapper 0.2.0

A web framework designed for easy use.
Documentation
## 入门教程

### 前言

Sapper 是一个轻量的 Web 框架,目的是为了方便使用。做个类比吧,这个框架的量级类似于 Python 的 Falcon,但会比 Falcon 的完全裸装更高一些,接近于 Flask。

### 介绍
Sapper 主要依赖于 [Hyper 0.10.13](https://github.com/hyperium/hyper) 提供 http server,当异步能够使用的时候(Rust 标准库完成),会考虑改成异步模式。

Sapper 框架分为几个部分组成:

- [Sapper]https://github.com/sappworks/sapper:主库,export 几个重要的 trait 和 struct,主要负责路由解析、错误处理等基本的框架功能,提供一个小的静态文件服务器的功能,单一的 Sapper 库是可以作为框架使用的,只是少一些比较方便的功能,所有流式数据需要自己转成想要的格式;
- [Sapper_std]https://github.com/sappworks/sapper_std:对一些脚手架功能的包装,合并为 std 库,供用户使用,提供大量方便可用的 macro(宏),同时将网络请求数据中的 Query/Form/Jsonbody 数据解析成方便使用的格式,可以很轻松的使用。

一般来说,在项目中,只需要引用上面两个库就可以正常工作了,当然,如果想要自己解析原始数据,也可以只引用 Sapper 主库。

下面几个库的功能全部被 Sapper_std 库引用并 export。
- [Sapper_session]https://github.com/sappworks/sapper_session 提供识别并解析 cookies 的 function,以及设置 cookies 的 function。

- [Sapper_logger]https://github.com/sappworks/sapper_logger 提供一个简单的 log 输出能力,日志格式如下:
```
[2017-12-09 12:20:12]  GET /ip/view Some("limit=25&offset=0") -> 200 OK (0.700839 ms)
```

- [Sapper_tmpl]https://github.com/sappworks/sapper_tmpl 引入 [tera]https://github.com/Keats/tera 这个 Jinja2 模板库,使 Sapper 能后端渲染 web 页面

- [Sapper_query]https://github.com/sappworks/sapper_query 解析 url 查询字符

- [Sapper_body]https://github.com/sappworks/sapper_body 解析请求 body 的数据: Form/Json

### 特性

Sapper 最大的特点是把 web 业务分为了三个层次(全局、模块、处理函数)去处理,每个模块都可以有自己的路由和中间件,全局可以定义全局共享的中间件和全局变量。把颗粒度放到最细就意味着每一个请求都可以有自己的中间件。

### 开始
这个示例尽量把 Sapper 的所有功能写进去,不过依然会有缺漏,一些没有写到的地方,可以参考 [Sapper_example](https://github.com/sappworks/sapper_example)

#### 建立项目

```
$ cargo new sapper_demo
```
在 cargo.toml 文件中增加依赖,为了演示自定义全局 not found 页面的功能(还未向 crates.io publish),依赖上增加 [patch] 项:
```toml
[dependencies]
 sapper = "^0.1"
 sapper_std = "^0.1"
 serde="*"
 serde_json="*"
 serde_derive="*"

[patch.crates-io]
 sapper = { git = 'https://github.com/sappworks/sapper.git' }
```

一个完整的 Sapper 项目目录大概如下,static 目录下的是静态文件,比如 js/css/png,views 目录下的是 web 模板文件,这个地方固定得比较死,tera 在 Sapper 内部指定加载这个目录下的所有文件。
```
|-- src
|   |-- bin
|   |   |-- main.rs
|   |-- lib.rs
|   |-- bar.rs
|   |-- foo.rs
|-- static
|-- views
|-- Cargo.lock
|-- Cargo.toml
```

在 `lib.rs` 下加入以下代码:
```rust
extern crate sapper;
#[macro_use]
extern crate sapper_std;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;

pub mod foo;
pub mod bar;

pub use foo::Foo;
pub use bar::{ Bar, Global };
```

#### bin 
接下来先写好 `main.rs` 启动项:
```rust
extern crate sapper;
extern crate sapper_std;
extern crate sapper_demo;

use sapper::{ SapperApp, SapperAppShell, Request, Response, Result as SapperResult };
use sapper_demo::{ Foo, Bar, Global };
use std::sync::Arc;

struct WebApp;

impl SapperAppShell for WebApp {
    fn before(&self, req: &mut Request) -> SapperResult<()> {
        sapper_std::init(req, Some("session"))?;
        Ok(())
    }

    fn after(&self, req: &Request, res: &mut Response) -> SapperResult<()> {
        sapper_std::finish(req, res)?;
        Ok(())
    }
}

fn main() {
    let global = Arc::new(String::from("global variable"));
    let mut app = SapperApp::new();
    app.address("127.0.0.1")
        .port(8080)
        .init_global(
            Box::new(move |req: &mut Request| {
                req.ext_mut().insert::<Global>(global.clone());
                Ok(())
            })
        )
        .with_shell(Box::new(WebApp))
        .add_module(Box::new(Foo))
        .add_module(Box::new(Bar))
        .static_service(true)
        .not_found_page(String::from("not found"));

    println!("Start listen on {}", "127.0.0.1:8080");
    app.run_http();
}
```

上面的代码目前是不能运行的,`Foo` 和 `Bar` 这两个结构体还没有定义。这里首先讲解一下上面代码:

`SapperApp` 是 Sapper 库中的核心结构之一,整个 web 项目都围绕这个结构进行,下面介绍一下它的一些常用方法(具体函数签名可以直接看源码):

`fn init_global()` 将想要全局共享的变量(如数据库连接池)注册在应用中。

`fn with_shell()` 注册全局中间件,上面代码将 std 的 `fn init()` 和 `fn finish()` 方法写入全局中间件。 `init` 将请求的各种参数解析并写入 `SapperRequest`,同时初始化 log,写入关注的 session key 值, `finish` 输出 log。

`fn add_moudle()` 注册子模块,每个模块需要符合 `SapperMoudle` trait。

`fn static_server()` 默认为 `true`, 即开启静态文件服务器功能。

`fn not_found_page()` 默认为 None,即如果路由没有指定的话,会返回 "404 not found" 字符串,如果需求自定义 404 页面,直接将对应的字符串传入即可。

#### Foo and Bar
在 foo.rs 里加上代码:
```rust
use sapper::{ SapperModule, SapperRouter, Response, Request, Result as SapperResult };
use sapper_std::{ QueryParams, PathParams, FormParams, JsonParams, Context, render };
use serde_json;

pub struct Foo;

impl Foo {
    fn index(_req: &mut Request) -> SapperResult<Response> {
        let mut web = Context::new();
        web.add("data", &"Foo 模块");
        res_html!("index.html", web)
    }

    // 解析 `/query?query=1`
    fn query(req: &mut Request) -> SapperResult<Response> {
        let params = get_query_params!(req);
        let query = t_param_parse!(params, "query", i64);
        let mut web = Context::new();
        web.add("data", &query);
        res_html!("index.html", web)
    }

    // 解析 `/user/:id`
    fn get_user(req: &mut Request) -> SapperResult<Response> {
        let params = get_path_params!(req);
        let id = t_param!(params, "id").clone();

        println!("{}", id);

        let json2ret = json!({
            "id": id
        });

        res_json!(json2ret)
    }

    // 解析 body json 数据
    fn post_json(req: &mut Request) -> SapperResult<Response> {
        #[derive(Serialize, Deserialize, Debug)]
        struct Person {
            foo: String,
            bar: String,
            num: i32,
        }

        let person: Person = get_json_params!(req);

        println!("{:#?}", person);

        let json2ret = json!({
            "status": true
        });
        res_json!(json2ret)
    }

    // 解析 form 数据
    fn test_post(req: &mut Request) -> SapperResult<Response> {
        let params = get_form_params!(req);
        let foo = t_param!(params, "foo");
        let bar = t_param!(params, "bar");
        let num = t_param_parse!(params, "num", i32);

        println!("{}, {}, {}", foo, bar, num);

        let json2ret = json!({
            "status": true
        });
        res_json!(json2ret)
    }
}

impl SapperModule for Foo {
    fn before(&self, _req: &mut Request) -> SapperResult<()> {
        Ok(())
    }

    fn after(&self, _req: &Request, _res: &mut Response) -> SapperResult<()> {
        Ok(())
    }

    fn router(&self, router: &mut SapperRouter) -> SapperResult<()> {
        router.get("/foo", Foo::index);

        router.get("/query", Foo::query);

        router.get("/user/:id", Foo::get_user);

        router.post("/test_post", Foo::test_post);

        router.post("/post_json", Foo::post_json);

        Ok(())
    }
}
```

`bar.rs` 代码:
```rust
use sapper::{ SapperModule, SapperRouter, Response, Request, Result as SapperResult, Key, Error as SapperError };
use std::sync::Arc;
use sapper::header::ContentType;

pub struct Bar;

impl Bar {
    fn index(_req: &mut Request) -> SapperResult<Response> {
        let mut res = Response::new();
        res.headers_mut().set(ContentType::html());
        res.write_body(String::from("bar"));
        Ok(res)
    }
}

impl SapperModule for Bar {
    fn before(&self, req: &mut Request) -> SapperResult<()> {
        let global = req.ext().get::<Global>().unwrap().to_string();
        let res = json!({
            "error": global
        });
        Err(SapperError::CustomJson(res.to_string()))
    }

    fn after(&self, _req: &Request, _res: &mut Response) -> SapperResult<()> {
        Ok(())
    }

    fn router(&self, router: &mut SapperRouter) -> SapperResult<()> {
        router.get("/bar", Bar::index);
        Ok(())
    }
}

pub struct Global;

impl Key for Global {
    type Value = Arc<String>;
}
```

`views` 文件夹下新增文件 `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link href="/foo.css" rel="stylesheet"/>
    <title>index</title>
</head>
<body>
<p>{{ data }}</p>
</body>
</html>
``` 

`static` 文件夹下新增文件 `foo.css`:
```css
p {
    color: red;
    text-align: center;
}
```

#### 代码解说
现在,demo 项目可以通过 `cargo run` 命令启动,监听在 `127.0.0.1:8080`。可以通过 `curl` 或者 Python 库 httpie 的命令 `http` 进行测试。 

##### `Foo` 模块

`Foo` 模块总共 5 个路由,分别展示 web 渲染、Query 解析、Path 解析、json 数据解析、Form 数据解析。除了标定的路由,其他请求都会到 `Error::NotFound` 这个错误处理里面,即返回 `SapperApp` 设定的 not found 界面,日志中不体现,设置了路由的访问,可以通过日志看到访问信息。

##### `Bar` 模块

`Bar` 模块设置了一个中间件,直接返回错误,这种情况下,访问 `127.0.0.1:8080/bar` 会直接在中间件被返回,即得到那个全局变量 `global`。

##### demo 源码
[https://github.com/sappworks/sapper_examples/tree/master/sapper_demo](https://github.com/sappworks/sapper_examples/tree/master/sapper_demo)

#### 中间件和 Error 处理
Sapper 的中间件和 Error 处理密切相关,下面是两个 trait 和 SapperResult 的源码:
```rust
type Result<T> = std::result::Result<T, Error>;

trait SapperModule: Sync + Send {
    fn before(&self, req: &mut SapperRequest) -> Result<()> {
        Ok(())
    }
    
    fn after(&self, req: &SapperRequest, res: &mut SapperResponse) -> Result<()> {
        Ok(())
    }
    
    fn router(&self, &mut SapperRouter) -> Result<()>;
    
}

trait SapperAppShell {
    fn before(&self, &mut SapperRequest) -> Result<()>;
    fn after(&self, &SapperRequest, &mut SapperResponse) -> Result<()>;
}
```
SapperModule 默认实现了 before 和 after。

整个 Sapper 中间件的运行机制是 **请求 -> 全局 before -> 模块 before -> 模块对应路由的处理函数 -> 模块 after -> 全局 after -> 返回**,在这期间,如果出现 Error 那么就直接跳出正常后续操作,原路返回。例如一个请求被模块 before 挡住了,那么就直接跳出,根据 Error 类型向请求方返回数据。

Sapper 的中间件正常返回值都是 `Ok(())`,意思是继续下去,如果返回 `Err(Error)` 就是直接跳出,根据 Error 类型进行处理,Error 类型如下:
```rust
pub enum Error {
    InvalidConfig,
    InvalidRouterConfig,
    FileNotExist,
    NotFound,
    Break,          // 400
    Unauthorized,   // 401
    Forbidden,      // 403
    TemporaryRedirect(String),     // 307
    Custom(String),
    CustomHtml(String),
    CustomJson(String),
}
```
Error 中带 String 的类型以及 NotFound 都是可以自定义返回值的
,比如 `CustomHtml` 和 `CustomJson`,可以根据自己需求向其中填入对应的字符串,错误处理将根据类型不同返回不同的 head 信息。

其他的类型返回的是固定的字符串,具体可以看源码 `app.rs` 第 [196](https://github.com/sappworks/sapper/blob/master/src/app.rs#L196) 行。

#### Sapper 源码
Sapper 源码中,SapperRequest 是对 hyper request 的魔改封装:
```rust
pub struct SapperRequest<'a, 'b: 'a> {
    raw_req: Box<HyperRequest<'a, 'b>>,
    ext: TypeMap
} 
```
这个 [typemap::TypeMap](https://github.com/reem/rust-typemap) 就是核心了,是一个安全的类型值存储 Map。Sapper 中的 Query,Form,Cookies,Json 等信息都存储在这个地方,有兴趣的话,可以看看源码。

### 开源应用
无耻地放出了一个博客源码地址: [https://github.com/driftluo/MyBlog](https://github.com/driftluo/MyBlog)

### Contribute
欢迎加入 Sapper 社区,欢迎提供高质量的代码,高质量的思路。