Skip to main content

xyz_rust/cli/
tree.rs

1// 命令树:点分注册名映射为多层子命令段;别名挂在父层查找路径上
2// (不出现在父帮助的命令列表里)。叶子节点携带 flag 定义、env 注入点
3// 与位置参数约束。
4
5use std::sync::Arc;
6
7use crate::cli::parse::FlagDef;
8use crate::errors;
9use crate::registry::Registry;
10use crate::spec::entry::Entry;
11use crate::spec::field::{FieldKind, FieldMeta};
12
13#[derive(Clone)]
14pub struct CmdNode {
15    /// 完整点分路径,如 user.add。
16    pub path: String,
17    /// 本段命令名(帮助列表与下沉匹配用)。
18    pub segment: String,
19    pub usage: String,
20    pub short: String,
21    pub long: String,
22    pub aliases: Vec<String>,
23    pub hidden: bool,
24    pub leaf: bool,
25    pub entry: Option<Arc<Entry>>,
26    pub defs: Vec<FlagDef>,
27    /// #[xyz(skip)] 且配置了 env 的注入字段。
28    pub env_only: Vec<FieldMeta>,
29    pub pos_f: Vec<FieldMeta>,
30    pub min_pos: usize,
31    pub max_pos: usize,
32    /// 有序子节点(按段名排序;别名不占列表位)。
33    pub children: Vec<CmdNode>,
34    /// 默认子命令的末段名(CliHints.default);执行期未匹配命令段时整串
35    /// 参数转发给它。
36    pub default_segment: Option<String>,
37}
38
39impl CmdNode {
40    fn new(segment: &str) -> Self {
41        CmdNode {
42            path: String::new(),
43            segment: segment.to_string(),
44            usage: String::new(),
45            short: String::new(),
46            long: String::new(),
47            aliases: Vec::new(),
48            hidden: false,
49            leaf: false,
50            entry: None,
51            defs: Vec::new(),
52            env_only: Vec::new(),
53            pos_f: Vec::new(),
54            min_pos: 0,
55            max_pos: 0,
56            children: Vec::new(),
57            default_segment: None,
58        }
59    }
60}
61
62/// 由注册表构建命令树。冲突(路径重叠、别名撞名、短名重复、required
63/// 位置参数排在 optional 之后)全部是构建期错误。
64pub fn build_tree(reg: &Registry) -> errors::Result<CmdNode> {
65    let mut root = CmdNode::new("");
66    for e in reg.all() {
67        add_entry(&mut root, &e)?;
68    }
69    for child in &mut root.children {
70        sort_node(child);
71    }
72    root.children.sort_by(|a, b| a.segment.cmp(&b.segment));
73    Ok(root)
74}
75
76fn sort_node(node: &mut CmdNode) {
77    for child in &mut node.children {
78        sort_node(child);
79    }
80    node.children.sort_by(|a, b| a.segment.cmp(&b.segment));
81}
82
83fn add_entry(root: &mut CmdNode, e: &Arc<Entry>) -> errors::Result<()> {
84    let parts: Vec<&str> = e.name.split('.').collect();
85    add_parts(root, &parts, 0, e)
86}
87
88fn add_parts(node: &mut CmdNode, parts: &[&str], idx: usize, e: &Arc<Entry>) -> errors::Result<()> {
89    if e.cli.skip {
90        return Ok(()); // 通道层面整体移除:不建子命令、别名、completion
91    }
92    let part = parts[idx];
93    let is_leaf_segment = idx == parts.len() - 1;
94    if !is_leaf_segment {
95        if !node.children.iter().any(|c| c.segment == part) {
96            node.children.push(CmdNode::new(part));
97        }
98        let idx_of = node
99            .children
100            .iter()
101            .position(|c| c.segment == part)
102            .unwrap();
103        return add_parts(&mut node.children[idx_of], parts, idx + 1, e);
104    }
105
106    // 叶子落点:已存在同名节点(且已是叶子或已带子树)即冲突。
107    match node.children.iter().position(|c| c.segment == part) {
108        Some(i) => {
109            if node.children[i].leaf || !node.children[i].children.is_empty() {
110                return Err(errors::Error::new(
111                    errors::Kind::Internal,
112                    format!(
113                        "cli: command {:?} conflicts with an existing command path",
114                        e.name
115                    ),
116                ));
117            }
118            // 别名挂在父节点查找表上(等价于子命令名,不进帮助列表)。
119            for alias in &e.cli.aliases {
120                if node
121                    .children
122                    .iter()
123                    .enumerate()
124                    .any(|(j, c)| j != i && c.segment == *alias)
125                {
126                    return Err(errors::Error::new(
127                        errors::Kind::Internal,
128                        format!(
129                            "cli: command {:?}: alias {:?} collides with an existing command path",
130                            e.name, alias
131                        ),
132                    ));
133                }
134            }
135            fill_leaf(&mut node.children[i], e)?;
136            set_default(node, part, e)?;
137            Ok(())
138        }
139        None => {
140            for alias in &e.cli.aliases {
141                if node.children.iter().any(|c| c.segment == *alias) {
142                    return Err(errors::Error::new(
143                        errors::Kind::Internal,
144                        format!(
145                            "cli: command {:?}: alias {:?} collides with an existing command path",
146                            e.name, alias
147                        ),
148                    ));
149                }
150            }
151            let mut leaf = CmdNode::new(part);
152            fill_leaf(&mut leaf, e)?;
153            node.children.push(leaf);
154            set_default(node, part, e)?;
155            Ok(())
156        }
157    }
158}
159
160/// Default:登记为父节点的默认子命令,一个父节点最多一个(注册期报错)。
161fn set_default(node: &mut CmdNode, part: &str, e: &Arc<Entry>) -> errors::Result<()> {
162    if !e.cli.default {
163        return Ok(());
164    }
165    if let Some(prev) = &node.default_segment {
166        if prev != part {
167            return Err(errors::Error::new(
168                errors::Kind::Internal,
169                format!(
170                    "cli: command {:?}: default conflicts with existing default {:?}",
171                    e.name, prev
172                ),
173            ));
174        }
175        return Ok(());
176    }
177    node.default_segment = Some(part.to_string());
178    Ok(())
179}
180
181fn fill_leaf(node: &mut CmdNode, e: &Arc<Entry>) -> errors::Result<()> {
182    node.path = e.name.clone();
183    node.usage = e.cli.usage.clone();
184    node.short = e.summary.clone();
185    node.long = e.description.clone();
186    node.aliases = e.cli.aliases.clone();
187    node.hidden = e.cli.hidden;
188    node.leaf = true;
189    node.entry = Some(Arc::clone(e));
190
191    let mut seen_sh: Vec<(char, String)> = Vec::new();
192    for f in &e.root.children {
193        if f.cli.skip {
194            continue;
195        }
196        if f.skip {
197            // json:"-" 字段不生成 flag;配置了 env 时注册纯注入点。
198            if f.cli.env_var.is_some() {
199                node.env_only.push(f.clone());
200            }
201            continue;
202        }
203        if f.cli.positional {
204            node.pos_f.push(f.clone());
205            continue;
206        }
207        let (kind, ok) = flag_kind_for(f, &e.name)?;
208        if !ok {
209            continue;
210        }
211        if let Some(sh) = f.cli.shorthand {
212            if let Some((_, prev)) = seen_sh.iter().find(|(c, _)| *c == sh) {
213                return Err(errors::Error::new(
214                    errors::Kind::Internal,
215                    format!(
216                        "cli: command {:?}: shorthand {sh:?} of field {:?} already used by {prev:?}",
217                        e.name, f.json_name
218                    ),
219                ));
220            }
221            seen_sh.push((sh, f.json_name.clone()));
222        }
223        node.defs.push(FlagDef {
224            long: f.json_name.clone(),
225            short: f.cli.shorthand,
226            kind,
227            field: f.clone(),
228        });
229    }
230
231    // 位置参数:required 必须是前缀,否则语义有歧义。
232    let mut min_pos = 0usize;
233    let mut all_required = true;
234    for (i, f) in node.pos_f.iter().enumerate() {
235        if f.required && !all_required {
236            return Err(errors::Error::new(
237                errors::Kind::Internal,
238                format!(
239                    "cli: command {:?}: required positional {:?} must not follow optional ones",
240                    e.name, f.json_name
241                ),
242            ));
243        }
244        if f.required {
245            min_pos = i + 1;
246        } else {
247            all_required = false;
248        }
249    }
250    node.min_pos = min_pos;
251    node.max_pos = node.pos_f.len();
252    Ok(())
253}
254
255/// 判定字段的 flag 类型;不支持的种类在构建时报错(Go flagKindFor 对应物)。
256fn flag_kind_for(
257    f: &FieldMeta,
258    cmd_name: &str,
259) -> errors::Result<(crate::cli::parse::FlagKind, bool)> {
260    use crate::cli::parse::FlagKind::{Bool, Slice, Str};
261    match f.kind {
262        FieldKind::Bool => Ok((Bool, true)),
263        FieldKind::Slice => {
264            // []byte 除外:字节字段按字符串进(属于 scalar 路径)。
265            if matches!(
266                f.elem.as_deref().map(|e| e.kind),
267                Some(FieldKind::Struct | FieldKind::Ptr | FieldKind::Slice)
268            ) {
269                return Err(errors::Error::new(
270                    errors::Kind::Internal,
271                    format!(
272                        "cli: command {cmd_name:?}: field {:?}: slice of {:?} is not supported by the CLI frontend yet",
273                        f.json_name,
274                        f.elem.as_deref().map(|e| e.kind)
275                    ),
276                ));
277            }
278            Ok((Slice, true))
279        }
280        FieldKind::Struct => Err(errors::Error::new(
281            errors::Kind::Internal,
282            format!(
283                "cli: command {cmd_name:?}: field {:?}: nested struct is not supported by the CLI frontend yet",
284                f.json_name
285            ),
286        )),
287        FieldKind::Union => {
288            // spec §4.7 尾部政策:CLI 无法原生表达的联合字段跳过
289            // (该命令其余字段照常、整个 CLI 不受影响)。完整 flag 形态
290            // 是后续迭代。
291            Ok((Str, false))
292        }
293        FieldKind::Ptr => {
294            if matches!(f.elem.as_deref().map(|e| e.kind), Some(FieldKind::Struct)) {
295                return Err(errors::Error::new(
296                    errors::Kind::Internal,
297                    format!(
298                        "cli: command {cmd_name:?}: field {:?}: pointer to struct is not supported by the CLI frontend yet",
299                        f.json_name
300                    ),
301                ));
302            }
303            Ok((Str, true))
304        }
305        _ => Ok((Str, true)),
306    }
307}