1use std::{
2 any,
3 collections::{HashMap, HashSet},
4 io::{self, Write as _},
5};
6
7use anstream::stream::{AsLockedWrite, RawStream};
8use anstyle::{AnsiColor, Color, Style};
9use smart_config::{
10 ConfigRepository, ParseError, ParseErrors,
11 metadata::ConfigMetadata,
12 value::{FileFormat, ValueOrigin, WithOrigin},
13 visit::{ConfigVisitor, VisitConfig},
14};
15
16use crate::{
17 CONFIG_PATH, ParamRef, Printer,
18 utils::{STRING, write_json_value, write_value},
19};
20
21const SECTION: Style = Style::new().bold();
22const ARROW: Style = Style::new().bold();
23const INACTIVE: Style = Style::new().italic();
24const RUST: Style = Style::new().dimmed();
25const JSON_FILE: Style = Style::new()
26 .bg_color(Some(Color::Ansi(AnsiColor::Cyan)))
27 .fg_color(None);
28const YAML_FILE: Style = Style::new()
29 .bg_color(Some(Color::Ansi(AnsiColor::Green)))
30 .fg_color(None);
31const DOTENV_FILE: Style = Style::new()
32 .bg_color(Some(Color::Ansi(AnsiColor::Magenta)))
33 .fg_color(None);
34const ERROR_LABEL: Style = Style::new()
35 .bold()
36 .bg_color(Some(Color::Ansi(AnsiColor::Red)))
37 .fg_color(None);
38
39#[derive(Debug)]
40struct ParamValuesVisitor {
41 config: &'static ConfigMetadata,
42 variant: Option<usize>,
43 param_values: HashMap<usize, serde_json::Value>,
44}
45
46impl ParamValuesVisitor {
47 fn new(config: &'static ConfigMetadata) -> Self {
48 Self {
49 config,
50 variant: None,
51 param_values: HashMap::new(),
52 }
53 }
54}
55
56impl ConfigVisitor for ParamValuesVisitor {
57 fn visit_tag(&mut self, variant_index: usize) {
58 self.variant = Some(variant_index);
59 }
60
61 fn visit_param(&mut self, param_index: usize, value: &dyn any::Any) {
62 let param = self.config.params[param_index];
63 let json = if param.type_description().contains_secrets() {
64 "[REDACTED]".into()
65 } else {
66 param.deserializer.serialize_param(value)
67 };
68 self.param_values.insert(param_index, json);
69 }
70
71 fn visit_nested_config(&mut self, _config_index: usize, _config: &dyn VisitConfig) {
72 }
74}
75
76type ErrorKey = (any::TypeId, String);
78
79#[derive(Debug)]
80struct ConfigErrors {
81 by_param: HashMap<ErrorKey, Vec<ParseError>>,
82 by_config: HashMap<ErrorKey, Vec<ParseError>>,
83}
84
85impl ConfigErrors {
86 fn new(repo: &ConfigRepository<'_>) -> Self {
87 let mut by_param = HashMap::<_, Vec<_>>::new();
88 let mut by_config = HashMap::<_, Vec<_>>::new();
89 for config_parser in repo.iter() {
90 if !config_parser.config().is_top_level() {
91 continue;
95 }
96
97 if let Err(errors) = config_parser.parse_opt() {
98 let mut new_params = HashSet::new();
100 let mut new_configs = HashSet::new();
101
102 for err in errors {
103 let key = (err.config().ty.id(), err.path().to_owned());
104 if err.param().is_some() {
105 if !by_param.contains_key(&key) || new_params.contains(&key) {
106 by_param.entry(key.clone()).or_default().push(err);
107 new_params.insert(key);
108 }
109 } else if !by_config.contains_key(&key) || new_configs.contains(&key) {
110 by_config.entry(key.clone()).or_default().push(err);
111 new_configs.insert(key);
112 }
113 }
114 }
115 }
116 Self {
117 by_param,
118 by_config,
119 }
120 }
121}
122
123impl From<ConfigErrors> for Result<(), ParseErrors> {
124 fn from(errors: ConfigErrors) -> Self {
125 let errors = errors
126 .by_config
127 .into_values()
128 .chain(errors.by_param.into_values())
129 .flatten();
130 errors.collect()
131 }
132}
133
134impl<W: RawStream + AsLockedWrite> Printer<W> {
135 #[allow(clippy::missing_panics_doc)] pub fn print_debug(
145 self,
146 repo: &ConfigRepository<'_>,
147 mut filter: impl FnMut(ParamRef<'_>) -> bool,
148 ) -> io::Result<Result<(), ParseErrors>> {
149 let mut writer = self.writer;
150 if repo.sources().is_empty() {
151 writeln!(&mut writer, "configuration is empty")?;
152 return Ok(Ok(()));
153 }
154
155 writeln!(&mut writer, "{SECTION}Configuration sources:{SECTION:#}")?;
156 for source in repo.sources() {
157 write!(&mut writer, "- ")?;
158 write_origin(&mut writer, &source.origin)?;
159 writeln!(&mut writer, ", {} param(s)", source.param_count)?;
160 }
161
162 writeln!(&mut writer)?;
163 writeln!(&mut writer, "{SECTION}Values:{SECTION:#}")?;
164
165 let errors = ConfigErrors::new(repo);
166 let merged = repo.merged();
167 for config_parser in repo.iter() {
168 let config = config_parser.config();
169 let config_name = config.metadata().ty.name_in_code();
170 let config_id = (config.metadata().ty.id(), config.prefix().to_owned());
171
172 if let Some(errors) = errors.by_config.get(&config_id) {
173 writeln!(
174 writer,
175 "{CONFIG_PATH}{}{CONFIG_PATH:#} {RUST}[Rust: {config_name}]{RUST:#}, config",
176 config.prefix()
177 )?;
178 write_de_errors(&mut writer, errors)?;
179 }
180
181 let (variant, mut param_values) =
182 if let Ok(Some(boxed_config)) = config_parser.parse_opt() {
183 let visitor_fn = config.metadata().visitor;
184 let mut visitor = ParamValuesVisitor::new(config.metadata());
185 visitor_fn(boxed_config.as_ref(), &mut visitor);
186 (visitor.variant, visitor.param_values)
187 } else {
188 (None, HashMap::new())
189 };
190
191 let variant = variant.map(|idx| {
192 let tag = config.metadata().tag.unwrap();
194 let name = tag.variants[idx].name;
195 let tag_param_idx = config.metadata().params.len() - 1;
197 param_values.insert(tag_param_idx, name.into());
198
199 ActiveTagVariant {
200 canonical_path: ParamRef {
201 config,
202 param: tag.param,
203 }
204 .canonical_path(),
205 name,
206 }
207 });
208
209 for (param_idx, param) in config.metadata().params.iter().enumerate() {
210 let param_ref = ParamRef { config, param };
211 if !filter(param_ref) {
212 continue;
213 }
214 let canonical_path = param_ref.canonical_path();
215
216 let raw_value = merged.pointer(&canonical_path);
217 let param_value = param_values.get(¶m_idx);
218 let mut param_written = false;
219 if param_value.is_some() || raw_value.is_some() {
220 write_param(
221 &mut writer,
222 param_ref,
223 &canonical_path,
224 param_value,
225 raw_value,
226 variant.as_ref(),
227 )?;
228 param_written = true;
229 }
230
231 let param_id = (config_id.0, canonical_path.clone());
232 if let Some(errors) = errors.by_param.get(¶m_id) {
233 if !param_written {
234 let field_name = param.rust_field_name;
235 let rust_variant = if let Some(variant) = param.tag_variant {
236 format!("::{}", variant.rust_name)
237 } else {
238 String::new()
239 };
240 writeln!(
241 writer,
242 "{canonical_path} {RUST}[Rust: {config_name}{rust_variant}.{field_name}]{RUST:#}"
243 )?;
244 }
245 write_de_errors(&mut writer, errors)?;
246 }
247 }
248 }
249 Ok(errors.into())
250 }
251}
252
253fn write_origin(writer: &mut impl io::Write, origin: &ValueOrigin) -> io::Result<()> {
254 match origin {
255 ValueOrigin::EnvVars => {
256 write!(writer, "{DOTENV_FILE}env{DOTENV_FILE:#}")
257 }
258 ValueOrigin::File { name, format } => {
259 let style = match format {
260 FileFormat::Json => JSON_FILE,
261 FileFormat::Yaml => YAML_FILE,
262 FileFormat::Dotenv => DOTENV_FILE,
263 _ => Style::new(),
264 };
265 write!(writer, "{style}{format}:{style:#}{name}")
266 }
267 ValueOrigin::Path { source, path } => {
268 if matches!(source.as_ref(), ValueOrigin::EnvVars) {
269 write!(writer, "{DOTENV_FILE}env:{DOTENV_FILE:#}{path:?}")
270 } else {
271 write_origin(writer, source)?;
272 if !path.is_empty() {
273 write!(writer, " {ARROW}->{ARROW:#} .{path}")?;
274 }
275 Ok(())
276 }
277 }
278 ValueOrigin::Synthetic { source, transform } => {
279 write_origin(writer, source)?;
280 write!(writer, " {ARROW}->{ARROW:#} {transform}")
281 }
282 _ => write!(writer, "{origin}"),
283 }
284}
285
286#[derive(Debug)]
287struct ActiveTagVariant {
288 canonical_path: String,
289 name: &'static str,
290}
291
292fn write_param(
293 writer: &mut impl io::Write,
294 param_ref: ParamRef<'_>,
295 path: &str,
296 visited_value: Option<&serde_json::Value>,
297 raw_value: Option<&WithOrigin>,
298 active_variant: Option<&ActiveTagVariant>,
299) -> io::Result<()> {
300 let activity_style = if visited_value.is_some() {
301 Style::new()
302 } else {
303 INACTIVE
304 };
305 let rust_variant = if let Some(variant) = param_ref.param.tag_variant {
306 format!("::{}", variant.rust_name)
307 } else {
308 String::new()
309 };
310
311 write!(
312 writer,
313 "{activity_style}{path}{activity_style:#} {RUST}[Rust: {}{rust_variant}.{}]{RUST:#}",
314 param_ref.config.metadata().ty.name_in_code(),
315 param_ref.param.rust_field_name
316 )?;
317
318 if let Some(value) = visited_value {
319 write!(writer, " = ")?;
320 write_json_value(writer, value, 0)?;
321 writeln!(writer)?;
322 } else {
323 writeln!(writer)?;
324 }
325
326 if let (Some(param_variant), Some(active_variant)) =
327 (param_ref.param.tag_variant, active_variant)
328 {
329 let tag_path = &active_variant.canonical_path;
330 let param_variant_name = param_variant.name;
331 let (label, eq) = if param_variant_name == active_variant.name {
332 ("Active", "==")
333 } else {
334 ("Inactive", "!=")
335 };
336 writeln!(
337 writer,
338 " {label}: {tag_path} {eq} {STRING}'{param_variant_name}'{STRING:#}"
339 )?;
340 }
341
342 if let Some(value) = raw_value {
343 write!(writer, " Raw: ")?;
344 write_value(writer, value, 2)?;
345 writeln!(writer)?;
346 write!(writer, " Origin: ")?;
347 write_origin(writer, &value.origin)?;
348 writeln!(writer)?;
349 }
350 Ok(())
351}
352
353fn write_de_errors(writer: &mut impl io::Write, errors: &[ParseError]) -> io::Result<()> {
354 if errors.len() == 1 {
355 write!(writer, " {ERROR_LABEL}Error:{ERROR_LABEL:#} ")?;
356 write_de_error(writer, &errors[0])
357 } else {
358 writeln!(writer, " {ERROR_LABEL}Errors:{ERROR_LABEL:#}")?;
359 for err in errors {
360 write!(writer, " - ")?;
361 write_de_error(writer, err)?;
362 }
363 Ok(())
364 }
365}
366
367fn write_de_error(writer: &mut impl io::Write, err: &ParseError) -> io::Result<()> {
368 writeln!(writer, "{}", err.inner())?;
369 if let Some(validation) = err.validation() {
370 writeln!(writer, " {SECTION}validation:{SECTION:#} {validation}")?;
371 }
372 writeln!(
373 writer,
374 " at {SECTION}{path}{SECTION:#}",
375 path = err.path()
376 )?;
377 if !matches!(err.origin(), ValueOrigin::Unknown) {
378 write!(writer, " ")?;
379 write_origin(writer, err.origin())?;
380 writeln!(writer)?;
381 }
382 Ok(())
383}