Skip to main content

git_bot_feedback/
output_variable.rs

1#[cfg(feature = "pyo3")]
2use pyo3::prelude::*;
3
4use std::fmt::Display;
5
6use crate::error::OutputVariableError;
7
8/// A type to represent an output variable.
9///
10/// This is akin to the key/value pairs used in most
11/// config file formats but with some limitations:
12///
13/// - Both [`Self::name`] and [`Self::value`] must be UTF-8 encoded.
14/// - The [`Self::value`] cannot span multiple lines.
15#[derive(Debug, Clone)]
16#[cfg_attr(
17    feature = "pyo3",
18    pyclass(module = "git_bot_feedback", from_py_object, str, get_all, set_all)
19)]
20pub struct OutputVariable {
21    /// The output variable's name.
22    pub name: String,
23
24    /// The output variable's value.
25    pub value: String,
26}
27
28#[cfg(feature = "pyo3")]
29#[pymethods]
30impl OutputVariable {
31    /// Create a new output variable instance.
32    #[new]
33    #[pyo3(
34        signature = (name, value),
35        text_signature = "(name: str, value: str)"
36    )]
37    pub fn new_py(name: String, value: String) -> Self {
38        Self { name, value }
39    }
40
41    /// Validate that the output variable is well-formed.
42    ///
43    /// Instead of returning a false boolean value when the
44    /// output variable is somehow invalid, this method raises an
45    /// exception to describe a specific problem. Prefer using a
46    /// try/except block with this function instead of checking
47    /// the return value (which is ``None``).
48    pub fn validate_py(&self) -> PyResult<()> {
49        self.validate()?;
50        Ok(())
51    }
52}
53
54impl OutputVariable {
55    /// Validate that the output variable is well-formed.
56    ///
57    /// Typically only used by implementations of
58    /// [`RestApiClient::write_output_variables`](crate::client::RestApiClient::write_output_variables).
59    pub fn validate(&self) -> Result<(), OutputVariableError> {
60        let name = self.name.trim();
61        if name.is_empty() {
62            return Err(OutputVariableError::NameIsEmpty);
63        }
64        for (i, c) in name.chars().enumerate() {
65            if i == 0 && c.is_ascii_digit() {
66                return Err(OutputVariableError::NameStartsWithNumber(name.to_string()));
67            }
68            if !(c.is_ascii_alphanumeric() || c == '_' || c == '-') {
69                return Err(OutputVariableError::NameContainsNonPrintableCharacters(
70                    name.to_string(),
71                ));
72            }
73        }
74        let value = self.value.trim();
75        if !value
76            .chars()
77            .all(|c| c.is_ascii_alphanumeric() || c.is_ascii_punctuation() || !c.is_ascii_control())
78        {
79            return Err(OutputVariableError::ValueContainsNonPrintableCharacters(
80                value.to_string(),
81            ));
82        }
83        Ok(())
84    }
85}
86
87impl Display for OutputVariable {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "{}={}", self.name.trim(), self.value.trim())
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    #![allow(clippy::unwrap_used)]
96
97    use super::{OutputVariable, OutputVariableError};
98
99    #[test]
100    fn empty_name() {
101        let var = OutputVariable {
102            name: "   ".to_string(),
103            value: "value".to_string(),
104        };
105        assert_eq!(var.validate(), Err(OutputVariableError::NameIsEmpty));
106    }
107
108    #[test]
109    fn name_starts_with_number() {
110        let var = OutputVariable {
111            name: "1var".to_string(),
112            value: "value".to_string(),
113        };
114        assert_eq!(
115            var.validate(),
116            Err(OutputVariableError::NameStartsWithNumber(
117                "1var".to_string()
118            ))
119        );
120    }
121
122    #[test]
123    fn name_contains_non_printable_characters() {
124        let var = OutputVariable {
125            name: "var\nname".to_string(),
126            value: "value".to_string(),
127        };
128        assert_eq!(
129            var.validate(),
130            Err(OutputVariableError::NameContainsNonPrintableCharacters(
131                "var\nname".to_string()
132            ))
133        );
134    }
135
136    #[test]
137    fn value_contains_non_printable_characters() {
138        let var = OutputVariable {
139            name: "var".to_string(),
140            value: "(val)\nline2".to_string(),
141        };
142        assert_eq!(
143            var.validate(),
144            Err(OutputVariableError::ValueContainsNonPrintableCharacters(
145                "(val)\nline2".to_string()
146            ))
147        );
148    }
149
150    #[test]
151    fn valid_variable() {
152        OutputVariable {
153            name: " VAR_NAME ".to_string(),
154            value: " value -(123) ".to_string(),
155        }
156        .validate()
157        .unwrap();
158    }
159}