Skip to main content

ephemeris_expr/
lib.rs

1//! A value that is computed at runtime from the process inputs.
2//!
3//! The state document carries [`Expr`] values. The command line evaluates
4//! them. This crate holds the type and its JSON shape only.
5
6use std::fmt;
7
8use facet::Facet;
9
10/// Where a value comes from.
11///
12/// An expression is externally tagged in JSON, with snake case tags.
13///
14/// ```
15/// use ephemeris_expr::Expr;
16///
17/// let expr = Expr::First(vec![
18///     Expr::Environment("TOKEN".to_owned()),
19///     Expr::Literal("fallback".to_owned()),
20/// ]);
21/// assert_eq!(
22///     expr.to_string(),
23///     "first of [environment TOKEN, literal \"fallback\"]"
24/// );
25/// ```
26#[derive(Clone, Debug, Eq, Facet, PartialEq)]
27#[facet(rename_all = "snake_case")]
28#[repr(u8)]
29pub enum Expr {
30    /// The text itself.
31    Literal(String),
32    /// The environment variable of that name.
33    Environment(String),
34    /// The environment variable of that name, declared as a CI variable.
35    Variable(String),
36    /// The environment variable of that name, declared as a CI secret.
37    Secret(String),
38    /// The contents of the file the inner expression names.
39    File(Box<Self>),
40    /// The first expression whose inputs are present.
41    First(Vec<Self>),
42}
43
44impl fmt::Display for Expr {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::Literal(text) => write!(f, "literal \"{text}\""),
48            Self::Environment(name) => write!(f, "environment {name}"),
49            Self::Variable(name) => write!(f, "variable {name}"),
50            Self::Secret(name) => write!(f, "secret {name}"),
51            Self::File(inner) => write!(f, "file from {inner}"),
52            Self::First(exprs) => {
53                let exprs: Vec<_> = exprs.iter().map(ToString::to_string).collect();
54                write!(f, "first of [{}]", exprs.join(", "))
55            }
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::Expr;
63
64    fn decode(json: &str) -> Expr {
65        facet_json::from_str(json).expect("expression decodes")
66    }
67
68    #[test]
69    fn every_variant_decodes() {
70        assert_eq!(
71            decode(r#"{"literal":"text"}"#),
72            Expr::Literal("text".to_owned())
73        );
74        assert_eq!(
75            decode(r#"{"environment":"A"}"#),
76            Expr::Environment("A".to_owned())
77        );
78        assert_eq!(
79            decode(r#"{"variable":"B"}"#),
80            Expr::Variable("B".to_owned())
81        );
82        assert_eq!(decode(r#"{"secret":"C"}"#), Expr::Secret("C".to_owned()));
83        assert_eq!(
84            decode(r#"{"file":{"environment":"D"}}"#),
85            Expr::File(Box::new(Expr::Environment("D".to_owned())))
86        );
87        assert_eq!(
88            decode(r#"{"first":[{"literal":"x"},{"secret":"E"}]}"#),
89            Expr::First(vec![
90                Expr::Literal("x".to_owned()),
91                Expr::Secret("E".to_owned())
92            ])
93        );
94    }
95}