use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize)]
pub struct ExplicitExpr(String);
impl ExplicitExpr {
pub fn from_curly(expr: impl Into<String>) -> Option<Self> {
let expr = expr.into();
if !expr.starts_with("${{") || !expr.ends_with("}}") {
return None;
}
Some(ExplicitExpr(expr))
}
pub fn as_raw(&self) -> &str {
&self.0
}
pub fn as_curly(&self) -> &str {
self.as_raw().trim()
}
pub fn as_bare(&self) -> &str {
self.as_curly()
.strip_prefix("${{")
.and_then(|e| e.strip_suffix("}}"))
.map(|e| e.trim())
.expect("invariant violated: ExplicitExpr must be an expression")
}
}
impl<'de> Deserialize<'de> for ExplicitExpr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
let Some(expr) = Self::from_curly(raw) else {
return Err(serde::de::Error::custom(
"invalid expression: expected '${{' and '}}' delimiters",
));
};
Ok(expr)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(untagged)]
pub enum LoE<T> {
Expr(ExplicitExpr),
Literal(T),
}
impl<T> Default for LoE<T>
where
T: Default,
{
fn default() -> Self {
Self::Literal(T::default())
}
}
pub type BoE = LoE<bool>;
#[cfg(test)]
mod tests {
use super::{ExplicitExpr, LoE};
#[test]
fn test_expr_invalid() {
let cases = &[
"not an expression",
"${{ missing end ",
"missing beginning }}",
" ${{ leading whitespace }}",
"${{ trailing whitespace }} ",
];
for case in cases {
let case = format!("\"{case}\"");
assert!(serde_yaml::from_str::<ExplicitExpr>(&case).is_err());
}
}
#[test]
fn test_expr() {
for (case, expected) in &[
("${{ foo }}", "foo"),
("${{ foo.bar }}", "foo.bar"),
("${{ foo['bar'] }}", "foo['bar']"),
("${{foo}}", "foo"),
("${{ foo}}", "foo"),
("${{ foo }}", "foo"),
] {
let case = format!("\"{case}\"");
let expr: ExplicitExpr = serde_yaml::from_str(&case).unwrap();
assert_eq!(expr.as_bare(), *expected);
}
}
#[test]
fn test_loe() {
let lit = "\"normal string\"";
assert_eq!(
serde_yaml::from_str::<LoE<String>>(lit).unwrap(),
LoE::Literal("normal string".to_string())
);
let expr = "\"${{ expr }}\"";
assert!(matches!(
serde_yaml::from_str::<LoE<String>>(expr).unwrap(),
LoE::Expr(_)
));
let invalid = "\"${{ invalid \"";
assert_eq!(
serde_yaml::from_str::<LoE<String>>(invalid).unwrap(),
LoE::Literal("${{ invalid ".to_string())
);
}
}