Skip to content

Commit fa48032

Browse files
authored
feat: add expr (#59)
1 parent 2636a12 commit fa48032

File tree

9 files changed

+401
-23
lines changed

9 files changed

+401
-23
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
/target
1+
/target/
2+
/**/target/

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ serde = { version = "1.0.210", features = ["derive"] }
1919
serde_json = { version = "1.0.128" }
2020
serde_yaml = "0.9.34"
2121
strum_macros = "0.26.4"
22+
gh-workflow-macros = { path = "./macros" }
23+
2224

2325
[dev-dependencies]
2426
insta = "1.40.0"

macros/Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

macros/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "gh-workflow-macros"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
syn = "1.0"
11+
quote = "1.0"
12+
heck = "0.5.0"

macros/src/lib.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use heck::ToSnakeCase;
2+
use proc_macro::TokenStream;
3+
use quote::quote;
4+
use syn::{parse_macro_input, Data, DeriveInput, Fields};
5+
6+
#[proc_macro_derive(Expr)]
7+
pub fn derive_expr(input: TokenStream) -> TokenStream {
8+
let input = parse_macro_input!(input as DeriveInput);
9+
let struct_name = input.ident;
10+
let ctor_name = struct_name.to_string().to_snake_case();
11+
let ctor_id = syn::Ident::new(&ctor_name, struct_name.span());
12+
13+
// Ensure it's a struct and get its fields
14+
let fields = if let Data::Struct(data_struct) = input.data {
15+
if let Fields::Named(fields) = data_struct.fields {
16+
fields
17+
} else {
18+
panic!("#[derive(Expr)] only supports structs with named fields")
19+
}
20+
} else {
21+
panic!("#[derive(Expr)] can only be used with structs");
22+
};
23+
24+
// Generate methods for each field
25+
let methods = fields.named.iter().map(|field| {
26+
let field_name = &field.ident;
27+
let field_type = &field.ty;
28+
let field_name_str = field_name.as_ref().unwrap().to_string();
29+
quote! {
30+
pub fn #field_name(&self) -> Expr<#field_type> {
31+
self.select::<#field_type>(#field_name_str)
32+
}
33+
}
34+
});
35+
36+
// Generate the output code
37+
let expanded = quote! {
38+
impl Expr<#struct_name> {
39+
#(#methods)*
40+
41+
pub fn #ctor_id() -> Self {
42+
Expr::<Github>::new().select(stringify!(#ctor_name))
43+
}
44+
}
45+
};
46+
47+
// eprintln!("Generated code:\n{}", expanded);
48+
49+
TokenStream::from(expanded)
50+
}

src/expr.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use std::fmt::Display;
2+
use std::marker::PhantomData;
3+
use std::rc::Rc;
4+
5+
use gh_workflow_macros::Expr;
6+
7+
pub struct Expr<A> {
8+
marker: PhantomData<A>,
9+
step: Step,
10+
}
11+
12+
#[derive(Default, Clone)]
13+
enum Step {
14+
#[default]
15+
Root,
16+
Select {
17+
name: Rc<String>,
18+
object: Box<Step>,
19+
},
20+
}
21+
22+
impl Step {
23+
fn select(name: impl Into<String>) -> Step {
24+
Step::Select { name: Rc::new(name.into()), object: Box::new(Step::Root) }
25+
}
26+
}
27+
28+
impl<A> Expr<A> {
29+
fn new() -> Self {
30+
Expr { marker: PhantomData, step: Step::Root }
31+
}
32+
33+
fn select<B>(&self, path: impl Into<String>) -> Expr<B> {
34+
Expr {
35+
marker: PhantomData,
36+
step: Step::Select {
37+
name: Rc::new(path.into()),
38+
object: Box::new(self.step.clone()),
39+
},
40+
}
41+
}
42+
}
43+
44+
impl<A> Display for Expr<A> {
45+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46+
let mut stack: Vec<Step> = vec![self.step.clone()];
47+
48+
write!(f, "{{{{ ")?;
49+
50+
loop {
51+
match stack.pop() {
52+
None => break,
53+
Some(step) => match step {
54+
Step::Root => break,
55+
Step::Select { name, object } => {
56+
if matches!(*object, Step::Root) {
57+
write!(f, "{}", name.replace('"', ""))?;
58+
} else {
59+
stack.push(Step::select(name.as_str()));
60+
// TODO: this is a hack to insert a `.` between the two steps
61+
stack.push(Step::select("."));
62+
stack.push(*object);
63+
}
64+
}
65+
},
66+
}
67+
}
68+
69+
write!(f, " }}}}")
70+
}
71+
}
72+
73+
#[derive(Expr)]
74+
pub struct Github {
75+
/// The name of the action currently running, or the id of a step.
76+
action: String,
77+
/// The path where an action is located. This property is only supported in
78+
/// composite actions.
79+
action_path: String,
80+
/// For a step executing an action, this is the ref of the action being
81+
/// executed.
82+
action_ref: String,
83+
/// For a step executing an action, this is the owner and repository name of
84+
/// the action.
85+
action_repository: String,
86+
/// For a composite action, the current result of the composite action.
87+
action_status: String,
88+
/// The username of the user that triggered the initial workflow run.
89+
actor: String,
90+
/// The account ID of the person or app that triggered the initial workflow
91+
/// run.
92+
actor_id: String,
93+
/// The URL of the GitHub REST API.
94+
api_url: String,
95+
/// The base_ref or target branch of the pull request in a workflow run.
96+
base_ref: String,
97+
/// Path on the runner to the file that sets environment variables from
98+
/// workflow commands.
99+
env: String,
100+
/// The full event webhook payload.
101+
event: serde_json::Value,
102+
/// The name of the event that triggered the workflow run.
103+
event_name: String,
104+
/// The path to the file on the runner that contains the full event webhook
105+
/// payload.
106+
event_path: String,
107+
/// The URL of the GitHub GraphQL API.
108+
graphql_url: String,
109+
/// The head_ref or source branch of the pull request in a workflow run.
110+
head_ref: String,
111+
/// The job id of the current job.
112+
job: String,
113+
/// The path of the repository.
114+
path: String,
115+
/// The short ref name of the branch or tag that triggered the workflow run.
116+
ref_name: String,
117+
/// true if branch protections are configured for the ref that triggered the
118+
/// workflow run.
119+
ref_protected: bool,
120+
/// The type of ref that triggered the workflow run. Valid values are branch
121+
/// or tag.
122+
ref_type: String,
123+
/// The owner and repository name.
124+
repository: String,
125+
/// The ID of the repository.
126+
repository_id: String,
127+
/// The repository owner's username.
128+
repository_owner: String,
129+
/// The repository owner's account ID.
130+
repository_owner_id: String,
131+
/// The Git URL to the repository.
132+
repository_url: String,
133+
/// The number of days that workflow run logs and artifacts are kept.
134+
retention_days: String,
135+
/// A unique number for each workflow run within a repository.
136+
run_id: String,
137+
/// A unique number for each run of a particular workflow in a repository.
138+
run_number: String,
139+
/// A unique number for each attempt of a particular workflow run in a
140+
/// repository.
141+
run_attempt: String,
142+
/// The source of a secret used in a workflow.
143+
secret_source: String,
144+
/// The URL of the GitHub server.
145+
server_url: String,
146+
/// The commit SHA that triggered the workflow.
147+
sha: String,
148+
/// A token to authenticate on behalf of the GitHub App installed on your
149+
/// repository.
150+
token: String,
151+
/// The username of the user that initiated the workflow run.
152+
triggering_actor: String,
153+
/// The name of the workflow.
154+
workflow: String,
155+
/// The ref path to the workflow.
156+
workflow_ref: String,
157+
/// The commit SHA for the workflow file.
158+
workflow_sha: String,
159+
/// The default working directory on the runner for steps.
160+
workspace: String,
161+
}
162+
163+
impl Expr<Github> {
164+
pub fn ref_(&self) -> Expr<String> {
165+
self.select("ref")
166+
}
167+
}
168+
169+
#[derive(Expr)]
170+
171+
/// The job context contains information about the currently running job.
172+
pub struct Job {
173+
/// A unique number for each container in a job. This property is only
174+
/// available if the job uses a container.
175+
container: Container,
176+
177+
/// The services configured for a job. This property is only available if
178+
/// the job uses service containers.
179+
services: Services,
180+
181+
/// The status of the current job.
182+
status: JobStatus,
183+
}
184+
185+
/// The status of a job execution
186+
#[derive(Clone)]
187+
pub enum JobStatus {
188+
/// The job completed successfully
189+
Success,
190+
/// The job failed
191+
Failure,
192+
/// The job was cancelled
193+
Cancelled,
194+
}
195+
196+
#[derive(Expr)]
197+
198+
/// Container information for a job. This is only available if the job runs in a
199+
/// container.
200+
pub struct Container {
201+
/// The ID of the container
202+
id: String,
203+
/// The container network
204+
network: String,
205+
}
206+
207+
#[derive(Expr)]
208+
209+
/// Services configured for a job. This is only available if the job uses
210+
/// service containers.
211+
pub struct Services {}
212+
213+
#[cfg(test)]
214+
mod test {
215+
use pretty_assertions::assert_eq;
216+
217+
use super::*;
218+
219+
#[test]
220+
fn test_expr() {
221+
let github = Expr::github(); // Expr<Github>
222+
223+
assert_eq!(github.to_string(), "{{ github }}");
224+
225+
let action = github.action(); // Expr<String>
226+
assert_eq!(action.to_string(), "{{ github.action }}");
227+
228+
let action_path = github.action_path(); // Expr<String>
229+
assert_eq!(action_path.to_string(), "{{ github.action_path }}");
230+
}
231+
}

0 commit comments

Comments
 (0)