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