Skip to main content

safe_chains/registry/
build.rs

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}