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