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