1use std::fmt;
4use std::marker::PhantomData;
5use std::rc::Rc;
6
7use gh_workflow_macros::Context;
8
9use crate::Expression;
10
11#[derive(Clone)]
12pub struct Context<A> {
13 marker: PhantomData<A>,
14 step: Step,
15}
16
17#[derive(Default, Clone)]
18enum Step {
19 #[default]
20 Root,
21 Select {
22 name: Rc<String>,
23 object: Box<Step>,
24 },
25 Eq {
26 left: Box<Step>,
27 right: Box<Step>,
28 },
29 And {
30 left: Box<Step>,
31 right: Box<Step>,
32 },
33 Or {
34 left: Box<Step>,
35 right: Box<Step>,
36 },
37 Literal(String),
38 Concat {
39 left: Box<Step>,
40 right: Box<Step>,
41 },
42}
43
44impl<A> Context<A> {
45 fn new() -> Self {
46 Context { marker: PhantomData, step: Step::Root }
47 }
48
49 fn select<B>(&self, path: impl Into<String>) -> Context<B> {
50 Context {
51 marker: PhantomData,
52 step: Step::Select {
53 name: Rc::new(path.into()),
54 object: Box::new(self.step.clone()),
55 },
56 }
57 }
58
59 pub fn eq(&self, other: Context<A>) -> Context<bool> {
60 Context {
61 marker: Default::default(),
62 step: Step::Eq {
63 left: Box::new(self.step.clone()),
64 right: Box::new(other.step.clone()),
65 },
66 }
67 }
68
69 pub fn and(&self, other: Context<A>) -> Context<bool> {
70 Context {
71 marker: Default::default(),
72 step: Step::And {
73 left: Box::new(self.step.clone()),
74 right: Box::new(other.step.clone()),
75 },
76 }
77 }
78
79 pub fn or(&self, other: Context<A>) -> Context<bool> {
80 Context {
81 marker: Default::default(),
82 step: Step::Or {
83 left: Box::new(self.step.clone()),
84 right: Box::new(other.step.clone()),
85 },
86 }
87 }
88}
89
90impl Context<String> {
91 pub fn concat(&self, other: Context<String>) -> Context<String> {
92 Context {
93 marker: Default::default(),
94 step: Step::Concat {
95 left: Box::new(self.step.clone()),
96 right: Box::new(other.step),
97 },
98 }
99 }
100}
101
102#[allow(unused)]
103#[derive(Context)]
104pub struct Github {
105 action: String,
107 action_path: String,
110 action_ref: String,
113 action_repository: String,
116 action_status: String,
118 actor: String,
120 actor_id: String,
123 api_url: String,
125 base_ref: String,
127 env: String,
130 event: serde_json::Value,
132 event_name: String,
134 event_path: String,
137 graphql_url: String,
139 head_ref: String,
141 job: String,
143 path: String,
145 ref_name: String,
147 ref_protected: bool,
150 ref_type: String,
153 repository: String,
155 repository_id: String,
157 repository_owner: String,
159 repository_owner_id: String,
161 repository_url: String,
163 retention_days: String,
165 run_id: String,
167 run_number: String,
169 run_attempt: String,
172 secret_source: String,
174 server_url: String,
176 sha: String,
178 token: String,
181 triggering_actor: String,
183 workflow: String,
185 workflow_ref: String,
187 workflow_sha: String,
189 workspace: String,
191}
192
193impl Context<Github> {
194 pub fn ref_(&self) -> Context<String> {
195 self.select("ref")
196 }
197}
198
199impl fmt::Display for Step {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 Step::Root => write!(f, ""),
203 Step::Select { name, object } => {
204 if matches!(**object, Step::Root) {
205 write!(f, "{}", name)
206 } else {
207 write!(f, "{}.{}", object, name)
208 }
209 }
210 Step::Eq { left, right } => {
211 write!(f, "{} == {}", left, right)
212 }
213 Step::And { left, right } => {
214 write!(f, "{} && {}", left, right)
215 }
216 Step::Or { left, right } => {
217 write!(f, "{} || {}", left, right)
218 }
219 Step::Literal(value) => {
220 write!(f, "'{}'", value)
221 }
222 Step::Concat { left, right } => {
223 write!(f, "{}{}", left, right)
224 }
225 }
226 }
227}
228
229impl<A> fmt::Display for Context<A> {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 write!(f, "${{{{ {} }}}}", self.step.to_string().replace('"', ""))
232 }
233}
234
235impl<A> From<Context<A>> for Expression {
236 fn from(value: Context<A>) -> Self {
237 Expression::new(value.to_string())
238 }
239}
240
241impl<T: Into<String>> From<T> for Context<String> {
242 fn from(value: T) -> Self {
243 Context {
244 marker: Default::default(),
245 step: Step::Literal(value.into()),
246 }
247 }
248}
249
250#[allow(unused)]
251#[derive(Context)]
252pub struct Job {
254 container: Container,
257
258 services: Services,
261
262 status: JobStatus,
264}
265
266#[derive(Clone)]
268pub enum JobStatus {
269 Success,
271 Failure,
273 Cancelled,
275}
276
277#[derive(Context)]
278#[allow(unused)]
279pub struct Container {
282 id: String,
284 network: String,
286}
287
288#[derive(Context)]
289
290pub struct Services {}
293
294#[cfg(test)]
295mod test {
296 use pretty_assertions::assert_eq;
297
298 use super::*;
299
300 #[test]
301 fn test_expr() {
302 let github = Context::github(); assert_eq!(github.to_string(), "${{ github }}");
305
306 let action = github.action(); assert_eq!(action.to_string(), "${{ github.action }}");
308
309 let action_path = github.action_path(); assert_eq!(action_path.to_string(), "${{ github.action_path }}");
311 }
312
313 #[test]
314 fn test_expr_eq() {
315 let github = Context::github();
316 let action = github.action();
317 let action_path = github.action_path();
318
319 let expr = action.eq(action_path);
320
321 assert_eq!(
322 expr.to_string(),
323 "${{ github.action == github.action_path }}"
324 );
325 }
326
327 #[test]
328 fn test_expr_and() {
329 let push = Context::github().event_name().eq("push".into());
330 let main = Context::github().ref_().eq("ref/heads/main".into());
331 let expr = push.and(main);
332
333 assert_eq!(
334 expr.to_string(),
335 "${{ github.event_name == 'push' && github.ref == 'ref/heads/main' }}"
336 )
337 }
338
339 #[test]
340 fn test_expr_or() {
341 let github = Context::github();
342 let action = github.action();
343 let action_path = github.action_path();
344 let action_ref = github.action_ref();
345
346 let expr = action.eq(action_path).or(action.eq(action_ref));
347
348 assert_eq!(
349 expr.to_string(),
350 "${{ github.action == github.action_path || github.action == github.action_ref }}"
351 );
352 }
353}