conf/
error.rs

1use crate::{ConfValueSource, FlattenedOptionalDebugInfo, ProgramOption};
2use clap::{Command, Error as ClapError, builder::Styles, error::ErrorKind};
3use std::{ffi::OsString, fmt, fmt::Write};
4
5/// An error which occurs when a `Conf::parse` function is called.
6/// This may conceptually represent many underlying errors of several different types.
7//
8// Note: For now this is a thin wrapper around clap::Error just so that we can control our public
9// API independently of clap. We may eventually need to make it contain an enum which is either a
10// clap::Error or a collection of InnerError or something like this, but this approach is working
11// adequately for now.
12#[derive(Debug)]
13pub struct Error(ClapError);
14
15impl fmt::Display for Error {
16    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
17        self.0.fmt(f)
18    }
19}
20
21impl std::error::Error for Error {
22    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
23        self.0.source()
24    }
25}
26
27impl Error {
28    /// Print formatted and colored error text to stderr or stdout as appropriate (as clap does)
29    pub fn print(&self) -> Result<(), std::io::Error> {
30        self.0.print()
31    }
32
33    /// Exit the program, printing an error message to stderr or stdout as appropriate (as clap
34    /// does)
35    pub fn exit(&self) -> ! {
36        self.0.exit()
37    }
38
39    /// The exit code this error will exit the program with
40    pub fn exit_code(&self) -> i32 {
41        self.0.exit_code()
42    }
43
44    // An error reported during program options generation
45    #[doc(hidden)]
46    pub fn skip_short_not_found(
47        not_found_chars: Vec<char>,
48        field_name: &'static str,
49        field_type_name: &'static str,
50    ) -> Self {
51        let buf = format!(
52            "Internal error (invalid skip short)\n  When flattening {field_type_name} at {field_name}, these short options were not found: {not_found_chars:?}\n  To fix this error, remove them from the skip_short attribute list."
53        );
54        ClapError::raw(ErrorKind::UnknownArgument, buf).into()
55    }
56
57    // An error reported when positional arguments are used in flatten optional
58    #[doc(hidden)]
59    pub fn positional_in_flatten_optional(
60        field_name: &str,
61        field_type_name: &str,
62        option_id: &str,
63    ) -> Self {
64        let buf = format!(
65            "Cannot use flatten optional with struct '{field_type_name}' at field '{field_name}' because it contains positional argument '{option_id}'. Positional arguments are not supported in flatten optional structs (but are supported in regular flatten)."
66        );
67        ClapError::raw(ErrorKind::ArgumentConflict, buf).into()
68    }
69}
70
71impl From<ClapError> for Error {
72    fn from(src: ClapError) -> Error {
73        Error(src)
74    }
75}
76
77impl From<fmt::Error> for Error {
78    fn from(src: fmt::Error) -> Error {
79        ClapError::from(src).into()
80    }
81}
82
83/// A single problem that occurs when a Conf attempts to parse env, or run a value parser, or run a
84/// validation predicate
85#[doc(hidden)]
86#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
87pub enum InnerError {
88    /// Missing a required parameter
89    // (missing program option, optional reason it is required, serde source is present)
90    MissingRequiredParameter(
91        Box<ProgramOption>,
92        Option<Box<OwnedFlattenedOptionalDebugInfo>>,
93        bool,
94    ),
95    /// Invalid parameter value
96    // (source, value string, program option, error message)
97    InvalidParameterValue(ConfValueSource<String>, String, Box<ProgramOption>, String),
98    /// Too Few Arguments
99    // (struct name, instance id prefix, single options, flattened fields, optional reason this is
100    // required, serde source is present)
101    TooFewArguments(
102        String,
103        String,
104        Vec<ProgramOption>,
105        Vec<String>,
106        Option<Box<OwnedFlattenedOptionalDebugInfo>>,
107        bool,
108    ),
109    /// Too many arguments
110    // (struct name, instance id prefix, single options, flattened fields (field name, option which
111    // appeared))
112    TooManyArguments(
113        String,
114        String,
115        Vec<(ProgramOption, ConfValueSource<String>)>,
116        Vec<(String, ProgramOption, ConfValueSource<String>)>,
117    ),
118    /// Validation failed
119    // (struct name, instance id_prefix, error message)
120    ValidationFailed(String, String, String),
121    /// Invalid UTF-8 in env
122    // (value omitted if it is secret)
123    InvalidUtf8Env(String, Box<ProgramOption>, Option<OsString>),
124    /// Missing required subcommand
125    // struct name, field name, subcommands
126    MissingRequiredSubcommand(String, String, Vec<String>),
127    /// Parsing (document)
128    // document name, field name, error
129    Serde(String, String, String),
130}
131
132impl InnerError {
133    /// Helper which makes InvalidParameterValue
134    pub fn invalid_value(
135        conf_value_source: ConfValueSource<&str>,
136        value_str: &str,
137        program_option: &ProgramOption,
138        err: impl fmt::Display,
139    ) -> Self {
140        let program_option = Box::new(program_option.clone());
141        Self::InvalidParameterValue(
142            conf_value_source.into_owned(),
143            value_str.to_owned(),
144            program_option,
145            err.to_string(),
146        )
147    }
148
149    /// Helper which makes InvalidParameterValue from an OsStr value
150    pub fn invalid_value_os(
151        conf_value_source: ConfValueSource<&str>,
152        value_os: &std::ffi::OsStr,
153        program_option: &ProgramOption,
154        err: impl fmt::Display,
155    ) -> Self {
156        let program_option = Box::new(program_option.clone());
157        Self::InvalidParameterValue(
158            conf_value_source.into_owned(),
159            value_os.to_string_lossy().into_owned(),
160            program_option,
161            err.to_string(),
162        )
163    }
164
165    /// Helper which makes MissingRequiredParameter
166    pub(crate) fn missing_required_parameter(
167        opt: &ProgramOption,
168        flattened_optional_debug_info: Option<FlattenedOptionalDebugInfo<'_>>,
169        serde_source_is_present: bool,
170    ) -> Self {
171        Self::MissingRequiredParameter(
172            Box::new(opt.clone()),
173            flattened_optional_debug_info.map(Into::into).map(Box::new),
174            serde_source_is_present,
175        )
176    }
177
178    /// Helper which makes TooFewArguments
179    pub(crate) fn too_few_arguments<'a>(
180        struct_name: &'static str,
181        instance_id_prefix: &str,
182        constraint_single_options: impl AsRef<[&'a ProgramOption]>,
183        constraint_flattened_ids: impl AsRef<[&'a str]>,
184        flattened_optional_debug_info: Option<FlattenedOptionalDebugInfo<'a>>,
185        serde_source_is_present: bool,
186    ) -> Self {
187        let constraint_single_options = constraint_single_options
188            .as_ref()
189            .iter()
190            .map(|opt| (*opt).clone())
191            .collect::<Vec<_>>();
192        let constraint_flattened_ids = constraint_flattened_ids
193            .as_ref()
194            .iter()
195            .map(|id| (*id).to_owned())
196            .collect::<Vec<_>>();
197        let flattened_optional_debug_info =
198            flattened_optional_debug_info.map(Into::into).map(Box::new);
199        Self::TooFewArguments(
200            struct_name.to_owned(),
201            instance_id_prefix.to_owned(),
202            constraint_single_options,
203            constraint_flattened_ids,
204            flattened_optional_debug_info,
205            serde_source_is_present,
206        )
207    }
208
209    /// Helper which makes TooManyArguments
210    // Constraint flattened data is (field name, option which appeared)
211    pub(crate) fn too_many_arguments<'a>(
212        struct_name: &'static str,
213        instance_id_prefix: &'a str,
214        constraint_single_options: impl AsRef<[(&'a ProgramOption, ConfValueSource<&'a str>)]>,
215        constraint_flattened_data: impl AsRef<[(&'a str, &'a ProgramOption, ConfValueSource<&'a str>)]>,
216    ) -> Self {
217        let constraint_single_options = constraint_single_options
218            .as_ref()
219            .iter()
220            .map(|(opt, src)| ((*opt).clone(), (*src).into_owned()))
221            .collect::<Vec<_>>();
222        let constraint_flattened_data = constraint_flattened_data
223            .as_ref()
224            .iter()
225            .map(|(field_name, opt, src)| {
226                (
227                    (*field_name).to_owned(),
228                    (*opt).clone(),
229                    (*src).into_owned(),
230                )
231            })
232            .collect::<Vec<_>>();
233        Self::TooManyArguments(
234            struct_name.to_owned(),
235            instance_id_prefix.to_owned(),
236            constraint_single_options,
237            constraint_flattened_data,
238        )
239    }
240
241    /// Helper which makes ValidationFailed
242    pub fn validation(
243        struct_name: &'static str,
244        instance_id_prefix: &str,
245        err: impl fmt::Display,
246    ) -> Self {
247        Self::ValidationFailed(
248            struct_name.to_owned(),
249            instance_id_prefix.to_owned(),
250            err.to_string(),
251        )
252    }
253
254    /// Helper which makes InvalidUtf8Env
255    pub(crate) fn invalid_utf8_env(
256        env_var: &str,
257        program_option: &ProgramOption,
258        val: Option<&OsString>,
259    ) -> Self {
260        Self::InvalidUtf8Env(
261            env_var.to_owned(),
262            Box::new(program_option.clone()),
263            val.cloned(),
264        )
265    }
266
267    /// Helper which makes MissingRequiredSubcommand
268    pub fn missing_required_subcommand(
269        struct_name: &str,
270        field_name: &str,
271        subcommand_names: &'static [&'static str],
272    ) -> Self {
273        Self::MissingRequiredSubcommand(
274            struct_name.to_owned(),
275            field_name.to_owned(),
276            subcommand_names.iter().map(|x| (*x).to_owned()).collect(),
277        )
278    }
279
280    /// Helper which makes Serde
281    pub fn serde(document_name: &str, field_name: &str, err: impl fmt::Display) -> Self {
282        Self::Serde(
283            document_name.to_owned(),
284            field_name.to_owned(),
285            err.to_string(),
286        )
287    }
288
289    // A short (one-line) description of the problem
290    fn title(&self) -> &'static str {
291        match self {
292            Self::InvalidUtf8Env(..) => "An env var contained invalid UTF8",
293            Self::MissingRequiredParameter(..) => "A required value was not provided",
294            Self::TooFewArguments(..) => "Too few arguments",
295            Self::TooManyArguments(..) => "Too many arguments",
296            Self::ValidationFailed(..) => "Validation failed",
297            Self::InvalidParameterValue(..) => "Invalid value",
298            Self::MissingRequiredSubcommand(..) => "Missing required subcommand",
299            Self::Serde(..) => "Parsing document",
300        }
301    }
302
303    // convert to clap error kind
304    fn error_kind(&self) -> ErrorKind {
305        match self {
306            Self::InvalidUtf8Env(..) => ErrorKind::InvalidValue,
307            Self::MissingRequiredParameter(..) => ErrorKind::MissingRequiredArgument,
308            Self::TooFewArguments(..) => ErrorKind::TooFewValues,
309            Self::TooManyArguments(..) => ErrorKind::TooManyValues,
310            Self::ValidationFailed(..) => ErrorKind::ValueValidation,
311            Self::InvalidParameterValue(..) => ErrorKind::InvalidValue,
312            Self::MissingRequiredSubcommand(..) => ErrorKind::MissingSubcommand,
313            Self::Serde(..) => ErrorKind::InvalidValue,
314        }
315    }
316
317    // get program option associated to this error, if any
318    // when only one error occurs, we print associated help text
319    fn get_program_option(&self) -> Option<&ProgramOption> {
320        match self {
321            Self::InvalidUtf8Env(_, opt, _) => Some(opt),
322            Self::MissingRequiredParameter(opt, ..) => Some(opt),
323            Self::InvalidParameterValue(_src, _val_str, opt, _err) => Some(opt),
324            Self::TooFewArguments(..) => None,
325            Self::TooManyArguments(..) => None,
326            Self::ValidationFailed(..) => None,
327            Self::MissingRequiredSubcommand(..) => None,
328            Self::Serde(..) => None,
329        }
330    }
331
332    // Print this error in a form for when it is the only error
333    fn print_solo(
334        &self,
335        stream: &mut impl std::fmt::Write,
336        styles: &Styles,
337    ) -> Result<(), std::fmt::Error> {
338        writeln!(stream, "{}", self.title())?;
339
340        self.print_body(stream, styles)?;
341
342        // Print the opt.print help text as well
343        if let Some(opt) = self.get_program_option() {
344            writeln!(stream)?;
345            writeln!(stream, "Help:")?;
346            opt.print(stream, None)?;
347        }
348
349        Ok(())
350    }
351
352    // Print indented details of this error (but not the opt.print text)
353    fn print_body(
354        &self,
355        stream: &mut impl std::fmt::Write,
356        styles: &Styles,
357    ) -> Result<(), std::fmt::Error> {
358        // Styling based on examples in clap like here:
359        // https://docs.rs/clap_builder/4.5.9/src/clap_builder/error/mod.rs.html#790
360        let invalid = styles.get_invalid();
361
362        match self {
363            Self::InvalidUtf8Env(name, opt, maybe_val) => {
364                let lossy_val = maybe_val
365                    .as_ref()
366                    .and_then(|val| {
367                        if opt.is_secret() {
368                            None
369                        } else {
370                            Some(val.to_string_lossy())
371                        }
372                    })
373                    .unwrap_or_else(|| "***secret***".into());
374
375                writeln!(
376                    stream,
377                    "  {name}: {}'{lossy_val}'{}",
378                    invalid.render(),
379                    invalid.render_reset()
380                )?;
381            }
382            Self::MissingRequiredParameter(
383                opt,
384                maybe_flatten_optional_debug_info,
385                serde_source_is_present,
386            ) => {
387                print_opt_requirements(stream, opt, "must be provided", *serde_source_is_present)?;
388                if let Some(flatten_optional) = maybe_flatten_optional_debug_info.as_ref() {
389                    // Indent 4 spaces
390                    write!(stream, "    ")?;
391                    flatten_optional.print_required_opt_context(stream)?;
392                }
393            }
394            Self::InvalidParameterValue(value_source, value_str, opt, err) => {
395                let context = format!(
396                    "  when parsing {} value",
397                    render_provided_opt(opt, value_source)
398                );
399                let mut estimated_len = context.len();
400                write!(stream, "{context}")?;
401                if !opt.is_secret() {
402                    write!(
403                        stream,
404                        " {}'{value_str}'{}",
405                        invalid.render(),
406                        invalid.render_reset()
407                    )?;
408                    estimated_len += 3 + value_str.len();
409                }
410                writeln!(
411                    stream,
412                    ": {err_str}",
413                    err_str = Self::format_err_str(err, estimated_len + 2)
414                )?;
415            }
416            Self::TooFewArguments(
417                struct_name,
418                instance_id_prefix,
419                single_opts,
420                flattened_opts,
421                maybe_flatten_optional_debug_info,
422                serde_source_is_present,
423            ) => {
424                let mut instance_id_prefix = instance_id_prefix.to_owned();
425                if !instance_id_prefix.is_empty() {
426                    instance_id_prefix.insert_str(0, " @ .");
427                    remove_trailing_dot(&mut instance_id_prefix);
428                }
429                writeln!(
430                    stream,
431                    "  One of these must be provided: (constraint on {struct_name}{instance_id_prefix}): "
432                )?;
433                for opt in single_opts {
434                    write!(stream, "  ")?;
435                    print_opt_requirements(stream, opt, "", *serde_source_is_present)?;
436                }
437                for field_name in flattened_opts {
438                    writeln!(stream, "    Argument group '{field_name}'")?;
439                }
440                if let Some(flatten_optional) = maybe_flatten_optional_debug_info.as_ref() {
441                    write!(stream, "  ")?;
442                    flatten_optional.print_required_opt_context(stream)?;
443                }
444            }
445            Self::TooManyArguments(
446                struct_name,
447                instance_id_prefix,
448                single_opts,
449                flattened_opts,
450            ) => {
451                let mut instance_id_prefix = instance_id_prefix.to_owned();
452                if !instance_id_prefix.is_empty() {
453                    instance_id_prefix.insert_str(0, " @ .");
454                    remove_trailing_dot(&mut instance_id_prefix);
455                }
456                writeln!(
457                    stream,
458                    "  Too many arguments, provide at most one of these: (constraint on {struct_name}{instance_id_prefix}): "
459                )?;
460                for (opt, source) in single_opts {
461                    let provided_opt = render_provided_opt(opt, source);
462                    writeln!(stream, "    {provided_opt}")?;
463                }
464                for (field_name, opt, source) in flattened_opts {
465                    let provided_opt = render_provided_opt(opt, source);
466                    writeln!(
467                        stream,
468                        "    {provided_opt} (part of argument group '{field_name}')"
469                    )?;
470                }
471            }
472            Self::ValidationFailed(struct_name, instance_id_prefix, err) => {
473                let mut context = format!("  {struct_name} value was invalid");
474                if !instance_id_prefix.is_empty() {
475                    context += &format!(" (@ .{instance_id_prefix})");
476                }
477                let estimated_len = context.len();
478                writeln!(
479                    stream,
480                    "{context}: {err_str}",
481                    err_str = Self::format_err_str(err, estimated_len + 2)
482                )?;
483            }
484            Self::MissingRequiredSubcommand(struct_name, field_name, subcommand_names) => {
485                writeln!(
486                    stream,
487                    "  A subcommand must be selected ({struct_name}::{field_name}):"
488                )?;
489                for name in subcommand_names {
490                    writeln!(stream, "    {name}")?;
491                }
492            }
493            Self::Serde(document_name, field_name, err) => {
494                let context = format!("  Parsing {document_name} (@ {field_name})");
495                let estimated_len = context.len();
496                writeln!(
497                    stream,
498                    "{context}: {err_str}",
499                    err_str = Self::format_err_str(err, estimated_len + 2)
500                )?;
501            }
502        }
503        Ok(())
504    }
505
506    // Formats an error string to look nicely indented if it has line breaks and starting with a
507    // line break if the error is long.
508    fn format_err_str(err_str: &str, estimated_line_length_so_far: usize) -> String {
509        const TARGET_LINE_LENGTH: usize = 100;
510        const INDENTATION: usize = 4;
511
512        let mut err_str = err_str.to_owned();
513        if err_str.len() + estimated_line_length_so_far > TARGET_LINE_LENGTH
514            || err_str.contains('\n')
515        {
516            err_str.insert(0, '\n');
517        }
518
519        let indented_newline = "\n".to_owned() + &" ".repeat(INDENTATION);
520        err_str.replace('\n', &indented_newline)
521    }
522}
523
524// Print one line describing a missing required option, showing all the ways it can be provided
525// The line is indented two spaces
526fn print_opt_requirements(
527    stream: &mut impl std::fmt::Write,
528    opt: &ProgramOption,
529    trailing_text: &str,
530    serde_source_is_present: bool,
531) -> fmt::Result {
532    let mut ways_to_provide = vec![];
533
534    // Check for env form
535    if let Some(name) = opt.env_form.as_deref() {
536        ways_to_provide.push(format!("env '{name}'"));
537    }
538
539    // Check for positional argument
540    if opt.is_positional {
541        let pos_name = format!("<{}>", opt.id);
542        ways_to_provide.push(format!("'{pos_name}'"));
543    }
544    // Check for switch (if not positional)
545    else if let Some(switch) = render_help_switch(opt) {
546        ways_to_provide.push(format!("'{switch}'"));
547    }
548
549    // Check for serde source
550    if opt.has_serde_source && serde_source_is_present {
551        ways_to_provide.push(format!("'{}' in config file", opt.id));
552    }
553
554    // Build the final line
555    if ways_to_provide.is_empty() {
556        writeln!(stream, "  Required value '{}' cannot be provided", opt.id)?;
557    } else {
558        let joined = ways_to_provide.join(", or ");
559        if trailing_text.is_empty() {
560            writeln!(stream, "  {joined}")?;
561        } else if ways_to_provide.len() == 1 {
562            // When there's only one way to provide, don't add a comma before trailing text
563            writeln!(stream, "  {joined} {trailing_text}")?;
564        } else {
565            writeln!(stream, "  {joined}, {trailing_text}")?;
566        }
567    }
568
569    Ok(())
570}
571
572fn render_provided_opt(opt: &ProgramOption, value_source: &ConfValueSource<String>) -> String {
573    match value_source {
574        ConfValueSource::Args => {
575            // If we have both a long and a short form, prefer to display the long form in this help
576            // message
577            let switch = render_help_switch(opt).unwrap_or_default();
578            format!("'{switch}'")
579        }
580        ConfValueSource::Default => "default value".into(),
581        ConfValueSource::Env(name) => {
582            format!("env '{name}'")
583        }
584        ConfValueSource::Document(name) => {
585            format!("document '{name}'")
586        }
587    }
588}
589
590fn render_help_switch(opt: &ProgramOption) -> Option<String> {
591    // If we have both a long and a short form, prefer to display the long form in this help message
592    opt.long_form
593        .as_deref()
594        .map(|l| format!("--{l}"))
595        .or_else(|| opt.short_form.map(|s| format!("-{s}")))
596}
597
598fn remove_trailing_dot(string: &mut String) {
599    if let Some(c) = string.pop() {
600        if c != '.' {
601            string.push(c);
602        }
603    }
604}
605
606/// A version of FlattenedOptionalDebugInfo that owns its data
607#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
608pub struct OwnedFlattenedOptionalDebugInfo {
609    pub struct_name: &'static str,
610    pub id_prefix: String,
611    pub option_appeared: Box<ProgramOption>,
612    pub value_source: ConfValueSource<String>,
613}
614
615impl<'a> From<FlattenedOptionalDebugInfo<'a>> for OwnedFlattenedOptionalDebugInfo {
616    fn from(src: FlattenedOptionalDebugInfo<'a>) -> Self {
617        Self {
618            struct_name: src.struct_name,
619            id_prefix: src.id_prefix,
620            option_appeared: Box::new(src.option_appeared.clone()),
621            value_source: src.value_source.into_owned(),
622        }
623    }
624}
625
626impl OwnedFlattenedOptionalDebugInfo {
627    /// Print context about why an option is required, if it is part of an flatten optional group.
628    /// Prints one line with no indentation, add indentation first if needed
629    fn print_required_opt_context(&self, stream: &mut impl std::fmt::Write) -> fmt::Result {
630        let provided_opt = render_provided_opt(&self.option_appeared, &self.value_source);
631        let mut context = self.struct_name.to_owned();
632        if !self.id_prefix.is_empty() {
633            context += " @ .";
634            context += &self.id_prefix;
635            remove_trailing_dot(&mut context);
636        }
637
638        writeln!(
639            stream,
640            "because {provided_opt} was provided (enabling argument group {context})"
641        )
642    }
643}
644
645impl fmt::Display for InnerError {
646    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
647        write!(fmt, "{}", self.title())
648    }
649}
650
651impl InnerError {
652    pub(crate) fn into_clap_error(self, command: &Command) -> Error {
653        let mut buf = String::new();
654
655        self.print_solo(&mut buf, command.get_styles()).unwrap();
656        ClapError::raw(self.error_kind(), buf)
657            .with_cmd(command)
658            .into()
659    }
660
661    pub(crate) fn vec_to_clap_error(mut src: Vec<InnerError>, command: &Command) -> Error {
662        assert!(!src.is_empty());
663        if src.len() == 1 {
664            return src.remove(0).into_clap_error(command);
665        }
666
667        src.sort();
668        let last_error_kind = src.last().unwrap().error_kind();
669
670        let styles = command.get_styles();
671        let error_sty = styles.get_error();
672
673        let mut buf = String::new();
674
675        let mut last_title = "";
676        for err in src {
677            if err.title() != last_title {
678                if !last_title.is_empty() {
679                    write!(
680                        &mut buf,
681                        "{}error: {}",
682                        error_sty.render(),
683                        error_sty.render_reset()
684                    )
685                    .unwrap();
686                }
687                writeln!(&mut buf, "{}", err.title()).unwrap();
688                last_title = err.title();
689            }
690            err.print_body(&mut buf, styles).unwrap();
691        }
692        ClapError::raw(last_error_kind, buf)
693            .with_cmd(command)
694            .into()
695    }
696}