Skip to main content

git_bot_feedback/
output_variable.rs

1use std::fmt::Display;
2
3use crate::error::OutputVariableError;
4
5/// A type to represent an output variable.
6///
7/// This is akin to the key/value pairs used in most
8/// config file formats but with some limitations:
9///
10/// - Both [OutputVariable::name] and [OutputVariable::value] must be UTF-8 encoded.
11/// - The [OutputVariable::value] cannot span multiple lines.
12#[derive(Debug, Clone)]
13pub struct OutputVariable {
14    /// The output variable's name.
15    pub name: String,
16
17    /// The output variable's value.
18    pub value: String,
19}
20
21impl OutputVariable {
22    /// Validate that the output variable is well-formed.
23    ///
24    /// Typically only used by implementations of
25    /// [`RestApiClient::write_output_variables`](crate::client::RestApiClient::write_output_variables).
26    pub fn validate(&self) -> Result<(), OutputVariableError> {
27        let name = self.name.trim();
28        if name.is_empty() {
29            return Err(OutputVariableError::NameIsEmpty);
30        }
31        for (i, c) in name.chars().enumerate() {
32            if i == 0 && c.is_ascii_digit() {
33                return Err(OutputVariableError::NameStartsWithNumber(name.to_string()));
34            }
35            if !(c.is_ascii_alphanumeric() || c == '_' || c == '-') {
36                return Err(OutputVariableError::NameContainsNonPrintableCharacters(
37                    name.to_string(),
38                ));
39            }
40        }
41        let value = self.value.trim();
42        if !value
43            .chars()
44            .all(|c| c.is_ascii_alphanumeric() || c.is_ascii_punctuation() || !c.is_ascii_control())
45        {
46            return Err(OutputVariableError::ValueContainsNonPrintableCharacters(
47                value.to_string(),
48            ));
49        }
50        Ok(())
51    }
52}
53
54impl Display for OutputVariable {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{}={}", self.name.trim(), self.value.trim())
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::{OutputVariable, OutputVariableError};
63
64    #[test]
65    fn empty_name() {
66        let var = OutputVariable {
67            name: "   ".to_string(),
68            value: "value".to_string(),
69        };
70        assert_eq!(var.validate(), Err(OutputVariableError::NameIsEmpty));
71    }
72
73    #[test]
74    fn name_starts_with_number() {
75        let var = OutputVariable {
76            name: "1var".to_string(),
77            value: "value".to_string(),
78        };
79        assert_eq!(
80            var.validate(),
81            Err(OutputVariableError::NameStartsWithNumber(
82                "1var".to_string()
83            ))
84        );
85    }
86
87    #[test]
88    fn name_contains_non_printable_characters() {
89        let var = OutputVariable {
90            name: "var\nname".to_string(),
91            value: "value".to_string(),
92        };
93        assert_eq!(
94            var.validate(),
95            Err(OutputVariableError::NameContainsNonPrintableCharacters(
96                "var\nname".to_string()
97            ))
98        );
99    }
100
101    #[test]
102    fn value_contains_non_printable_characters() {
103        let var = OutputVariable {
104            name: "var".to_string(),
105            value: "(val)\nline2".to_string(),
106        };
107        assert_eq!(
108            var.validate(),
109            Err(OutputVariableError::ValueContainsNonPrintableCharacters(
110                "(val)\nline2".to_string()
111            ))
112        );
113    }
114
115    #[test]
116    fn valid_variable() {
117        OutputVariable {
118            name: " VAR_NAME ".to_string(),
119            value: " value -(123) ".to_string(),
120        }
121        .validate()
122        .unwrap();
123    }
124}