1use std::collections::HashMap;
2
3use crate::policy::FlagStyle;
4use crate::verdict::SafetyLevel;
5
6use super::types::*;
7
8pub(super) fn build_policy(
9 standalone: Vec<String>,
10 valued: Vec<String>,
11 bare: Option<bool>,
12 max_positional: Option<usize>,
13 positional_style: Option<bool>,
14 numeric_dash: Option<bool>,
15) -> OwnedPolicy {
16 OwnedPolicy {
17 standalone,
18 valued,
19 bare: bare.unwrap_or(true),
20 max_positional,
21 flag_style: if positional_style.unwrap_or(false) {
22 FlagStyle::Positional
23 } else {
24 FlagStyle::Strict
25 },
26 numeric_dash: numeric_dash.unwrap_or(false),
27 }
28}
29
30fn allow_all_policy() -> OwnedPolicy {
31 OwnedPolicy {
32 standalone: Vec::new(),
33 valued: Vec::new(),
34 bare: true,
35 max_positional: None,
36 flag_style: FlagStyle::Positional,
37 numeric_dash: false,
38 }
39}
40
41fn filter_candidates(subs: Vec<TomlSub>) -> impl Iterator<Item = TomlSub> {
42 subs.into_iter().filter(|s| !s.candidate.unwrap_or(false))
43}
44
45pub(super) fn build_sub(toml: TomlSub) -> SubSpec {
46 if let Some(handler_name) = toml.handler {
47 return SubSpec {
48 name: toml.name,
49 kind: DispatchKind::Custom { handler_name },
50 };
51 }
52
53 if toml.allow_all.unwrap_or(false) {
54 return SubSpec {
55 name: toml.name,
56 kind: DispatchKind::Policy {
57 policy: allow_all_policy(),
58 level: toml.level.unwrap_or(TomlLevel::Inert).into(),
59 },
60 };
61 }
62
63 if let Some(sep) = toml.delegate_after {
64 return SubSpec {
65 name: toml.name,
66 kind: DispatchKind::DelegateAfterSeparator { separator: sep },
67 };
68 }
69
70 if let Some(skip) = toml.delegate_skip {
71 return SubSpec {
72 name: toml.name,
73 kind: DispatchKind::DelegateSkip { skip },
74 };
75 }
76
77 if !toml.sub.is_empty() {
78 return SubSpec {
79 name: toml.name,
80 kind: DispatchKind::Branching {
81 subs: filter_candidates(toml.sub).map(build_sub).collect(),
82 bare_flags: Vec::new(),
83 bare_ok: toml.nested_bare.unwrap_or(false),
84 pre_standalone: toml.standalone,
85 pre_valued: toml.valued,
86 first_arg: Vec::new(),
87 first_arg_level: SafetyLevel::Inert,
88 },
89 };
90 }
91
92 let policy = build_policy(
93 toml.standalone,
94 toml.valued,
95 toml.bare,
96 toml.max_positional,
97 toml.positional_style,
98 toml.numeric_dash,
99 );
100 let level: SafetyLevel = toml.level.unwrap_or(TomlLevel::Inert).into();
101
102 if !toml.write_flags.is_empty() {
103 return SubSpec {
104 name: toml.name,
105 kind: DispatchKind::WriteFlagged {
106 policy,
107 base_level: level,
108 write_flags: toml.write_flags,
109 },
110 };
111 }
112
113 if let Some(guard) = toml.guard {
114 let mut require_any = vec![guard];
115 if let Some(short) = toml.guard_short {
116 require_any.push(short);
117 }
118 return SubSpec {
119 name: toml.name,
120 kind: DispatchKind::RequireAny {
121 require_any,
122 policy,
123 level,
124 accept_bare_help: true,
125 },
126 };
127 }
128
129 if !toml.first_arg.is_empty() {
130 return SubSpec {
131 name: toml.name,
132 kind: DispatchKind::FirstArg {
133 patterns: toml.first_arg,
134 level,
135 },
136 };
137 }
138
139 if !toml.require_any.is_empty() {
140 return SubSpec {
141 name: toml.name,
142 kind: DispatchKind::RequireAny {
143 require_any: toml.require_any,
144 policy,
145 level,
146 accept_bare_help: false,
147 },
148 };
149 }
150
151 SubSpec {
152 name: toml.name,
153 kind: DispatchKind::Policy { policy, level },
154 }
155}
156
157#[allow(clippy::too_many_lines)]
158pub(super) fn build_command(toml: TomlCommand, category: &str) -> CommandSpec {
159 let cat = category.to_string();
160 let desc = toml.description.unwrap_or_default();
161 if let Some(handler_name) = toml.handler {
162 return CommandSpec {
163 name: toml.name,
164 description: desc,
165 aliases: toml.aliases,
166 url: toml.url,
167 category: cat,
168 kind: DispatchKind::Custom { handler_name },
169 };
170 }
171
172 if let Some(w) = toml.wrapper {
173 if !toml.sub.is_empty() || !toml.bare_flags.is_empty() {
174 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
175 return CommandSpec {
176 name: toml.name,
177 description: desc,
178 aliases: toml.aliases,
179 url: toml.url,
180 category: cat,
181 kind: DispatchKind::Branching {
182 bare_flags: toml.bare_flags,
183 subs: filter_candidates(toml.sub).map(build_sub).collect(),
184 pre_standalone: w.standalone,
185 pre_valued: w.valued,
186 bare_ok: toml.bare.unwrap_or(false),
187 first_arg: toml.first_arg,
188 first_arg_level,
189 },
190 };
191 }
192 return CommandSpec {
193 name: toml.name,
194 description: desc,
195 aliases: toml.aliases,
196 url: toml.url,
197 category: cat,
198 kind: DispatchKind::Wrapper {
199 standalone: w.standalone,
200 valued: w.valued,
201 positional_skip: w.positional_skip.unwrap_or(0),
202 separator: w.separator,
203 bare_ok: w.bare_ok.unwrap_or(false),
204 },
205 };
206 }
207
208 if !toml.sub.is_empty() || !toml.bare_flags.is_empty() {
209 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
210 return CommandSpec {
211 name: toml.name,
212 description: desc,
213 aliases: toml.aliases,
214 url: toml.url,
215 category: cat,
216 kind: DispatchKind::Branching {
217 bare_flags: toml.bare_flags,
218 subs: filter_candidates(toml.sub).map(build_sub).collect(),
219 pre_standalone: Vec::new(),
220 pre_valued: Vec::new(),
221 bare_ok: toml.bare.unwrap_or(false),
222 first_arg: toml.first_arg,
223 first_arg_level,
224 },
225 };
226 }
227
228 let policy = build_policy(
229 toml.standalone,
230 toml.valued,
231 toml.bare,
232 toml.max_positional,
233 toml.positional_style,
234 toml.numeric_dash,
235 );
236
237 let level = toml.level.unwrap_or(TomlLevel::Inert).into();
238
239 if !toml.first_arg.is_empty() {
240 return CommandSpec {
241 name: toml.name,
242 description: desc,
243 aliases: toml.aliases,
244 url: toml.url,
245 category: cat,
246 kind: DispatchKind::FirstArg {
247 patterns: toml.first_arg,
248 level,
249 },
250 };
251 }
252
253 if !toml.require_any.is_empty() {
254 return CommandSpec {
255 name: toml.name,
256 description: desc,
257 aliases: toml.aliases,
258 url: toml.url,
259 category: cat,
260 kind: DispatchKind::RequireAny {
261 require_any: toml.require_any,
262 policy,
263 level,
264 accept_bare_help: false,
265 },
266 };
267 }
268
269 CommandSpec {
270 name: toml.name,
271 description: desc,
272 aliases: toml.aliases,
273 url: toml.url,
274 category: cat,
275 kind: DispatchKind::Policy {
276 policy,
277 level,
278 },
279 }
280}
281
282pub fn load_toml(source: &str, category: &str) -> Vec<CommandSpec> {
283 let file: TomlFile = match toml::from_str(source) {
284 Ok(f) => f,
285 Err(e) => {
286 let preview: String = source.chars().take(80).collect();
287 panic!("invalid TOML command definition: {e}\n source begins: {preview}");
288 }
289 };
290 file.command.into_iter()
291 .filter(|cmd| !cmd.candidate.unwrap_or(false))
292 .map(|cmd| build_command(cmd, category))
293 .collect()
294}
295
296pub fn build_registry(specs: Vec<CommandSpec>) -> HashMap<String, CommandSpec> {
297 let mut map = HashMap::new();
298 for spec in specs {
299 for alias in &spec.aliases {
300 map.insert(alias.clone(), CommandSpec {
301 name: spec.name.clone(),
302 description: spec.description.clone(),
303 aliases: vec![],
304 url: spec.url.clone(),
305 category: spec.category.clone(),
306 kind: spec.kind.clone(),
307 });
308 }
309 map.insert(spec.name.clone(), spec);
310 }
311 map
312}