1
//! Declare error types.
2

            
3
use tor_error::{ErrorKind, HasKind};
4

            
5
/// An error related to an option passed to Arti via a configuration
6
/// builder.
7
//
8
// API NOTE: When possible, we should expose this error type rather than
9
// wrapping it in `TorError`. It can provide specific information about  what
10
// part of the configuration was invalid.
11
//
12
// This is part of the public API.
13
4
#[derive(Debug, Clone, thiserror::Error)]
14
#[non_exhaustive]
15
pub enum ConfigBuildError {
16
    /// A mandatory field was not present.
17
    #[error("Field was not provided: {field}")]
18
    MissingField {
19
        /// The name of the missing field.
20
        field: String,
21
    },
22
    /// A single field had a value that proved to be unusable.
23
    #[error("Value of {field} was incorrect: {problem}")]
24
    Invalid {
25
        /// The name of the invalid field
26
        field: String,
27
        /// A description of the problem.
28
        problem: String,
29
    },
30
    /// Multiple fields are inconsistent.
31
    #[error("Fields {fields:?} are inconsistent: {problem}")]
32
    Inconsistent {
33
        /// The names of the inconsistent fields
34
        fields: Vec<String>,
35
        /// The problem that makes them inconsistent
36
        problem: String,
37
    },
38
}
39

            
40
impl From<derive_builder::UninitializedFieldError> for ConfigBuildError {
41
1
    fn from(val: derive_builder::UninitializedFieldError) -> Self {
42
1
        ConfigBuildError::MissingField {
43
1
            field: val.field_name().to_string(),
44
1
        }
45
1
    }
46
}
47

            
48
impl ConfigBuildError {
49
    /// Return a new ConfigBuildError that prefixes its field name with
50
    /// `prefix` and a dot.
51
    #[must_use]
52
3
    pub fn within(&self, prefix: &str) -> Self {
53
3
        use ConfigBuildError::*;
54
3
        match self {
55
1
            MissingField { field } => MissingField {
56
1
                field: format!("{}.{}", prefix, field),
57
1
            },
58
1
            Invalid { field, problem } => Invalid {
59
1
                field: format!("{}.{}", prefix, field),
60
1
                problem: problem.clone(),
61
1
            },
62
1
            Inconsistent { fields, problem } => Inconsistent {
63
2
                fields: fields.iter().map(|f| format!("{}.{}", prefix, f)).collect(),
64
1
                problem: problem.clone(),
65
1
            },
66
        }
67
3
    }
68
}
69

            
70
impl HasKind for ConfigBuildError {
71
    fn kind(&self) -> ErrorKind {
72
        ErrorKind::InvalidConfig
73
    }
74
}
75

            
76
/// An error caused when attempting to reconfigure an existing Arti client, or one of its modules.
77
1
#[derive(Debug, Clone, thiserror::Error)]
78
#[non_exhaustive]
79
pub enum ReconfigureError {
80
    /// Tried to change a field that cannot change on a running client.
81
    #[error("Cannot change {field} on a running client.")]
82
    CannotChange {
83
        /// The field (or fields) that we tried to change.
84
        field: String,
85
    },
86
}
87

            
88
impl HasKind for ReconfigureError {
89
    fn kind(&self) -> ErrorKind {
90
        ErrorKind::InvalidConfigTransition
91
    }
92
}
93

            
94
#[cfg(test)]
95
mod test {
96
    #![allow(clippy::unwrap_used)]
97
    use super::*;
98

            
99
    #[test]
100
    fn within() {
101
        let e1 = ConfigBuildError::MissingField {
102
            field: "lettuce".to_owned(),
103
        };
104
        let e2 = ConfigBuildError::Invalid {
105
            field: "tomato".to_owned(),
106
            problem: "too crunchy".to_owned(),
107
        };
108
        let e3 = ConfigBuildError::Inconsistent {
109
            fields: vec!["mayo".to_owned(), "avocado".to_owned()],
110
            problem: "pick one".to_owned(),
111
        };
112

            
113
        assert_eq!(
114
            &e1.within("sandwich").to_string(),
115
            "Field was not provided: sandwich.lettuce"
116
        );
117
        assert_eq!(
118
            &e2.within("sandwich").to_string(),
119
            "Value of sandwich.tomato was incorrect: too crunchy"
120
        );
121
        assert_eq!(
122
            &e3.within("sandwich").to_string(),
123
            r#"Fields ["sandwich.mayo", "sandwich.avocado"] are inconsistent: pick one"#
124
        );
125
    }
126

            
127
    #[derive(derive_builder::Builder, Debug)]
128
    #[builder(build_fn(error = "ConfigBuildError"))]
129
    #[allow(dead_code)]
130
    struct Cephalopod {
131
        // arms have suction cups for their whole length
132
        arms: u8,
133
        // Tentacles have suction cups at the ends
134
        tentacles: u8,
135
    }
136

            
137
    #[test]
138
    fn build_err() {
139
        let squid = CephalopodBuilder::default().arms(8).tentacles(2).build();
140
        let octopus = CephalopodBuilder::default().arms(8).build();
141
        assert!(squid.is_ok());
142
        let squid = squid.unwrap();
143
        assert_eq!(squid.arms, 8);
144
        assert_eq!(squid.tentacles, 2);
145
        assert!(octopus.is_err());
146
        assert_eq!(
147
            &octopus.unwrap_err().to_string(),
148
            "Field was not provided: tentacles"
149
        );
150
    }
151
}