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    #![allow(clippy::unwrap_used)]
63
64    use super::{OutputVariable, OutputVariableError};
65
66    #[test]
67    fn empty_name() {
68        let var = OutputVariable {
69            name: "   ".to_string(),
70            value: "value".to_string(),
71        };
72        assert_eq!(var.validate(), Err(OutputVariableError::NameIsEmpty));
73    }
74
75    #[test]
76    fn name_starts_with_number() {
77        let var = OutputVariable {
78            name: "1var".to_string(),
79            value: "value".to_string(),
80        };
81        assert_eq!(
82            var.validate(),
83            Err(OutputVariableError::NameStartsWithNumber(
84                "1var".to_string()
85            ))
86        );
87    }
88
89    #[test]
90    fn name_contains_non_printable_characters() {
91        let var = OutputVariable {
92            name: "var\nname".to_string(),
93            value: "value".to_string(),
94        };
95        assert_eq!(
96            var.validate(),
97            Err(OutputVariableError::NameContainsNonPrintableCharacters(
98                "var\nname".to_string()
99            ))
100        );
101    }
102
103    #[test]
104    fn value_contains_non_printable_characters() {
105        let var = OutputVariable {
106            name: "var".to_string(),
107            value: "(val)\nline2".to_string(),
108        };
109        assert_eq!(
110            var.validate(),
111            Err(OutputVariableError::ValueContainsNonPrintableCharacters(
112                "(val)\nline2".to_string()
113            ))
114        );
115    }
116
117    #[test]
118    fn valid_variable() {
119        OutputVariable {
120            name: " VAR_NAME ".to_string(),
121            value: " value -(123) ".to_string(),
122        }
123        .validate()
124        .unwrap();
125    }
126}