实用 Rust Web 开发 - API Rest
更新:根据这个问题,异步不适用于 Diesel,因此,to_async
来自的方法web::get
可能无法按预期工作,它会工作但不是你想要的方式,所以,老实说,你可能会将它更改为to
。
这是一系列博客文章中的第一篇,展示了如何使用 Rust 进行 Web 开发,我尝试尽可能实用,使用已经为这项工作选择的工具。
我将从基础开始,直到我们可以创建基本的 API Rest 端点,用于列出、创建、编辑和删除虚构网上商店的产品。
我会一步一步地讲解,但是最好先阅读一下Rust 书籍,了解一些有关 Rust 语言的知识。
我们需要做的第一件事是安装 Rust,我们可以访问https://www.rust-lang.org/tools/install并按照说明进行操作,因为我使用的是 Linux,所以示例代码取自该操作系统,但是您也可以在 Windows 或 Mac 上尝试。
在终端中执行下一步并按照说明进行操作:curl https://sh.rustup.rs -sSf | sh
您可以通过运行来验证 Rust 是否正确安装rustc -V
,它将显示已安装的 rustc 版本。
接下来我们要做的是创建一个新项目,我们可以将其命名为 mystore,在终端窗口中运行以下命令:cargo new mystore --bin
。
如果一切正确,我们将能够看到一个名为 mystore 的文件夹,我们可以看到 Rust 项目的基本结构:
接下来我们需要一个 Web 框架,我们将使用 actix-web,一个基于 Actix(一个 Actor 框架)的高级框架。在 中添加以下代码行cargo.toml
:
[dependencies]
actix = "0.8"
actix-web = "1.0.0-beta"
现在,当您执行时cargo build
,将安装板条箱并编译项目。
我们将从一个 hello world 示例开始,添加下一行代码src/main.rs
:
extern crate actix_web;
use actix_web::{HttpServer, App, web, HttpRequest, HttpResponse};
// Here is the handler,
// we are returning a json response with an ok status
// that contains the text Hello World
fn index(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok().json("Hello world!")
}
fn main() {
// We are creating an Application instance and
// register the request handler with a route and a resource
// that creates a specific path, then the application instance
// can be used with HttpServer to listen for incoming connections.
HttpServer::new(|| App::new().service(
web::resource("/").route(web::get().to_async(index))))
.bind("127.0.0.1:8088")
.unwrap()
.run();
}
cargo run
在终端中执行,然后转到http://localhost:8088/并查看结果,如果您可以在浏览器中看到文本 Hello world!,则一切按预期工作。
现在,我们将选择数据库驱动程序,在本例中为diesel,我们在中添加一个依赖项Cargo.toml
:
[dependencies]
diesel = { version = "1.0.0", features = ["postgres"] }
dotenv = "0.9.0"
如果我们执行,cargo build
那么板条箱就会被安装,项目也会被编译。
安装 cli 工具也是个好主意cargo install diesel_cli
。
如果您在安装 diesel_cli 时遇到问题,可能是因为缺少数据库驱动程序,因此请确保包含它们,如果您使用 Ubuntu,则可能需要安装postgresql-server-dev-all
。
在 bash 中执行下一个命令:
$ echo DATABASE_URL=postgres://postgres:@localhost/mystore > .env
现在,diesel 设置数据库的一切准备就绪,运行diesel setup
即可创建数据库。如果您在配置 postgres 时遇到问题,请查看本指南。
让我们创建一个表来处理产品:
diesel migration generate create_products
Diesel CLI 将创建两个迁移文件,up.sql
和down.sql
。
up.sql
:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
stock FLOAT NOT NULL,
price INTEGER --representing cents
)
down.sql
:
DROP TABLE products
应用迁移:
diesel migration run
我们将加载我们需要的库main.rs
:
src/main.rs
:
#[macro_use]
extern crate diesel;
extern crate dotenv;
接下来我们创建一个文件来处理数据库连接,我们将其命名为 db_connection.rb 并将其保存在 src 中。
src/db_connection.rs
:
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;
pub fn establish_connection() -> PgConnection {
dotenv().ok(); // This will load our .env file.
// Load the DATABASE_URL env variable into database_url, in case of error
// it will through a message "DATABASE_URL must be set"
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
// Load the configuration in a postgres connection,
// the ampersand(&) means we're taking a reference for the variable.
// The function you need to call will tell you if you have to pass a
// reference or a value, borrow it or not.
PgConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
接下来,我们将创建第一个资源:产品列表。
我们首先需要的是几个结构,一个用于创建资源,另一个用于获取资源,在本例中是用于产品。
我们可以将它们保存在名为 models 的文件夹中,但在此之前,我们需要一种方法来加载我们的文件,我们添加下几行main.rs
:
src/main.rs
:
pub mod schema;
pub mod models;
pub mod db_connection;
我们需要在模型文件夹中创建一个名为mod.rs
:
src/models/mod.rs
:
pub mod product;
src/models/product.rs
:
use crate::schema::products;
#[derive(Queryable)]
pub struct Product {
pub id: i32,
pub name: String,
pub stock: f64,
pub price: Option<i32> // For a value that can be null,
// in Rust is an Option type that
// will be None when the db value is null
}
#[derive(Insertable)]
#[table_name="products"]
pub struct NewProduct {
pub name: Option<String>,
pub stock: Option<f64>,
pub price: Option<i32>
}
因此,让我们添加一些代码来获取产品列表,我们将创建一个名为 ProductList 的新结构来处理产品列表,并添加一个函数列表来从数据库中获取产品,将下一个块添加到models/product.rs
:
// This will tell the compiler that the struct will be serialized and
// deserialized, we need to install serde to make it work.
#[derive(Serialize, Deserialize)]
pub struct ProductList(pub Vec<Product>);
impl ProductList {
pub fn list() -> Self {
// These four statements can be placed in the top, or here, your call.
use diesel::RunQueryDsl;
use diesel::QueryDsl;
use crate::schema::products::dsl::*;
use crate::db_connection::establish_connection;
let connection = establish_connection();
let result =
products
.limit(10)
.load::<Product>(&connection)
.expect("Error loading products");
// We return a value by leaving it without a comma
ProductList(result)
}
}
我这样做是为了让我们可以自由地向该结构添加任何特征,我们不能对 Vector 这样做,因为我们不拥有它,ProductList 使用 Rust 中的新类型模式。
现在,我们只需要一个句柄来回答产品列表的请求,我们将使用serde
它将数据序列化为 json 响应。
我们需要编辑Cargo.toml
,main.rs
和models/product.rs
:
Cargo.toml
:
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
main.rs
:
pub mod handlers; // This goes to the top to load the next handlers module
extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;
src/models/product.rs
:
#[derive(Queryable, Serialize, Deserialize)]
pub struct Product {
pub id: i32,
pub name: String,
pub stock: f64,
pub price: Option<i32>
}
mod.rs
添加名为的文件src/handlers
:
pub mod products;
我们可以在处理程序文件夹中创建一个名为的文件products.rs
:
src/handlers/products.rs
:
use actix_web::{HttpRequest, HttpResponse };
use crate::models::product::ProductList;
// This is calling the list method on ProductList and
// serializing it to a json response
pub fn index(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok().json(ProductList::list())
}
我们需要将索引处理程序添加到我们的服务器中以main.rs
获得 Rest API 的第一部分,更新文件使其看起来像这样:
src/main.rs
:
pub mod schema;
pub mod db_connection;
pub mod models;
pub mod handlers;
#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;
extern crate actix;
extern crate actix_web;
extern crate futures;
use actix_web::{App, HttpServer, web};
fn main() {
let sys = actix::System::new("mystore");
HttpServer::new(
|| App::new()
.service(
web::resource("/products")
.route(web::get().to_async(handlers::products::index))
))
.bind("127.0.0.1:8088").unwrap()
.start();
println!("Started http server: 127.0.0.1:8088");
let _ = sys.run();
}
让我们在终端运行中添加一些数据并查看它是什么样子:
psql -U postgres -d mystore -c "INSERT INTO products(name, stock, price) VALUES ('shoes', 10.0, 100); INSERT INTO products(name, stock, price) VALUES ('hats', 5.0, 50);"
然后执行:
cargo run
最后转到http://localhost:8088/products。
如果一切按预期进行,您应该会在 json 值中看到几个产品。
创建产品
向结构添加Deserialize
特征NewProduct
并添加函数来创建产品:
#[derive(Insertable, Deserialize)]
#[table_name="products"]
pub struct NewProduct {
pub name: String,
pub stock: f64,
pub price: Option<i32>
}
impl NewProduct {
// Take a look at the method definition, I'm borrowing self,
// just for fun remove the & after writing the handler and
// take a look at the error, to make it work we would need to use into_inner (https://actix.rs/api/actix-web/stable/actix_web/struct.Json.html#method.into_inner)
// which points to the inner value of the Json request.
pub fn create(&self) -> Result<Product, diesel::result::Error> {
use diesel::RunQueryDsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
diesel::insert_into(products::table)
.values(self)
.get_result(&connection)
}
}
然后添加一个处理程序来创建产品:
use crate::models::product::NewProduct;
use actix_web::web;
pub fn create(new_product: web::Json<NewProduct>) -> Result<HttpResponse, HttpResponse> {
// we call the method create from NewProduct and map an ok status response when
// everything works, but map the error from diesel error
// to an internal server error when something fails.
new_product
.create()
.map(|product| HttpResponse::Ok().json(product))
.map_err(|e| {
HttpResponse::InternalServerError().json(e.to_string())
})
}
最后添加相应的路由并启动服务器:
src/main.rs
:
HttpServer::new(
|| App::new()
.service(
web::resource("/products")
.route(web::get().to_async(handlers::products::index))
.route(web::post().to_async(handlers::products::create))
))
.bind("127.0.0.1:8088").unwrap()
.start();
cargo run
我们可以创建一个新产品:
curl http://127.0.0.1:8088/products \
-H "Content-Type: application/json" \
-d '{"name": "socks", "stock": 7, "price": 2}'
显示产品
src/models/product.rs
:
impl Product {
pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
products::table.find(id).first(&connection)
}
}
src/handlers/products.rs
:
use crate::models::product::Product;
pub fn show(id: web::Path<i32>) -> Result<HttpResponse, HttpResponse> {
Product::find(&id)
.map(|product| HttpResponse::Ok().json(product))
.map_err(|e| {
HttpResponse::InternalServerError().json(e.to_string())
})
}
src/main.rs
:
HttpServer::new(
|| App::new()
.service(
web::resource("/products")
.route(web::get().to_async(handlers::products::index))
.route(web::post().to_async(handlers::products::create))
)
.service(
web::resource("/products/{id}")
.route(web::get().to_async(handlers::products::show))
)
)
.bind("127.0.0.1:8088").unwrap()
.start();
cargo run
如果一切正常,你应该在http://127.0.0.1:8088/products/1中看到一只鞋子
删除产品
向产品模型添加新方法:
src/models/product.rs
:
impl Product {
pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
products::table.find(id).first(&connection)
}
pub fn destroy(id: &i32) -> Result<(), diesel::result::Error> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use crate::schema::products::dsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
// Take a look at the question mark at the end,
// it's a syntax sugar that allows you to match
// the return type to the one in the method signature return,
// as long as it is the same error type, it works for Result and Option.
diesel::delete(dsl::products.find(id)).execute(&connection)?;
Ok(())
}
}
src/handlers/products.rs
:
pub fn destroy(id: web::Path<i32>) -> Result<HttpResponse, HttpResponse> {
Product::destroy(&id)
.map(|_| HttpResponse::Ok().json(()))
.map_err(|e| {
HttpResponse::InternalServerError().json(e.to_string())
})
}
src/main.rs
:
HttpServer::new(
|| App::new()
.service(
web::resource("/products")
.route(web::get().to_async(handlers::products::index))
.route(web::post().to_async(handlers::products::create))
)
.service(
web::resource("/products/{id}")
.route(web::get().to_async(handlers::products::show))
.route(web::delete().to_async(handlers::products::destroy))
)
)
.bind("127.0.0.1:8088").unwrap()
.start();
cargo run
让我们删除一只鞋子:
curl -X DELETE http://127.0.0.1:8088/products/1 \
-H "Content-Type: application/json"
你不应该在http://127.0.0.1:8088/products中看到鞋子
更新产品
将 AsChangeset 特征添加到 NewProduct,这样您就可以将结构直接传递给更新,否则您需要指定要更新的每个字段。
src/models/product.rs
:
#[derive(Insertable, Deserialize, AsChangeset)]
#[table_name="products"]
pub struct NewProduct {
pub name: Option<String>,
pub stock: Option<f64>,
pub price: Option<i32>
}
impl Product {
pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
products::table.find(id).first(&connection)
}
pub fn destroy(id: &i32) -> Result<(), diesel::result::Error> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use crate::schema::products::dsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
diesel::delete(dsl::products.find(id)).execute(&connection)?;
Ok(())
}
pub fn update(id: &i32, new_product: &NewProduct) -> Result<(), diesel::result::Error> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use crate::schema::products::dsl;
use crate::db_connection::establish_connection;
let connection = establish_connection();
diesel::update(dsl::products.find(id))
.set(new_product)
.execute(&connection)?;
Ok(())
}
}
src/handlers/product.rs
:
pub fn update(id: web::Path<i32>, new_product: web::Json<NewProduct>) -> Result<HttpResponse, HttpResponse> {
Product::update(&id, &new_product)
.map(|_| HttpResponse::Ok().json(()))
.map_err(|e| {
HttpResponse::InternalServerError().json(e.to_string())
})
}
src/main.rs
:
HttpServer::new(
|| App::new()
.service(
web::resource("/products")
.route(web::get().to_async(handlers::products::index))
.route(web::post().to_async(handlers::products::create))
)
.service(
web::resource("/products/{id}")
.route(web::get().to_async(handlers::products::show))
.route(web::delete().to_async(handlers::products::destroy))
.route(web::patch().to_async(handlers::products::update))
)
)
.bind("127.0.0.1:8088").unwrap()
.start();
cargo run
现在,让我们为产品添加库存:
curl -X PATCH http://127.0.0.1:8088/products/3 \
-H "Content-Type: application/json" \
-d '{"stock": 8}'
您现在应该有 8 双袜子:http://127.0.0.1: 8088/products/3 。
在此处查看完整的源代码。
Rust 不是最简单的编程语言,但它的优点克服了问题,Rust 允许您长期编写高性能和高效的应用程序。
文章来源:https://dev.to/werner/practical-rust-web-development-api-rest-29g1