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