Skip to main content

ephemeris_forge/
variable_name.rs

1use std::collections::BTreeSet;
2
3/// The names a forge refuses for Actions variables, beyond the shared shape.
4#[derive(Clone, Copy, Debug)]
5pub struct VariableNameRules {
6    /// Names that are reserved in any letter case.
7    pub reserved_names: &'static [&'static str],
8    /// Prefixes that are reserved in any letter case.
9    pub reserved_prefixes: &'static [&'static str],
10}
11
12/// A set of Actions variable names that a forge cannot store.
13#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
14pub enum VariableNameError {
15    /// The name has the wrong shape or is reserved.
16    #[error("{name:?} is not a valid Actions variable name")]
17    Invalid {
18        /// The rejected name.
19        name: String,
20    },
21    /// Two names differ only in letter case.
22    #[error("Actions variable {name:?} has another name that differs only in letter case")]
23    CaseClash {
24        /// One of the clashing names.
25        name: String,
26    },
27}
28
29impl VariableNameRules {
30    /// Whether `name` is acceptable.
31    ///
32    /// A name has letters, digits and underscores and does not start with a
33    /// digit. It is not reserved.
34    #[must_use]
35    pub fn is_valid(&self, name: &str) -> bool {
36        let upper = name.to_ascii_uppercase();
37        let mut characters = name.chars();
38        characters
39            .next()
40            .is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
41            && characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
42            && !self.reserved_names.contains(&upper.as_str())
43            && !self
44                .reserved_prefixes
45                .iter()
46                .any(|prefix| upper.starts_with(prefix))
47    }
48
49    /// Check the names of one scope, which forges store in upper case.
50    ///
51    /// # Errors
52    ///
53    /// Returns the first invalid name, or the first name that differs only in
54    /// letter case from an earlier one.
55    pub fn check<'a, I: IntoIterator<Item = &'a str>>(
56        &self,
57        names: I,
58    ) -> Result<(), VariableNameError> {
59        let mut seen = BTreeSet::new();
60        for name in names {
61            if !self.is_valid(name) {
62                return Err(VariableNameError::Invalid {
63                    name: name.to_owned(),
64                });
65            }
66            if !seen.insert(name.to_ascii_uppercase()) {
67                return Err(VariableNameError::CaseClash {
68                    name: name.to_owned(),
69                });
70            }
71        }
72        Ok(())
73    }
74}