-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from woodruffw/ww/better-expr
- Loading branch information
Showing
5 changed files
with
150 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
//! GitHub Actions expression parsing and handling. | ||
use serde::{Deserialize, Serialize}; | ||
|
||
/// An explicit GitHub Actions expression, fenced by `${{ <expr> }}`. | ||
#[derive(Debug, PartialEq, Serialize)] | ||
pub struct ExplicitExpr(String); | ||
|
||
impl ExplicitExpr { | ||
/// Construct an `ExplicitExpr` from the given string, consuming it | ||
/// in the process. | ||
/// | ||
/// Returns `None` if the input is not a valid explicit expression. | ||
pub fn from_curly(expr: impl Into<String>) -> Option<Self> { | ||
// Invariant preservation: we store the full string, but | ||
// we expect it to be a well-formed expression. | ||
let expr = expr.into(); | ||
let trimmed = expr.trim(); | ||
if !trimmed.starts_with("${{") || !trimmed.ends_with("}}") { | ||
return None; | ||
} | ||
|
||
Some(ExplicitExpr(expr)) | ||
} | ||
|
||
/// Return the original string underlying this expression, including | ||
/// its exact whitespace and curly delimiters. | ||
pub fn as_raw(&self) -> &str { | ||
&self.0 | ||
} | ||
|
||
/// Return the "curly" form of this expression, with leading and trailing | ||
/// whitespace removed. | ||
/// | ||
/// Whitespace *within* the expression body is not removed or normalized. | ||
pub fn as_curly(&self) -> &str { | ||
self.as_raw().trim() | ||
} | ||
|
||
/// Return the "bare" form of this expression, i.e. the `body` within | ||
/// `${{ body }}`. Leading and trailing whitespace within | ||
/// the expression body is removed. | ||
pub fn as_bare(&self) -> &str { | ||
return 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) | ||
} | ||
} | ||
|
||
/// A "literal or expr" type, for places in GitHub Actions where a | ||
/// key can either have a literal value (array, object, etc.) or an | ||
/// expression string. | ||
#[derive(Debug, Deserialize, PartialEq, Serialize)] | ||
#[serde(untagged)] | ||
pub enum LoE<T> { | ||
// Observe that `Expr` comes first, since `LoE<String>` should always | ||
// attempt to parse as an expression before falling back on a literal | ||
// string. | ||
Expr(ExplicitExpr), | ||
Literal(T), | ||
} | ||
|
||
impl<T> Default for LoE<T> | ||
where | ||
T: Default, | ||
{ | ||
fn default() -> Self { | ||
Self::Literal(T::default()) | ||
} | ||
} | ||
|
||
/// A convenience alias for a `bool` literal or an actions expression. | ||
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 }}", | ||
]; | ||
|
||
for case in cases { | ||
let case = format!("\"{case}\""); | ||
assert!(serde_yaml::from_str::<ExplicitExpr>(&case).is_err()); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_expr() { | ||
let expr = "\" ${{ foo }} \\t \""; | ||
let expr: ExplicitExpr = serde_yaml::from_str(&expr).unwrap(); | ||
assert_eq!(expr.as_bare(), "foo"); | ||
} | ||
|
||
#[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(_) | ||
)); | ||
|
||
// Invalid expr deserializes as string. | ||
let invalid = "\"${{ invalid \""; | ||
assert_eq!( | ||
serde_yaml::from_str::<LoE<String>>(invalid).unwrap(), | ||
LoE::Literal("${{ invalid ".to_string()) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters