Skip to main content

cookie_agent_protocol/
setup_value.rs

1use std::fmt;
2
3use crate::{BoundedSetupString, SafeCode, SafeSetupValue, SetupFieldDescriptor, SetupFieldType};
4
5/// An error returned when text does not satisfy a setup field descriptor.
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct SetupValueParseError {
8    message: String,
9}
10
11impl SetupValueParseError {
12    fn new(field: &SetupFieldDescriptor, message: impl fmt::Display) -> Self {
13        Self {
14            message: format!("setup field `{}` {message}", field.id),
15        }
16    }
17}
18
19impl fmt::Display for SetupValueParseError {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        formatter.write_str(&self.message)
22    }
23}
24
25impl std::error::Error for SetupValueParseError {}
26
27/// Parses text according to a setup field's type and validation constraints.
28///
29/// Boolean input is case-insensitive and accepts `true`, `yes`, `y`, and `1`
30/// for true and `false`, `no`, `n`, and `0` for false.
31pub fn parse_setup_value(
32    field: &SetupFieldDescriptor,
33    raw: &str,
34) -> Result<SafeSetupValue, SetupValueParseError> {
35    let length = u32::try_from(raw.len()).unwrap_or(u32::MAX);
36    if field
37        .validation
38        .min_length
39        .is_some_and(|minimum| length < minimum)
40    {
41        return Err(SetupValueParseError::new(
42            field,
43            "is shorter than the allowed minimum",
44        ));
45    }
46    if field
47        .validation
48        .max_length
49        .is_some_and(|maximum| length > maximum)
50    {
51        return Err(SetupValueParseError::new(
52            field,
53            "is longer than the allowed maximum",
54        ));
55    }
56
57    match field.validation.value_type {
58        SetupFieldType::String => BoundedSetupString::new(raw.to_owned())
59            .map(SafeSetupValue::String)
60            .map_err(|error| SetupValueParseError::new(field, error)),
61        SetupFieldType::Code => SafeCode::new(raw.to_owned())
62            .map(SafeSetupValue::Code)
63            .map_err(|error| SetupValueParseError::new(field, error)),
64        SetupFieldType::Integer => {
65            let value = raw
66                .parse::<i64>()
67                .map_err(|_| SetupValueParseError::new(field, "must be an integer"))?;
68            if field
69                .validation
70                .minimum
71                .is_some_and(|minimum| value < minimum)
72                || field
73                    .validation
74                    .maximum
75                    .is_some_and(|maximum| value > maximum)
76            {
77                return Err(SetupValueParseError::new(
78                    field,
79                    "is outside the allowed range",
80                ));
81            }
82            Ok(SafeSetupValue::Integer(value))
83        }
84        SetupFieldType::Bool => match raw.to_ascii_lowercase().as_str() {
85            "true" | "yes" | "y" | "1" => Ok(SafeSetupValue::Bool(true)),
86            "false" | "no" | "n" | "0" => Ok(SafeSetupValue::Bool(false)),
87            _ => Err(SetupValueParseError::new(field, "must be true or false")),
88        },
89    }
90}
91
92/// Formats a setup value for editing or display.
93#[must_use]
94pub fn setup_value_text(value: &SafeSetupValue) -> String {
95    match value {
96        SafeSetupValue::Bool(value) => value.to_string(),
97        SafeSetupValue::Integer(value) => value.to_string(),
98        SafeSetupValue::Code(value) => value.to_string(),
99        SafeSetupValue::String(value) => value.as_str().to_owned(),
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use crate::{
106        SafeDisplayText, SetupFieldId, SetupFieldValidation, SetupValueParseError,
107        parse_setup_value,
108    };
109
110    use super::{SafeSetupValue, SetupFieldDescriptor, SetupFieldType};
111
112    fn field(value_type: SetupFieldType) -> SetupFieldDescriptor {
113        SetupFieldDescriptor {
114            id: SetupFieldId::new("example").expect("field ID"),
115            display_name: SafeDisplayText::new("Example").expect("display name"),
116            help: SafeDisplayText::new("Example value").expect("help"),
117            required: true,
118            default: None,
119            validation: SetupFieldValidation {
120                value_type,
121                min_length: None,
122                max_length: None,
123                minimum: None,
124                maximum: None,
125            },
126            safe_to_project: true,
127        }
128    }
129
130    fn message(error: SetupValueParseError) -> String {
131        error.to_string()
132    }
133
134    #[test]
135    fn validates_string_length() {
136        let mut descriptor = field(SetupFieldType::String);
137        descriptor.validation.min_length = Some(2);
138        descriptor.validation.max_length = Some(4);
139
140        assert_eq!(
141            message(parse_setup_value(&descriptor, "a").unwrap_err()),
142            "setup field `example` is shorter than the allowed minimum"
143        );
144        assert_eq!(
145            message(parse_setup_value(&descriptor, "abcde").unwrap_err()),
146            "setup field `example` is longer than the allowed maximum"
147        );
148        assert!(matches!(
149            parse_setup_value(&descriptor, "abcd"),
150            Ok(SafeSetupValue::String(_))
151        ));
152    }
153
154    #[test]
155    fn validates_integer_range_and_format() {
156        let mut descriptor = field(SetupFieldType::Integer);
157        descriptor.validation.minimum = Some(2);
158        descriptor.validation.maximum = Some(4);
159
160        assert_eq!(
161            parse_setup_value(&descriptor, "3"),
162            Ok(SafeSetupValue::Integer(3))
163        );
164        assert_eq!(
165            message(parse_setup_value(&descriptor, "1").unwrap_err()),
166            "setup field `example` is outside the allowed range"
167        );
168        assert_eq!(
169            message(parse_setup_value(&descriptor, "three").unwrap_err()),
170            "setup field `example` must be an integer"
171        );
172    }
173
174    #[test]
175    fn parses_documented_boolean_variants_case_insensitively() {
176        let descriptor = field(SetupFieldType::Bool);
177        for raw in ["true", "TRUE", "yes", "Y", "1"] {
178            assert_eq!(
179                parse_setup_value(&descriptor, raw),
180                Ok(SafeSetupValue::Bool(true)),
181                "{raw}"
182            );
183        }
184        for raw in ["false", "FALSE", "no", "N", "0"] {
185            assert_eq!(
186                parse_setup_value(&descriptor, raw),
187                Ok(SafeSetupValue::Bool(false)),
188                "{raw}"
189            );
190        }
191        assert_eq!(
192            message(parse_setup_value(&descriptor, "maybe").unwrap_err()),
193            "setup field `example` must be true or false"
194        );
195    }
196}