Skip to main content

nagisa_core/
args.rs

1//! 声明式命令参数解析:`#[derive(Args)]` + `Args<T>` 提取器,**在段流上有序解析**。
2//!
3//! 命令头(由匹配器消费)之后的剩余消息段被切成一个 **token 流**——
4//! 文本段切成 `Word` token,非文本段(图片/@/回复/表情…)保留成 `Element` token,**顺序不变**。
5//! 于是位置参数既能是文本(`from: String`)也能是元素(`#[arg(image)] pic: Media`),
6//! 还支持 `--opt v` / `-x` 旗标。元素**必填字段缺失 → `Skip`(命令不触发)**;
7//! `Option<T>` = 可选;`#[arg(rest)]` 收尾。
8use crate::ctx::Ctx;
9use crate::extract::{Extracted, FromContext, Reject};
10use crate::matcher::ParsedCommand;
11use async_trait::async_trait;
12use nagisa_types::id::{MessageId, Uin};
13use nagisa_types::resource::Media;
14use nagisa_types::segment::Segment;
15
16/// 参数解析错误(由生成代码产出,提取器据此 `Skip`)。
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ArgError {
19    /// 缺少必填参数 `field`(文本或元素)。
20    Missing(&'static str),
21    /// `field` 的值 `value` 无法解析为 `expected` 类型。
22    Parse { field: &'static str, value: String, expected: &'static str },
23}
24
25impl std::fmt::Display for ArgError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            ArgError::Missing(field) => write!(f, "missing required argument `{field}`"),
29            ArgError::Parse { field, value, expected } => {
30                write!(f, "argument `{field}`: cannot parse {value:?} as {expected}")
31            }
32        }
33    }
34}
35impl std::error::Error for ArgError {}
36
37/// 把单个文本 token 解析成一个字段值。`#[derive(Args)]` 的文本字段类型需实现它。
38pub trait FromArg: Sized {
39    /// 期望类型的人读名(用于报错)。
40    const TYPE_NAME: &'static str;
41    fn from_arg(s: &str) -> Option<Self>;
42}
43
44macro_rules! from_arg_via_fromstr {
45    ($($t:ty => $name:literal),* $(,)?) => {$(
46        impl FromArg for $t {
47            const TYPE_NAME: &'static str = $name;
48            fn from_arg(s: &str) -> Option<Self> { s.parse().ok() }
49        }
50    )*};
51}
52from_arg_via_fromstr! {
53    String => "string", i64 => "int", i32 => "int", i16 => "int", i8 => "int",
54    u64 => "uint", u32 => "uint", u16 => "uint", u8 => "uint",
55    f64 => "number", bool => "bool",
56}
57
58impl FromArg for Uin {
59    const TYPE_NAME: &'static str = "uin";
60    fn from_arg(s: &str) -> Option<Self> {
61        // 兼容 "@123" / "123"。
62        s.trim_start_matches('@').parse::<i64>().ok().map(Uin)
63    }
64}
65
66/// 参数 token:文本词 或 一个非文本消息段。
67#[derive(Clone, Copy, Debug)]
68pub enum ArgToken<'a> {
69    Word(&'a str),
70    Element(&'a Segment),
71}
72
73/// 把剩余消息段切成 token 流:文本段按空白切词,非文本段各成一个 `Element` token,顺序保留。
74pub fn tokenize_segments(args: &[Segment]) -> Vec<ArgToken<'_>> {
75    let mut out = Vec::new();
76    for seg in args {
77        match seg {
78            Segment::Text(t) => out.extend(t.split_whitespace().map(ArgToken::Word)),
79            other => out.push(ArgToken::Element(other)),
80        }
81    }
82    out
83}
84
85// —— 元素 → 字段值 抽取(供 `#[arg(image/at/reply/face/record/video)]` 生成代码调用)。——
86pub fn seg_as_image(seg: &Segment) -> Option<Media> {
87    match seg {
88        Segment::Image { res, .. } => Some(res.clone()),
89        _ => None,
90    }
91}
92pub fn seg_as_record(seg: &Segment) -> Option<Media> {
93    match seg {
94        Segment::Record { res, .. } => Some(res.clone()),
95        _ => None,
96    }
97}
98pub fn seg_as_video(seg: &Segment) -> Option<Media> {
99    match seg {
100        Segment::Video { res, .. } => Some(res.clone()),
101        _ => None,
102    }
103}
104pub fn seg_as_at(seg: &Segment) -> Option<Uin> {
105    match seg {
106        Segment::Mention { user, .. } => Some(*user),
107        _ => None,
108    }
109}
110pub fn seg_as_reply(seg: &Segment) -> Option<MessageId> {
111    match seg {
112        Segment::Reply { id, .. } => Some(id.clone()),
113        _ => None,
114    }
115}
116pub fn seg_as_face(seg: &Segment) -> Option<String> {
117    match seg {
118        Segment::Face { id, .. } => Some(id.clone()),
119        _ => None,
120    }
121}
122
123/// 跳过 `text` 前 `k` 个空白分隔词,返回其后的**原文**(保留内部空白/换行;
124/// 仅去掉到正文首字符前的分隔空白)。用于 `#[arg(rest, raw)]` 的正文保真。
125pub fn skip_words(text: &str, k: usize) -> String {
126    let mut rest = text.trim_start();
127    for _ in 0..k {
128        match rest.find(char::is_whitespace) {
129            Some(pos) => rest = rest[pos..].trim_start(),
130            None => {
131                rest = "";
132                break;
133            }
134        }
135    }
136    rest.to_string()
137}
138
139/// 由 `#[derive(Args)]` 生成。把 token 流(+ 原始文本,供 `#[arg(rest, raw)]` 保真)解析为 `Self`。
140pub trait ParseArgs: Sized {
141    fn parse_args(tokens: &[ArgToken<'_>], raw_text: &str) -> std::result::Result<Self, ArgError>;
142}
143
144/// 参数角色,决定 help 里的写法(旗标 `[-a]` / 选项 `[-r 值]` / 位置 `<名>` 等)。
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub enum ArgKind {
147    /// 布尔旗标,如 `-a` / `--anonymous`。
148    Flag,
149    /// 取值选项,如 `-r 值` / `--remaining 值`。
150    Opt,
151    /// 文本位置参。
152    Positional,
153    /// 收尾自由文本。
154    Rest,
155    /// `@某人` 或裸 QQ 号。
156    AtOrId,
157    /// 消息元素(图片 / at / 回复…)。
158    Element,
159}
160
161/// 一个命令参数的元数据(由 `#[derive(Args)]` 生成),供 help 自动生成用法说明。
162#[derive(Clone, Copy, Debug)]
163pub struct ArgSpec {
164    /// 显示名(`#[arg(name="…")]`,缺则字段标识符)。
165    pub name: &'static str,
166    /// 角色(旗标 / 选项 / 位置 / …)。
167    pub kind: ArgKind,
168    /// 短旗标字符(无则空串),如 `a`。
169    pub short: &'static str,
170    /// 长旗标名(无则空串),如 `anonymous`。
171    pub long: &'static str,
172    /// 是否必填(非 `Option`、无 `default` 的位置 / 元素 / at_or_id 参)。
173    pub required: bool,
174    /// 默认值(`#[arg(default="…")]`,无则空串)。
175    pub default: &'static str,
176    /// 一句话说明(`#[arg(desc="…")]`,无则空串)。
177    pub desc: &'static str,
178}
179
180/// 由 `#[derive(Args)]` 生成,暴露各字段的 [`ArgSpec`],供 help 自动生成用法。无字段或手写
181/// `ParseArgs` 的类型可不实现(此时 help 退回命令级 `usage` 文本)。
182pub trait ArgsMeta {
183    /// 按声明顺序排列的参数规格。
184    const SPECS: &'static [ArgSpec];
185}
186
187/// 类型化参数提取器:`async fn h(args: Args<MyArgs>)`。
188/// 仅在命令匹配后(`ParsedCommand` 存在)可用;解析失败 → `Skip`(=本 handler 不触发)。
189pub struct Args<T>(pub T);
190
191#[async_trait]
192impl<T: ParseArgs + Send> FromContext for Args<T> {
193    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
194        let parsed = ctx.get_ext::<ParsedCommand>().ok_or(Reject::Skip)?;
195        let tokens = tokenize_segments(&parsed.args);
196        // args_text 是各文本段原文拼接(空白保真),供 `#[arg(rest, raw)]` 取正文。
197        match T::parse_args(&tokens, &parsed.args_text) {
198            Ok(v) => Ok(Args(v)),
199            Err(e) => {
200                // 显式 `#[command(usage="…")]` 串优先于 dev 自动 hint;
201                // 共享同一 parse-miss 策略(Args / Slots / usage= 三处同源)。
202                let usage = ctx.get_ext::<crate::matcher::CommandUsage>().map(|crate::matcher::CommandUsage(u)| u);
203                on_parse_miss(ctx, &parsed.command, &e, usage.as_deref()).await;
204                Err(Reject::Skip)
205            }
206        }
207    }
208}
209
210/// 决定一次 head/tail 解析失败该做什么。**Args 与 Slots 共用一份策略**:
211///   - prod + `usage` 存在 ⇒ 回贴该 usage 串,然后 Skip。
212///   - dev(`App::debug()`)⇒ WARN + 回贴自动 usage_hint(既有 Args 行为),然后 Skip。
213///   - prod + 无 usage ⇒ `debug!` 日志、静默 Skip(既有 Args 行为)。
214///
215/// 优先级:显式 `usage=` **总是**胜过 dev 自动 hint。把既有 `Args` 的内联 dev-WARN
216/// 分支原样搬到这里——`Args` 行为字节级保持不变,只是策略现在被 `Args`/`Slots` 共用。
217pub(crate) async fn on_parse_miss(ctx: &Ctx, command: &str, err: &ArgError, usage: Option<&str>) {
218    // 显式 usage:无论 dev 与否都回贴该串后返回(优先于自动 hint)。
219    if let Some(usage) = usage {
220        tracing::debug!(command = %command, error = %err, "parse failed; replying explicit usage");
221        if let Some(m) = ctx.message() {
222            let _ = ctx.bot().send(&m.peer, &[Segment::text(usage)]).await;
223        }
224        return;
225    }
226    // 否则:prod 走 `debug!`(参数没中是预期分支,不刷屏);dev 升级 WARN + 回贴自动 hint。
227    if ctx.is_dev() {
228        let hint = usage_hint(command, err);
229        tracing::warn!(
230            command = %command,
231            error = %err,
232            "[dev] parse failed; skipping handler — {hint}"
233        );
234        if let Some(m) = ctx.message() {
235            let _ = ctx.bot().send(&m.peer, &[Segment::text(hint)]).await;
236        }
237    } else {
238        tracing::debug!(error = %err, "parse failed; skipping handler");
239    }
240}
241
242/// 为一次失败的 `Args<T>` 解析构造一条人读用法提示,如
243/// `用法错误: 命令 `transfer` — argument `amount`: cannot parse "x" as int`。
244/// 由 dev 模式的 WARN + 可选的用法自动回贴使用。
245fn usage_hint(command: &str, err: &ArgError) -> String {
246    format!("用法错误: 命令 `{command}` — {err}")
247}