smart_config_commands/
help.rs

1use std::{io, io::Write as _};
2
3use anstream::stream::{AsLockedWrite, RawStream};
4use anstyle::{AnsiColor, Color, Style};
5use smart_config::{
6    ConfigRef, ConfigSchema,
7    metadata::{BasicTypes, ConfigTag, ConfigVariant, TypeDescription, TypeSuffixes},
8    pat::{PatternDisplay, RawStr},
9};
10
11use crate::{
12    CONFIG_PATH, ParamRef, Printer,
13    utils::{NULL, STRING, write_json_value},
14};
15
16const INDENT: &str = "  ";
17const DIMMED: Style = Style::new().dimmed();
18const MAIN_NAME: Style = Style::new().bold();
19const DEPRECATED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
20const DEFAULT_VARIANT: Style = Style::new().bold();
21const FIELD: Style = Style::new().underline();
22const UNIT: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
23const SECRET: Style = Style::new()
24    .bg_color(Some(Color::Ansi(AnsiColor::Cyan)))
25    .fg_color(None);
26
27fn collect_conditions(mut config: ConfigRef<'_>) -> Vec<(ParamRef<'_>, &ConfigVariant)> {
28    let mut conditions = vec![];
29    while let Some((parent_ref, this_ref)) = config.parent_link() {
30        if let Some(variant) = this_ref.tag_variant {
31            conditions.push((ParamRef::for_tag(parent_ref), variant));
32        }
33        config = parent_ref;
34    }
35    conditions
36}
37
38impl<W: RawStream + AsLockedWrite> Printer<W> {
39    /// Prints help on config params in the provided `schema`. Params can be filtered by the supplied predicate.
40    ///
41    /// # Errors
42    ///
43    /// Propagates I/O errors.
44    pub fn print_help(
45        self,
46        schema: &ConfigSchema,
47        mut filter: impl FnMut(ParamRef<'_>) -> bool,
48    ) -> io::Result<()> {
49        let mut writer = self.writer;
50        for config in schema.iter() {
51            let conditions = collect_conditions(config);
52
53            let mut filtered_params: Vec<_> = config
54                .metadata()
55                .params
56                .iter()
57                .map(|param| ParamRef { config, param })
58                .filter(|&param_ref| filter(param_ref))
59                .collect();
60            if filtered_params.is_empty() {
61                continue;
62            }
63
64            let validations = config.metadata().validations;
65            if !validations.is_empty() {
66                write_config_help(&mut writer, config)?;
67                writeln!(&mut writer)?;
68            }
69
70            if let Some(tag) = &config.metadata().tag {
71                write_tag_help(&mut writer, config, tag, &conditions)?;
72                // Do not output the tag param twice.
73                filtered_params
74                    .retain(|param| param.param.rust_field_name != tag.param.rust_field_name);
75                writeln!(&mut writer)?;
76            }
77
78            for param_ref in filtered_params {
79                param_ref.write_help(&mut writer, &conditions)?;
80                writeln!(&mut writer)?;
81            }
82        }
83        Ok(())
84    }
85}
86
87fn write_config_help(writer: &mut impl io::Write, config: ConfigRef<'_>) -> io::Result<()> {
88    writeln!(
89        writer,
90        "{MAIN_NAME}{CONFIG_PATH}{}{CONFIG_PATH:#}{MAIN_NAME:#}",
91        config.prefix()
92    )?;
93    for (alias, options) in config.aliases() {
94        let config_style = if options.is_deprecated {
95            CONFIG_PATH.strikethrough()
96        } else {
97            CONFIG_PATH
98        };
99        write!(writer, "{config_style}{alias}{config_style:#}")?;
100        if options.is_deprecated {
101            writeln!(writer, " {DEPRECATED}[deprecated alias]{DEPRECATED:#}")?;
102        } else {
103            writeln!(writer)?;
104        }
105    }
106    writeln!(
107        writer,
108        "{INDENT}{FIELD}Config{FIELD:#}: {}",
109        config.metadata().ty.name_in_code()
110    )?;
111
112    writeln!(writer, "{INDENT}{FIELD}Validations{FIELD:#}:")?;
113    for &validation in config.metadata().validations {
114        let description = validation.to_string();
115        writeln!(writer, "{INDENT}- {description}")?;
116    }
117    Ok(())
118}
119
120fn write_tag_help(
121    writer: &mut impl io::Write,
122    config: ConfigRef<'_>,
123    tag: &ConfigTag,
124    conditions: &[(ParamRef<'_>, &ConfigVariant)],
125) -> io::Result<()> {
126    ParamRef {
127        config,
128        param: tag.param,
129    }
130    .write_locations(writer)?;
131    writeln!(
132        writer,
133        "{INDENT}{FIELD}Type{FIELD:#}: string tag with variants:"
134    )?;
135
136    let default_variant_name = tag.default_variant.map(|variant| variant.rust_name);
137
138    for variant in tag.variants {
139        let default_marker = if default_variant_name == Some(variant.rust_name) {
140            format!(" {DEFAULT_VARIANT}(default){DEFAULT_VARIANT:#}")
141        } else {
142            String::new()
143        };
144
145        writeln!(
146            writer,
147            "{INDENT}- {STRING}'{name}'{STRING:#} {DIMMED}[Rust: {config_name}::{rust_name}]{DIMMED:#}{default_marker}",
148            name = variant.name,
149            config_name = config.metadata().ty.name_in_code(),
150            rust_name = variant.rust_name
151        )?;
152        if !variant.aliases.is_empty() {
153            write!(writer, "{INDENT}  {FIELD}Aliases{FIELD:#}: ")?;
154            for (i, &alias) in variant.aliases.iter().enumerate() {
155                write!(writer, "{STRING}'{alias}'{STRING:#}")?;
156                if i + 1 < variant.aliases.len() {
157                    write!(writer, ", ")?;
158                }
159            }
160            writeln!(writer)?;
161        }
162
163        if !variant.help.is_empty() {
164            for line in variant.help.lines() {
165                writeln!(writer, "{INDENT}  {line}")?;
166            }
167        }
168    }
169
170    let condition_count = conditions.len();
171    ParamRef::write_tag_conditions(writer, condition_count, conditions.iter().copied())
172}
173
174impl ParamRef<'_> {
175    fn write_locations(&self, writer: &mut impl io::Write) -> io::Result<()> {
176        let all_paths = self.all_paths();
177        let mut main_name = true;
178        for (path, options) in all_paths {
179            let (prefix, name) = path.rsplit_once('.').unwrap_or(("", &path));
180            let prefix_sep = if prefix.is_empty() || prefix.ends_with('.') {
181                ""
182            } else {
183                "."
184            };
185            let name_style = if main_name {
186                MAIN_NAME
187            } else if options.is_deprecated {
188                Style::new().strikethrough()
189            } else {
190                Style::new()
191            };
192            main_name = false;
193            write!(
194                writer,
195                "{DIMMED}{prefix}{prefix_sep}{DIMMED:#}{name_style}{name}{name_style:#}"
196            )?;
197
198            if options.is_deprecated {
199                writeln!(writer, " {DEPRECATED}[deprecated alias]{DEPRECATED:#}")?;
200            } else {
201                writeln!(writer)?;
202            }
203        }
204        Ok(())
205    }
206
207    fn write_help(
208        &self,
209        writer: &mut impl io::Write,
210        conditions: &[(ParamRef<'_>, &ConfigVariant)],
211    ) -> io::Result<()> {
212        self.write_locations(writer)?;
213        let description = self.param.type_description();
214        write_type_description(writer, None, 2, self.param.expecting, &description)?;
215
216        // `conditions` are ordered from most specific to least specific; we want the reverse ordering.
217        let full_conditions = conditions.iter().rev().copied().chain(
218            self.param
219                .tag_variant
220                .map(|variant| (ParamRef::for_tag(self.config), variant)),
221        );
222        let condition_count = conditions.len() + usize::from(self.param.tag_variant.is_some());
223        Self::write_tag_conditions(writer, condition_count, full_conditions)?;
224
225        let default = self.param.default_value_json();
226        if let Some(default) = &default {
227            write!(writer, "{INDENT}{FIELD}Default{FIELD:#}: ")?;
228            write_json_value(writer, default, 2)?;
229            writeln!(writer)?;
230        }
231
232        let example = self
233            .param
234            .example_value_json()
235            .filter(|val| Some(val) != default.as_ref());
236        if let Some(example) = example {
237            write!(writer, "{INDENT}{FIELD}Example{FIELD:#}: ")?;
238            write_json_value(writer, &example, 2)?;
239            writeln!(writer)?;
240        }
241
242        if let Some(fallback) = self.param.fallback {
243            write!(writer, "{INDENT}{FIELD}Fallbacks{FIELD:#}: ")?;
244            let fallback = fallback.to_string();
245            let mut lines = fallback.lines();
246            if let Some(first_line) = lines.next() {
247                writeln!(writer, "{first_line}")?;
248                for line in lines {
249                    writeln!(writer, "{INDENT}  {line}")?;
250                }
251            }
252        }
253
254        if !self.param.help.is_empty() {
255            for line in self.param.help.lines() {
256                writeln!(writer, "{INDENT}{line}")?;
257            }
258        }
259        Ok(())
260    }
261
262    fn write_tag_conditions<'a>(
263        writer: &mut impl io::Write,
264        condition_count: usize,
265        conditions: impl Iterator<Item = (ParamRef<'a>, &'a ConfigVariant)>,
266    ) -> io::Result<()> {
267        if condition_count == 0 {
268            return Ok(());
269        }
270
271        let tag_field = if condition_count == 1 { "Tag" } else { "Tags" };
272        write!(writer, "{INDENT}{FIELD}{tag_field}{FIELD:#}: ")?;
273        for (i, (tag_ref, variant)) in conditions.enumerate() {
274            let tag_name = tag_ref.canonical_path();
275            let variant = variant.name;
276            write!(writer, "{tag_name} == {STRING}'{variant}'{STRING:#}")?;
277            if i + 1 < condition_count {
278                write!(writer, " && ")?;
279            }
280        }
281        writeln!(writer)
282    }
283}
284
285fn write_separator(
286    writer: &mut impl io::Write,
287    relation_to_parent: &str,
288    indent: usize,
289    sep: &PatternDisplay,
290) -> io::Result<()> {
291    const REGEX: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Magenta)));
292
293    write!(
294        writer,
295        "{:>indent$}{FIELD}{relation_to_parent}{FIELD:#}: ",
296        ""
297    )?;
298    match sep {
299        PatternDisplay::Exact(s) => {
300            writeln!(writer, "exact match: {STRING}{s:?}{STRING:#}")
301        }
302        PatternDisplay::Regex(regex) => {
303            // TODO: highlight regex syntax
304            writeln!(writer, "regex: {REGEX}{}{REGEX:#}", RawStr(regex))
305        }
306        _ => writeln!(writer, "{sep}"),
307    }
308}
309
310fn write_type_description(
311    writer: &mut impl io::Write,
312    relation_to_parent: Option<&str>,
313    indent: usize,
314    expecting: BasicTypes,
315    description: &TypeDescription,
316) -> io::Result<()> {
317    let maybe_secret = if description.contains_secrets() {
318        format!("{SECRET}secret{SECRET:#} ")
319    } else {
320        String::new()
321    };
322    let rust_type = description.rust_type();
323    let rust_type = if rust_type.is_empty() {
324        String::new()
325    } else {
326        format!(" {DIMMED}[Rust: {rust_type}]{DIMMED:#}")
327    };
328    let ty = format!("{maybe_secret}{expecting}{rust_type}");
329
330    let details = if let Some(details) = description.details() {
331        format!("; {details}")
332    } else {
333        String::new()
334    };
335    let unit = if let Some(unit) = description.unit() {
336        format!("; unit: {UNIT}{unit}{UNIT:#}")
337    } else {
338        String::new()
339    };
340
341    let field_name = relation_to_parent.unwrap_or("Type");
342    writeln!(
343        writer,
344        "{:>indent$}{FIELD}{field_name}{FIELD:#}: {ty}{details}{unit}",
345        ""
346    )?;
347
348    // Suffixes are only active for top-level types, not for array items etc.
349    if let (None, Some(suffixes)) = (relation_to_parent, description.suffixes()) {
350        let suffixes = match suffixes {
351            TypeSuffixes::DurationUnits => Some(format!(
352                "duration units from millis to weeks, e.g. {STRING}_ms{STRING:#} or {STRING}_in_sec{STRING:#}"
353            )),
354            TypeSuffixes::SizeUnits => Some(format!(
355                "byte size units up to gigabytes, e.g. {STRING}_mb{STRING:#} or {STRING}_in_kib{STRING:#}"
356            )),
357            TypeSuffixes::EtherUnits => Some(format!(
358                "ether value units, e.g. {STRING}_gwei{STRING:#} or {STRING}_in_ether{STRING:#}"
359            )),
360            _ => None,
361        };
362        if let Some(suffixes) = &suffixes {
363            writeln!(
364                writer,
365                "{:>indent$}{FIELD}Name suffixes{FIELD:#}: {suffixes}",
366                ""
367            )?;
368        }
369    }
370
371    let validations = description.validations();
372    if !validations.is_empty() {
373        writeln!(writer, "{:>indent$}{FIELD}Validations{FIELD:#}:", "")?;
374        for validation in validations {
375            writeln!(writer, "{:>indent$}- {validation}", "")?;
376        }
377    }
378
379    if let Some(condition) = description.deserialize_if() {
380        writeln!(
381            writer,
382            "{:>indent$}{FIELD}Filtering{FIELD:#}: {condition}, otherwise set to {NULL}null{NULL:#}",
383            ""
384        )?;
385    }
386
387    if let Some((expecting, item)) = description.items() {
388        write_type_description(writer, Some("Array items"), indent + 2, expecting, item)?;
389    }
390    if let Some(sep) = description.item_separator() {
391        write_separator(writer, "Item separator", indent + 2, sep)?;
392    }
393
394    if let Some((expecting, key)) = description.keys() {
395        write_type_description(writer, Some("Map keys"), indent + 2, expecting, key)?;
396    }
397    if let Some((expecting, value)) = description.values() {
398        write_type_description(writer, Some("Map values"), indent + 2, expecting, value)?;
399    }
400    if let Some((entry_sep, kv_sep)) = description.entry_separators() {
401        write_separator(writer, "Entries separator", indent + 2, entry_sep)?;
402        write_separator(writer, "Key–value separator", indent + 2, kv_sep)?;
403    }
404
405    if let Some((expecting, fallback)) = description.fallback() {
406        write_type_description(writer, Some("Fallback"), indent + 2, expecting, fallback)?;
407    }
408
409    Ok(())
410}