Skip to content

Commit 885111c

Browse files
committed
feat: Introduce routing system documentation
This commit adds a new documentation page for the routing system, detailing routing forms, multiple endpoints, `IntoResponse` trait, and extractors. It also updates existing development documentation to reflect changes in `sithra-kit` APIs, including `plugin!`, `matchopt!`, and `msg!` macros, and clarifies `Initialize` struct and error handling during initialization.
1 parent 9739846 commit 885111c

File tree

5 files changed

+301
-56
lines changed

5 files changed

+301
-56
lines changed

content/docs/development/index.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,19 @@ anyhow = "1.0"
164164

165165
```rust title="src/main.rs"
166166
use sithra_kit::{
167-
plugin::Plugin,
167+
matchopt, plugin,
168168
server::extract::payload::Payload,
169169
types::{
170170
message::{Message, SendMessage, common::CommonSegment as H},
171-
smsg,
171+
msg,
172172
},
173173
};
174174

175175
/// 插件入口点
176176
#[tokio::main]
177177
async fn main() {
178178
// 1. 初始化插件
179-
let (plugin, _) = Plugin::new::<()>().await.unwrap();
179+
let (plugin, _) = plugin!();
180180

181181
// 2. 注册消息处理路由
182182
let plugin = plugin.map(|r| r.route_typed(Message::on(echo)));
@@ -196,7 +196,7 @@ async fn main() {
196196
/// 当收到以 "echo " 开头的消息时,返回去掉 "echo " 前缀的内容
197197
async fn echo(Payload(msg): Payload<Message<H>>) -> Option<SendMessage> {
198198
// 1. 消息是否为文本开头,如果是则获取文本,否则返回 None
199-
let text = msg.content.first()?.text_opt()?;
199+
let text = matchopt!(msg.content.first()?, H::Text(t) => t)?;
200200

201201
// 2. 检查消息是否以 "echo " 开头,如果不是则返回 None
202202
let text = text.strip_prefix("echo ")?.to_owned();
@@ -214,7 +214,7 @@ async fn echo(Payload(msg): Payload<Message<H>>) -> Option<SendMessage> {
214214
}
215215

216216
// 6. 返回要发送的消息
217-
Some(smsg!(content))
217+
Some(msg!(content))
218218
}
219219
```
220220

@@ -310,7 +310,7 @@ cd my-custom-plugin
310310
[package]
311311
name = "my-custom-plugin"
312312
version = "0.1.0"
313-
edition = "2021"
313+
edition.workspace = true
314314

315315
[dependencies]
316316
sithra-kit.workspace = true
@@ -392,7 +392,7 @@ just run
392392
</Cards>
393393

394394
<Callout type="info" icon={<Terminal />}>
395-
在开发模式下,可以使用 <code>RUST_LOG=debug just run</code> 命令启动
395+
在开发模式下,可以使用 <code>SITHRA_LOG=debug just run</code> 命令启动
396396
sithra-rs,以显示更详细的调试信息。
397397
</Callout>
398398

content/docs/development/initialize.mdx

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ async fn main() {
5555
初始化结果封装在 `Initialize` 结构体中,它的定义如下:
5656

5757
```rust
58-
pub struct Initialize<C> {
58+
pub struct Initialize<C = ()> {
5959
pub config: C,
60-
pub data_path: String,
6160
pub id: String,
61+
pub data_path: String,
6262
}
6363
```
6464

@@ -67,8 +67,8 @@ pub struct Initialize<C> {
6767
这个结构体包含了两个重要字段:
6868

6969
- `config` 字段表示插件的配置信息,可以为任何可反序列化类型
70-
- `data_path` 字段表示插件的数据路径,我们约定插件的所有数据都存储在该路径下
7170
- `id` 字段表示插件的唯一标识符,用于在框架中区分不同的插件实例。
71+
- `data_path` 字段表示插件的数据路径,我们约定插件的所有数据都存储在该路径下
7272

7373
</Callout>
7474

@@ -77,6 +77,7 @@ pub struct Initialize<C> {
7777
通过为 `plugin!` 宏指定泛型参数,我们可以定义插件专属的配置类型:
7878

7979
```rust
80+
#[derive(Debug, Clone, Deserialize)]
8081
struct Config {
8182
admins: Vec<String>,
8283
}
@@ -112,6 +113,30 @@ tokio::select! {
112113
}
113114
```
114115

116+
### 错误处理
117+
118+
在初始化过程中,插件应捕获并处理所有错误。`plugin` 提供了以下方法来响应初始化状态:
119+
120+
- `plugin.err(error)`: 响应错误并终止初始化过程(返回 `never`)。
121+
- `plugin.ok()`: 响应初始化完成消息并允许插件继续执行。
122+
- `plugin.expect(result)`: 这是一个特殊方法。如果 `result``Err(error)`,其行为与调用 `plugin.err(error)` 一致,终止初始化。如果 `result``Ok(value)`,则返回 `value` 并继续初始化。
123+
124+
签名:
125+
```rust
126+
type Self = Plugin;
127+
128+
/// Send an initialization response indicating failure.
129+
async fn err(&mut self, err: impl Into<PluginInitError>) -> !;
130+
/// Send an initialization response indicating success.
131+
fn ok(&mut self);
132+
/// Tool used to handle errors and continue initialization.
133+
async fn expect<T, E: Display>(&mut self, r: Result<T, E>) -> T;
134+
```
135+
136+
**隐式行为:**
137+
138+
如果在调用 `plugin.run()` 之前没有显式地通过 `plugin.ok()``plugin.err()` 确认初始化状态,框架将自动调用 `plugin.ok()` 来确认初始化完成。一旦 `plugin.ok()` 被调用,框架将停止等待初始化消息,并将插件视为已成功启动,不再捕获初始化错误信息。因此,除非有特殊需求,否则不建议手动调用 `plugin.ok()` 来完成初始化。
139+
115140
<Callout type="warn" icon={<Terminal />}>
116141

117142
当插件退出时,框架内部会直接丢弃插件实例,析构时插件会被 kill。
@@ -151,9 +176,12 @@ tokio::select! {
151176

152177
```rust
153178
pub struct Plugin {
154-
peer: Peer,
155-
server: Server,
156-
router: Router,
179+
peer: Peer,
180+
pub server: Server,
181+
router: Router,
182+
name: String,
183+
version: String,
184+
initialize_done: bool,
157185
}
158186
```
159187

@@ -174,22 +202,19 @@ pub struct Plugin {
174202
让我们拆解 `Plugin::new` 方法,了解其内部实现:
175203

176204
```rust
177-
pub async fn new<Config>(version: &str, name: &str) -> (Self, Initialize<Config>)
205+
pub async fn new<Config, F>(version: &str, name: &str, init_fn: F) -> (Self, Initialize<Config>)
178206
where
179207
Config: for<'de> Deserialize<'de>,
208+
F: FnOnce(&mut Initialize<Config>) -> InitializeResult,
180209
{
181-
// 处理命令行选项,如 --version
182210
if handle_options(version, name) {
183211
process::exit(0);
184212
}
185-
// 创建对等节点、服务端和路由表
186213
let peer = Peer::new();
187214
let server = Server::new();
188215
let router = Router::new();
189-
// 从对等节点创建帧序列
190216
let mut framed = crate::transport::util::framed(peer);
191217

192-
// 执行初始化循环,等待框架发送初始化消息
193218
let init = loop {
194219
let Some(msg) = <FramedPeer as StreamExt>::next(&mut framed).await else {
195220
break Err(PluginInitError::ConnectionClosed);
@@ -203,47 +228,46 @@ where
203228
}
204229
};
205230

206-
// 处理初始化结果
207-
let init = match init {
231+
let mut init = match init {
208232
Ok(init) => init,
209233
Err(err) => {
210-
// 发送错误信息给框架
211234
framed
212235
.send(
213236
DataPack::builder()
214237
.path(Initialize::<Config>::path())
215-
.build_with_payload(InitializeResult::Err(err)),
238+
.build_with_payload(&InitializeResult::Err(err)),
216239
)
217-
.await.ok();
218-
// 等待退出信号
240+
.await
241+
.ok();
219242
tokio::signal::ctrl_c().await.ok();
220-
// 退出进程
221243
process::exit(1);
222244
}
223245
};
224246

225-
// 初始化日志系统
226247
init_log(server.client().sink());
227248

228-
// 发送初始化成功响应给框架
229-
server
230-
.client()
231-
.send(
232-
RequestDataPack::default()
233-
.path(Initialize::<Config>::path())
234-
.payload(InitializeResult::Ok(())),
235-
)
236-
.unwrap_or_else(|_| panic!("Failed to send initialization response: [{name}]"));
249+
if let Err(err) = init_fn(&mut init) {
250+
framed
251+
.send(
252+
DataPack::builder()
253+
.path(Initialize::<Config>::path())
254+
.build_with_payload(&InitializeResult::Err(err)),
255+
)
256+
.await
257+
.ok();
258+
tokio::signal::ctrl_c().await.ok();
259+
process::exit(1);
260+
}
237261

238-
// 用不到帧序列了,我们换回对等节点
239262
let peer = framed.into_inner();
240-
241-
// 返回实例
242263
(
243264
Self {
244265
peer,
245266
server,
246267
router,
268+
name: name.to_owned(),
269+
version: version.to_owned(),
270+
initialize_done: false,
247271
},
248272
init,
249273
)
@@ -259,7 +283,7 @@ where
259283
3. 等待并解析框架的初始化消息
260284
4. 如果初始化失败,向框架报告错误并退出
261285
5. 初始化日志系统
262-
6. 向框架发送初始化成功响应
286+
6. 执行用户提供的初始化函数
263287
7. 返回插件实例和初始化结果
264288

265289
</Callout>
@@ -269,11 +293,16 @@ where
269293
`Plugin::run` 方法的实现相对简洁:
270294

271295
```rust
272-
pub fn run(self) -> JoinSet<Result<(), ServerError>> {
296+
pub fn run(mut self) -> JoinSet<Result<(), ServerError>> {
297+
// 如果初始化未完成,则隐式发送成功响应
298+
if !self.initialize_done {
299+
self.ok();
300+
}
273301
let Self {
274302
peer,
275303
server,
276304
router,
305+
..
277306
} = self;
278307
// 分割对等节点为写入和读取部分
279308
let (write, read) = peer.split();
@@ -345,16 +374,8 @@ pub fn run(self) -> JoinSet<Result<(), ServerError>> {
345374

346375
// 初始化日志
347376
init_log(server.client().sink());
348-
349-
// 发送初始化成功响应
350-
server
351-
.client()
352-
.send(
353-
RequestDataPack::default()
354-
.path(Initialize::<Config>::path())
355-
.payload(InitializeResult::Ok(())),
356-
)
357-
.unwrap_or_else(|_| panic!("Failed to send initialization response: [{NAME}]"));
377+
378+
358379

359380
// 用不到帧序列了,我们换回对等节点
360381
let peer = framed.into_inner();
@@ -413,7 +434,13 @@ pub fn run(self) -> JoinSet<Result<(), ServerError>> {
413434

414435
// 初始化日志
415436
init_log(server.client().sink());
416-
437+
438+
// 这里是自定义初始化逻辑的地方,对应 `plugin!` 宏的第二个参数
439+
// 例如:
440+
// if let Err(e) = some_init_task(&init.config).await {
441+
// // report error and exit
442+
// }
443+
417444
// 发送初始化成功响应
418445
server
419446
.client()
@@ -458,4 +485,4 @@ pub fn run(self) -> JoinSet<Result<(), ServerError>> {
458485
恭喜您已经深入了解了 sithra-rs 插件的初始化流程!接下来,您可以:
459486

460487
- 了解如何[注册和处理路由](/docs/development/router)
461-
- 探索[内部服务](/docs/development/server)的实现和使用
488+
- 探索[内部服务](/docs/development/server)的实现和使用

content/docs/development/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"index",
99
"standards",
1010
"---逐步深入---",
11-
"initialize"
11+
"initialize",
12+
"route"
1213
]
1314
}

0 commit comments

Comments
 (0)