发送 Index 请求
让我们添加索引图书的 API,然后放入一些测试数据供后续使用。
添加 async_trait
依赖用于在 trait
中使用 async fn
:
cargo add async_trait
创建 src/domain/gateway/book_manager.rs:
use std::error::Error;
use async_trait::async_trait;
use crate::domain::model;
#[async_trait]
pub trait BookManager: Send + Sync {
async fn index_book(&self, b: &model::Book) -> Result<String, Box<dyn Error>>;
}
此处也是在使用4层架构。项目越大,该架构的好处越明显。阅读更多。
某个类型可以安全地传送到其它线程,那么它就是 Send;某个类型可以在线程间安全共享,那么它就是 Sync。
添加 src/domain/gateway/mod.rs:
mod book_manager;
pub use book_manager::BookManager;
记得为其它子目录创建 mod.rs 文件,以导出符号。
创建 src/infrastructure/search/es.rs:
use std::error::Error;
use async_trait::async_trait;
use elasticsearch::http::transport::Transport;
use elasticsearch::{Elasticsearch, IndexParts};
use serde_json::Value;
use crate::domain::gateway::BookManager;
use crate::domain::model;
const INDEX_BOOK: &str = "book_idx";
pub struct ElasticSearchEngine {
client: Elasticsearch,
}
impl ElasticSearchEngine {
pub fn new(address: &str) -> Result<Self, Box<dyn Error>> {
let transport = Transport::single_node(address)?;
let client = Elasticsearch::new(transport);
Ok(ElasticSearchEngine { client })
}
}
#[async_trait]
impl BookManager for ElasticSearchEngine {
async fn index_book(&self, b: &model::Book) -> Result<String, Box<dyn Error>> {
let response = self
.client
.index(IndexParts::Index(INDEX_BOOK))
.body(b)
.send()
.await?;
let response_body = response.json::<Value>().await?;
Ok(response_body["_id"].as_str().unwrap().into())
}
}
默认情况下,Elasticsearch 允许你将文档索引到尚不存在的索引中。 当你将文档索引到不存在的索引时,Elasticsearch 将会使用默认设置动态地创建索引。这在开发者不想显式地创建索引时会很方便。
如果你遇到了
unexpected transfer-encoding parsed
错误:
- 尝试禁用代理,包括系统代理。系统代理会被默认启用。
- 或者,使用
TransportBuilder
的disable_proxy()
方法。let url = Url::parse(address)?; let conn_pool = SingleNodeConnectionPool::new(url); let transport = TransportBuilder::new(conn_pool).disable_proxy().build()?;
添加 toml
依赖:
cargo add toml
提示: TOML 全称是 Tom's Obvious Minimal Language。
创建 src/infrastructure/config/mod.rs:
use std::fs;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub app: ApplicationConfig,
pub search: SearchConfig,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SearchConfig {
pub address: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ApplicationConfig {
pub port: i32,
}
pub fn parse_config(file_name: &str) -> Config {
let content = fs::read_to_string(file_name).expect("Failed to open TOML config file");
toml::from_str(&content).expect("Failed to parse TOML config file")
}
添加 src/infrastructure/mod.rs:
mod config;
pub use config::{parse_config, Config};
pub mod search;
创建 config.toml:
[app]
port = 3000
page_size = 10
[search]
address = "http://localhost:9200"
警醒:
不要直接 git 提交 config.toml。可能会导致敏感数据泄露。如果非要提交的话,建议只提交配置格式模板。
比如:[app] port = 3000 page_size = 10 [search] address = ""
创建 src/application/executor/book_operator.rs:
use std::error::Error;
use std::sync::Arc;
use crate::domain::gateway;
use crate::domain::model;
pub struct BookOperator {
book_manager: Arc<dyn gateway::BookManager>,
}
impl BookOperator {
pub fn new(b: Arc<dyn gateway::BookManager>) -> Self {
BookOperator { book_manager: b }
}
pub async fn create_book(&self, b: model::Book) -> Result<String, Box<dyn Error>> {
Ok(self.book_manager.index_book(&b).await?)
}
}
创建 src/application/wire_helper.rs:
use std::sync::Arc;
use crate::domain::gateway;
use crate::infrastructure::search;
use crate::infrastructure::Config;
pub struct WireHelper {
engine: Arc<search::ElasticSearchEngine>,
}
impl WireHelper {
pub fn new(c: &Config) -> Result<Self, Box<dyn std::error::Error>> {
let engine = Arc::new(search::ElasticSearchEngine::new(&c.search.address)?);
Ok(WireHelper { engine })
}
pub fn book_manager(&self) -> Arc<dyn gateway::BookManager> {
Arc::clone(&self.engine) as Arc<dyn gateway::BookManager>
}
}
创建 src/adapter/router.rs:
use axum::{
extract::{Json, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Router,
};
use std::sync::Arc;
use serde_json::json;
use crate::application;
use crate::application::executor;
use crate::domain::model;
pub struct RestHandler {
book_operator: executor::BookOperator,
}
async fn create_book(
State(rest_handler): State<Arc<RestHandler>>,
Json(book): Json<model::Book>,
) -> Result<Json<serde_json::Value>, impl IntoResponse> {
match rest_handler.book_operator.create_book(book).await {
Ok(book_id) => Ok(Json(json!({"id": book_id}))),
Err(err) => Err((StatusCode::INTERNAL_SERVER_ERROR, err.to_string())),
}
}
async fn welcome() -> Json<serde_json::Value> {
Json(json!({
"status": "ok"
}))
}
pub fn make_router(wire_helper: &application::WireHelper) -> Router {
let rest_handler = Arc::new(RestHandler {
book_operator: executor::BookOperator::new(wire_helper.book_manager()),
});
Router::new()
.route("/", get(welcome))
.route("/books", post(create_book))
.with_state(rest_handler)
}
在 handlers 之间共享一些 state(状态)是很常见的操作。例如,可能需要共享一个数据库连接池或某服务的客户端。
最常见的三种方法是:
在这里,我们使用 State
来在 handlers 之间共享 rest_handler
。
用以下代码替换 src/main.rs 中内容:
mod adapter;
mod application;
mod domain;
mod infrastructure;
const CONFIG_FILE: &str = "config.toml";
#[tokio::main]
async fn main() {
let c = infrastructure::parse_config(CONFIG_FILE);
let wire_helper = application::WireHelper::new(&c).expect("Failed to create WireHelper");
let app = adapter::make_router(&wire_helper);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
记得添加以下 mod.rs 文件:
- src/adapter/mod.rs
- src/application/executor/mod.rs
- src/application/mod.rs
- src/domain/gateway/mod.rs
- src/domain/mod.rs
- src/infrastructure/mod.rs
- src/infrastructure/search/mod.rs
Cargo.toml 中变化:
@@ -6,7 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+async-trait = "0.1.80"
axum = "0.7.5"
+elasticsearch = "8.5.0-alpha.1"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
tokio = { version = "1.37.0", features = ["full"] }
+toml = "0.8.12"
再次运行 server,然后使用 curl
进行测试:
cargo run
样例请求:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \
http://localhost:3000/books
样例响应:
{"id":"gnRLFI8Be1nlzJXaEsQR"}
放入测试数据
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Hunger Games","author":"Suzanne Collins","published_at":"2008-09-14","content":"In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \
http://localhost:3000/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \
http://localhost:3000/books