nagisa_core/slots.rs
1//! 类型化命名 slot 投影:`Slots<T>` —— `Args<T>` 的字面孪生。
2//!
3//! 消息有**两个解析面**:HEAD(匹配器看到的命名正则捕获组)与 TAIL(命令头之后的段流)。
4//! `Args<T>`(`args.rs`)拥有 TAIL;**`Slots<T>` 拥有 HEAD**——`Matcher::slots` 编出的
5//! 命名、可选、类型化捕获组,投影成结构体字段。`Captures(Vec<String>)` 是位置/无类型的,
6//! `Slots` 是命名 + `Option<T>` 可选 + 边界处一次性类型化解析。
7//!
8//! 设计要点:
9//! - **`SlotValue`**:单捕获投影。空白桥 `impl<T: FromArg> SlotValue for T` 让 int/String/Uin
10//! **及任意 `#[derive(ArgEnum)]` 枚举**零代码即是 `SlotValue`(union/enum 槽免费)。
11//! **没有** `impl SlotValue for (A,B)`:tuple 是多组关注,由 `#[derive(Slots)]` 在它知道两个
12//! 内组的那一层处理。
13//! - **`FromSlots`**:由 `#[derive(Slots)]` 生成,产出 `matcher()` + 从 `named_captures` 投影。
14//! - **`Slots<T>: FromContext`**:`Args<T>` 的镜像,共享同一 parse-miss 路径(`on_parse_miss`)。
15//! - **`Tail<T>`**:贪婪尾槽(`(.*)` → `Option<String>`),或 `Tail<Vec<Segment>>` 收原始多模态尾。
16use crate::args::{on_parse_miss, ArgError, ArgSpec, FromArg};
17use crate::ctx::Ctx;
18use crate::extract::{Extracted, FromContext, Reject};
19use crate::matcher::{Matcher, ParsedCommand};
20use async_trait::async_trait;
21use nagisa_types::segment::Segment;
22use std::borrow::Cow;
23use std::collections::HashMap;
24
25/// 命名正则槽的别名:名 → `Some(text)`(命中) | `None`(缺省的可选槽)。
26/// 即 `ParsedCommand.named_captures` 的类型,`FromSlots::from_slots` 据此投影。
27pub type NamedCaptures = HashMap<Cow<'static, str>, Option<String>>;
28
29/// 在框架边界把**单个**捕获组文本解析成一个字段值。`FromArg` 的兄弟,复用 `ArgError`。
30///
31/// `from_capture` 总是看到**恰好一个**捕获组 → 一个 `&str`,且**无从**得知槽的分隔符
32/// (那住在 `SlotSpec`/derive 里,从不被穿进来)——故 tuple **不**实现 `SlotValue`,
33/// 由 derive 在多组层处理。
34pub trait SlotValue: Sized {
35 /// 期望类型的人读名(报错用)。
36 const TYPE_NAME: &'static str;
37 /// 把单个捕获组文本解析成 `Self`。
38 fn from_capture(s: &str) -> Result<Self, ArgError>;
39}
40
41/// 空白桥:任意 `FromArg` 类型即是 `SlotValue`——复用 String/整数/Uin 实现**及**每个
42/// `#[derive(ArgEnum)]` 类型。`FromArg` 对 tuple 无实现(`args.rs`),故与未来手写的
43/// 单捕获 `SlotValue` 无 coherence 冲突。
44impl<T: FromArg> SlotValue for T {
45 const TYPE_NAME: &'static str = <T as FromArg>::TYPE_NAME;
46 fn from_capture(s: &str) -> Result<Self, ArgError> {
47 T::from_arg(s).ok_or_else(|| ArgError::Parse {
48 field: "<slot>",
49 value: s.to_string(),
50 expected: <T as FromArg>::TYPE_NAME,
51 })
52 }
53}
54
55/// 由 `#[derive(Slots)]` 生成:产出命令头匹配器 + 从命名捕获投影出 `Self`。
56pub trait FromSlots: Sized {
57 /// 命令头构建器(`Matcher::slots(..)`)。
58 fn matcher() -> Matcher;
59 /// 从命名捕获投影出 `Self`;失败 ⇒ `ArgError`(经 `on_parse_miss` 处理)。
60 fn from_slots(caps: &NamedCaptures) -> Result<Self, ArgError>;
61 /// 可选的显式用法串(`#[slots(usage="…")]`);默认 `None`。
62 fn usage() -> Option<&'static str> {
63 None
64 }
65 /// **可枚举的具体命令词**:把「字面块 × 各必填 `union` 块」的笛卡尔积在**编译期**算成一串完整
66 /// 命令词(如 `查看排行榜` / `查看金币榜` …),供 `#[command(slots=..)]` 填进 `TriggerMeta.words`
67 /// → help 像字面命令一样**自动枚举**这些形态。可选块不展开;任一必填块非 `union`(`re`/`tail`,
68 /// 不可枚举)⇒ `&[]`(help 退回按命令名显示)。关联**常量**(非函数)使其可在 `static`/`const`
69 /// 上下文里引用(`#[command]` 注册即静态)。默认 `&[]`。用于 help 的命令词解析("help 查看金币榜")。
70 const COMMAND_WORDS: &'static [&'static str] = &[];
71 /// **用法模板**:把区块序列渲成一行(固定块原样、必填捕获块 `<选项|…>`、可选捕获块 `[选项|…]`),
72 /// 如 `查看<金币|等级|发言>榜[全局|全站]`。供 `#[command(slots=..)]` 填进 `TriggerMeta.synopsis`
73 /// → help 直接当用法行显示(把选项当**参数**呈现,而非别名)。默认空串。
74 const SLOTS_SYNOPSIS: &'static str = "";
75 /// **各捕获槽的参数规格**:每个命名槽产一条 [`ArgSpec`](必填/可选、可选值列进 `desc`),供
76 /// `#[command(slots=..)]` 填进 `TriggerMeta.args` → help 在「参数」区逐行列出(如 `board` 是
77 /// 选择参数、`全局` 是可选开关)。与 `#[derive(Args)]` 的 `SPECS` 同一个 help 渲染通道。默认空。
78 const SLOTS: &'static [ArgSpec] = &[];
79}
80
81/// 类型化命名 slot 提取器:`async fn h(m: Slots<ViewBoard>)`。
82/// `Args<T>` 的字面孪生——同样仅在命令匹配后(`ParsedCommand` 存在)可用,解析失败经**共享**
83/// `on_parse_miss`(prod usage 回贴 / dev WARN / 静默)后 `Skip`。
84pub struct Slots<T>(pub T);
85
86#[async_trait]
87impl<T: FromSlots + Send> FromContext for Slots<T> {
88 async fn from_context(ctx: &Ctx) -> Extracted<Self> {
89 let p = ctx.get_ext::<ParsedCommand>().ok_or(Reject::Skip)?; // = Args<T> 的入口。
90 match T::from_slots(&p.named_captures) {
91 Ok(v) => Ok(Slots(v)),
92 Err(e) => {
93 on_parse_miss(ctx, &p.command, &e, T::usage()).await;
94 Err(Reject::Skip)
95 }
96 }
97 }
98}
99
100/// 贪婪尾槽的载荷类型。`#[slot(tail)] q: Option<String>` 直接用 `String`
101/// (无需 `Tail` 包装);`Tail<Vec<Segment>>` 把未解析的多模态尾原样交还(此实现下尾是纯文本,
102/// 故包成单个 `Segment::Text`——`from_slots` 仅见文本捕获)。
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct Tail<T>(pub T);
105
106/// 从尾捕获文本构造一个尾载荷。derive 对 `#[slot(tail)]` 字段调用它。
107pub trait FromTailText: Sized {
108 fn from_tail_text(s: &str) -> Self;
109}
110
111impl FromTailText for String {
112 fn from_tail_text(s: &str) -> Self {
113 s.to_string()
114 }
115}
116
117impl<T: FromTailText> FromTailText for Tail<T> {
118 fn from_tail_text(s: &str) -> Self {
119 Tail(T::from_tail_text(s))
120 }
121}
122
123impl FromTailText for Vec<Segment> {
124 fn from_tail_text(s: &str) -> Self {
125 if s.is_empty() {
126 Vec::new()
127 } else {
128 vec![Segment::text(s)]
129 }
130 }
131}