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 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(|¶m_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 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 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 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 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}