use std::fmt;
use std::marker::PhantomData;
use std::rc::Rc;
use gh_workflow_macros::Context;
use crate::Expression;
#[derive(Clone)]
pub struct Context<A> {
marker: PhantomData<A>,
step: Step,
}
#[derive(Default, Clone)]
enum Step {
#[default]
Root,
Select {
name: Rc<String>,
object: Box<Step>,
},
Eq {
left: Box<Step>,
right: Box<Step>,
},
And {
left: Box<Step>,
right: Box<Step>,
},
Or {
left: Box<Step>,
right: Box<Step>,
},
Literal(String),
Concat {
left: Box<Step>,
right: Box<Step>,
},
}
impl<A> Context<A> {
fn new() -> Self {
Context { marker: PhantomData, step: Step::Root }
}
fn select<B>(&self, path: impl Into<String>) -> Context<B> {
Context {
marker: PhantomData,
step: Step::Select {
name: Rc::new(path.into()),
object: Box::new(self.step.clone()),
},
}
}
pub fn eq(&self, other: Context<A>) -> Context<bool> {
Context {
marker: Default::default(),
step: Step::Eq {
left: Box::new(self.step.clone()),
right: Box::new(other.step.clone()),
},
}
}
pub fn and(&self, other: Context<A>) -> Context<bool> {
Context {
marker: Default::default(),
step: Step::And {
left: Box::new(self.step.clone()),
right: Box::new(other.step.clone()),
},
}
}
pub fn or(&self, other: Context<A>) -> Context<bool> {
Context {
marker: Default::default(),
step: Step::Or {
left: Box::new(self.step.clone()),
right: Box::new(other.step.clone()),
},
}
}
}
impl Context<String> {
pub fn concat(&self, other: Context<String>) -> Context<String> {
Context {
marker: Default::default(),
step: Step::Concat {
left: Box::new(self.step.clone()),
right: Box::new(other.step),
},
}
}
}
#[allow(unused)]
#[derive(Context)]
pub struct Github {
action: String,
action_path: String,
action_ref: String,
action_repository: String,
action_status: String,
actor: String,
actor_id: String,
api_url: String,
base_ref: String,
env: String,
event: serde_json::Value,
event_name: String,
event_path: String,
graphql_url: String,
head_ref: String,
job: String,
path: String,
ref_name: String,
ref_protected: bool,
ref_type: String,
repository: String,
repository_id: String,
repository_owner: String,
repository_owner_id: String,
repository_url: String,
retention_days: String,
run_id: String,
run_number: String,
run_attempt: String,
secret_source: String,
server_url: String,
sha: String,
token: String,
triggering_actor: String,
workflow: String,
workflow_ref: String,
workflow_sha: String,
workspace: String,
}
impl Context<Github> {
pub fn ref_(&self) -> Context<String> {
self.select("ref")
}
}
impl fmt::Display for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Step::Root => write!(f, ""),
Step::Select { name, object } => {
if matches!(**object, Step::Root) {
write!(f, "{}", name)
} else {
write!(f, "{}.{}", object, name)
}
}
Step::Eq { left, right } => {
write!(f, "{} == {}", left, right)
}
Step::And { left, right } => {
write!(f, "{} && {}", left, right)
}
Step::Or { left, right } => {
write!(f, "{} || {}", left, right)
}
Step::Literal(value) => {
write!(f, "'{}'", value)
}
Step::Concat { left, right } => {
write!(f, "{}{}", left, right)
}
}
}
}
impl<A> fmt::Display for Context<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "${{{{ {} }}}}", self.step.to_string().replace('"', ""))
}
}
impl<A> From<Context<A>> for Expression {
fn from(value: Context<A>) -> Self {
Expression::new(value.to_string())
}
}
impl<T: Into<String>> From<T> for Context<String> {
fn from(value: T) -> Self {
Context {
marker: Default::default(),
step: Step::Literal(value.into()),
}
}
}
#[allow(unused)]
#[derive(Context)]
pub struct Job {
container: Container,
services: Services,
status: JobStatus,
}
#[derive(Clone)]
pub enum JobStatus {
Success,
Failure,
Cancelled,
}
#[derive(Context)]
#[allow(unused)]
pub struct Container {
id: String,
network: String,
}
#[derive(Context)]
pub struct Services {}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_expr() {
let github = Context::github();
assert_eq!(github.to_string(), "${{ github }}");
let action = github.action(); assert_eq!(action.to_string(), "${{ github.action }}");
let action_path = github.action_path(); assert_eq!(action_path.to_string(), "${{ github.action_path }}");
}
#[test]
fn test_expr_eq() {
let github = Context::github();
let action = github.action();
let action_path = github.action_path();
let expr = action.eq(action_path);
assert_eq!(
expr.to_string(),
"${{ github.action == github.action_path }}"
);
}
#[test]
fn test_expr_and() {
let push = Context::github().event_name().eq("push".into());
let main = Context::github().ref_().eq("ref/heads/main".into());
let expr = push.and(main);
assert_eq!(
expr.to_string(),
"${{ github.event_name == 'push' && github.ref == 'ref/heads/main' }}"
)
}
#[test]
fn test_expr_or() {
let github = Context::github();
let action = github.action();
let action_path = github.action_path();
let action_ref = github.action_ref();
let expr = action.eq(action_path).or(action.eq(action_ref));
assert_eq!(
expr.to_string(),
"${{ github.action == github.action_path || github.action == github.action_ref }}"
);
}
}