microsoft/openvmm

Public

mirrored fromhttps://github.com/microsoft/openvmmAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
db5d175dd4b6daae00a32dfb2f2bd38b981c02ae

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

flowey/flowey_core/src/node.rs

2909lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Core types and traits used to create and work with flowey nodes.
5
6mod github_context;
7mod spec;
8
9pub use github_context::GhOutput;
10pub use github_context::GhToRust;
11pub use github_context::RustToGh;
12
13use self::steps::ado::AdoRuntimeVar;
14use self::steps::ado::AdoStepServices;
15use self::steps::github::GhStepBuilder;
16use self::steps::rust::RustRuntimeServices;
17use self::user_facing::ClaimedGhParam;
18use self::user_facing::GhPermission;
19use self::user_facing::GhPermissionValue;
20use crate::node::github_context::GhContextVarReader;
21use github_context::state::Root;
22use serde::Deserialize;
23use serde::Serialize;
24use serde::de::DeserializeOwned;
25use std::cell::RefCell;
26use std::collections::BTreeMap;
27use std::path::PathBuf;
28use std::rc::Rc;
29use user_facing::GhParam;
30
31/// Node types which are considered "user facing", and re-exported in the
32/// `flowey` crate.
33pub mod user_facing {
34 pub use super::ClaimVar;
35 pub use super::ClaimedReadVar;
36 pub use super::ClaimedWriteVar;
37 pub use super::FlowArch;
38 pub use super::FlowBackend;
39 pub use super::FlowNode;
40 pub use super::FlowPlatform;
41 pub use super::FlowPlatformKind;
42 pub use super::GhUserSecretVar;
43 pub use super::ImportCtx;
44 pub use super::IntoRequest;
45 pub use super::NodeCtx;
46 pub use super::ReadVar;
47 pub use super::SideEffect;
48 pub use super::SimpleFlowNode;
49 pub use super::StepCtx;
50 pub use super::VarClaimed;
51 pub use super::VarEqBacking;
52 pub use super::VarNotClaimed;
53 pub use super::WriteVar;
54 pub use super::steps::ado::AdoResourcesRepositoryId;
55 pub use super::steps::ado::AdoRuntimeVar;
56 pub use super::steps::ado::AdoStepServices;
57 pub use super::steps::github::ClaimedGhParam;
58 pub use super::steps::github::GhParam;
59 pub use super::steps::github::GhPermission;
60 pub use super::steps::github::GhPermissionValue;
61 pub use super::steps::rust::RustRuntimeServices;
62 pub use crate::flowey_request;
63 pub use crate::new_flow_node;
64 pub use crate::new_simple_flow_node;
65 pub use crate::node::FlowPlatformLinuxDistro;
66 pub use crate::pipeline::Artifact;
67
68 /// Helper method to streamline request validation in cases where a value is
69 /// expected to be identical across all incoming requests.
70 pub fn same_across_all_reqs<T: PartialEq>(
71 req_name: &str,
72 var: &mut Option<T>,
73 new: T,
74 ) -> anyhow::Result<()> {
75 match (var.as_ref(), new) {
76 (None, v) => *var = Some(v),
77 (Some(old), new) => {
78 if *old != new {
79 anyhow::bail!("`{}` must be consistent across requests", req_name);
80 }
81 }
82 }
83
84 Ok(())
85 }
86
87 /// Helper method to streamline request validation in cases where a value is
88 /// expected to be identical across all incoming requests, using a custom
89 /// comparison function.
90 pub fn same_across_all_reqs_backing_var<V: VarEqBacking>(
91 req_name: &str,
92 var: &mut Option<V>,
93 new: V,
94 ) -> anyhow::Result<()> {
95 match (var.as_ref(), new) {
96 (None, v) => *var = Some(v),
97 (Some(old), new) => {
98 if !old.eq(&new) {
99 anyhow::bail!("`{}` must be consistent across requests", req_name);
100 }
101 }
102 }
103
104 Ok(())
105 }
106
107 /// Helper method to handle Linux distros that are supported only on one
108 /// host architecture.
109 /// match_arch!(var, arch, result)
110 #[macro_export]
111 macro_rules! match_arch {
112 ($host_arch:expr, $match_arch:pat, $expr:expr) => {
113 if matches!($host_arch, $match_arch) {
114 $expr
115 } else {
116 anyhow::bail!("Linux distro not supported on host arch {}", $host_arch);
117 }
118 };
119 }
120}
121
122/// Check if `ReadVar` / `WriteVar` instances are backed by the same underlying
123/// flowey Var.
124///
125/// # Why not use `Eq`? Why have a whole separate trait?
126///
127/// `ReadVar` and `WriteVar` are, in some sense, flowey's analog to
128/// "pointers", insofar as these types primary purpose is to mediate access to
129/// some contained value, as opposed to being "values" themselves.
130///
131/// Assuming you agree with this analogy, then we can apply the same logic to
132/// `ReadVar` and `WriteVar` as Rust does to `Box<T>` wrt. what the `Eq`
133/// implementation should mean.
134///
135/// Namely: `Eq` should check the equality of the _contained objects_, as
136/// opposed to the pointers themselves.
137///
138/// Unfortunately, unlike `Box<T>`, it is _impossible_ to have an `Eq` impl for
139/// `ReadVar` / `WriteVar` that checks contents for equality, due to the fact
140/// that these types exist at flow resolution time, whereas the values they
141/// contain only exist at flow runtime.
142///
143/// As such, we have a separate trait to perform different kinds of equality
144/// checks on Vars.
145pub trait VarEqBacking {
146 /// Check if `self` is backed by the same variable as `other`.
147 fn eq(&self, other: &Self) -> bool;
148}
149
150impl<T> VarEqBacking for WriteVar<T>
151where
152 T: Serialize + DeserializeOwned,
153{
154 fn eq(&self, other: &Self) -> bool {
155 self.backing_var == other.backing_var
156 }
157}
158
159impl<T> VarEqBacking for ReadVar<T>
160where
161 T: Serialize + DeserializeOwned + PartialEq + Eq + Clone,
162{
163 fn eq(&self, other: &Self) -> bool {
164 self.backing_var == other.backing_var
165 }
166}
167
168// TODO: this should be generic across all tuple sizes
169impl<T, U> VarEqBacking for (T, U)
170where
171 T: VarEqBacking,
172 U: VarEqBacking,
173{
174 fn eq(&self, other: &Self) -> bool {
175 (self.0.eq(&other.0)) && (self.1.eq(&other.1))
176 }
177}
178
179/// Type corresponding to a step which performs a side-effect,
180/// without returning a specific value.
181///
182/// e.g: A step responsible for installing a package from `apt` might claim a
183/// `WriteVar<SideEffect>`, with any step requiring the package to have been
184/// installed prior being able to claim the corresponding `ReadVar<SideEffect>.`
185pub type SideEffect = ();
186
187/// Uninhabited type used to denote that a particular [`WriteVar`] / [`ReadVar`]
188/// is not currently claimed by any step, and cannot be directly accessed.
189#[derive(Clone, Debug, Serialize, Deserialize)]
190pub enum VarNotClaimed {}
191
192/// Uninhabited type used to denote that a particular [`WriteVar`] / [`ReadVar`]
193/// is currently claimed by a step, and can be read/written to.
194#[derive(Clone, Debug, Serialize, Deserialize)]
195pub enum VarClaimed {}
196
197/// Write a value into a flowey Var at runtime, which can then be read via a
198/// corresponding [`ReadVar`].
199///
200/// Vars in flowey must be serde de/serializable, in order to be de/serialized
201/// between multiple steps/nodes.
202///
203/// In order to write a value into a `WriteVar`, it must first be _claimed_ by a
204/// particular step (using the [`ClaimVar::claim`] API). Once claimed, the Var
205/// can be written to using APIs such as [`RustRuntimeServices::write`], or
206/// [`AdoStepServices::set_var`]
207///
208/// Note that it is only possible to write a value into a `WriteVar` _once_.
209/// Once the value has been written, the `WriteVar` type is immediately
210/// consumed, making it impossible to overwrite the stored value at some later
211/// point in execution.
212///
213/// This "write-once" property is foundational to flowey's execution model, as
214/// by recoding what step wrote to a Var, and what step(s) read from the Var, it
215/// is possible to infer what order steps must be run in.
216#[derive(Debug, Serialize, Deserialize)]
217pub struct WriteVar<T: Serialize + DeserializeOwned, C = VarNotClaimed> {
218 backing_var: String,
219 /// If true, then readers on this var expect to read a side effect (`()`)
220 /// and not `T`.
221 is_side_effect: bool,
222
223 #[serde(skip)]
224 _kind: core::marker::PhantomData<(T, C)>,
225}
226
227/// A [`WriteVar`] which has been claimed by a particular step, allowing it
228/// to be written to at runtime.
229pub type ClaimedWriteVar<T> = WriteVar<T, VarClaimed>;
230
231impl<T: Serialize + DeserializeOwned> WriteVar<T, VarNotClaimed> {
232 /// (Internal API) Switch the claim marker to "claimed".
233 fn into_claimed(self) -> WriteVar<T, VarClaimed> {
234 let Self {
235 backing_var,
236 is_side_effect,
237 _kind,
238 } = self;
239
240 WriteVar {
241 backing_var,
242 is_side_effect,
243 _kind: std::marker::PhantomData,
244 }
245 }
246
247 /// Write a static value into the Var.
248 #[track_caller]
249 pub fn write_static(self, ctx: &mut NodeCtx<'_>, val: T)
250 where
251 T: 'static,
252 {
253 let val = ReadVar::from_static(val);
254 val.write_into(ctx, self, |v| v);
255 }
256
257 pub(crate) fn into_json(self) -> WriteVar<serde_json::Value> {
258 WriteVar {
259 backing_var: self.backing_var,
260 is_side_effect: self.is_side_effect,
261 _kind: std::marker::PhantomData,
262 }
263 }
264}
265
266impl WriteVar<SideEffect, VarNotClaimed> {
267 /// Transforms this writer into one that can be used to write a `T`.
268 ///
269 /// This is useful when a reader only cares about the side effect of an
270 /// operation, but the writer wants to provide output as well.
271 pub fn discard_result<T: Serialize + DeserializeOwned>(self) -> WriteVar<T> {
272 WriteVar {
273 backing_var: self.backing_var,
274 is_side_effect: true,
275 _kind: std::marker::PhantomData,
276 }
277 }
278}
279
280/// Claim one or more flowey Vars for a particular step.
281///
282/// By having this be a trait, it is possible to `claim` both single instances
283/// of `ReadVar` / `WriteVar`, as well as whole _collections_ of Vars.
284//
285// FUTURE: flowey should include a derive macro for easily claiming read/write
286// vars in user-defined structs / enums.
287pub trait ClaimVar {
288 /// The claimed version of Self.
289 type Claimed;
290 /// Claim the Var for this step, allowing it to be accessed at runtime.
291 fn claim(self, ctx: &mut StepCtx<'_>) -> Self::Claimed;
292}
293
294/// Read the value of one or more flowey Vars.
295///
296/// By having this be a trait, it is possible to `read` both single
297/// instances of `ReadVar` / `WriteVar`, as well as whole _collections_ of
298/// Vars.
299pub trait ReadVarValue {
300 /// The read value of Self.
301 type Value;
302 /// Read the value of the Var at runtime.
303 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value;
304}
305
306impl<T: Serialize + DeserializeOwned> ClaimVar for ReadVar<T> {
307 type Claimed = ClaimedReadVar<T>;
308
309 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
310 if let ReadVarBacking::RuntimeVar {
311 var,
312 is_side_effect: _,
313 } = &self.backing_var
314 {
315 ctx.backend.borrow_mut().on_claimed_runtime_var(var, true);
316 }
317 self.into_claimed()
318 }
319}
320
321impl<T: Serialize + DeserializeOwned> ClaimVar for WriteVar<T> {
322 type Claimed = ClaimedWriteVar<T>;
323
324 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedWriteVar<T> {
325 ctx.backend
326 .borrow_mut()
327 .on_claimed_runtime_var(&self.backing_var, false);
328 self.into_claimed()
329 }
330}
331
332impl<T: Serialize + DeserializeOwned> ReadVarValue for ClaimedReadVar<T> {
333 type Value = T;
334
335 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
336 match self.backing_var {
337 ReadVarBacking::RuntimeVar {
338 var,
339 is_side_effect,
340 } => {
341 // Always get the data to validate that the variable is actually there.
342 let data = rt.get_var(&var, is_side_effect);
343 if is_side_effect {
344 // This was converted into a `ReadVar<SideEffect>` from
345 // another type, so parse the value that a
346 // `WriteVar<SideEffect>` would have written.
347 serde_json::from_slice(b"null").expect("should be deserializing into ()")
348 } else {
349 // This is a normal variable.
350 serde_json::from_slice(&data).expect("improve this error path")
351 }
352 }
353 ReadVarBacking::Inline(val) => val,
354 }
355 }
356}
357
358impl<T: ClaimVar> ClaimVar for Vec<T> {
359 type Claimed = Vec<T::Claimed>;
360
361 fn claim(self, ctx: &mut StepCtx<'_>) -> Vec<T::Claimed> {
362 self.into_iter().map(|v| v.claim(ctx)).collect()
363 }
364}
365
366impl<T: ReadVarValue> ReadVarValue for Vec<T> {
367 type Value = Vec<T::Value>;
368
369 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
370 self.into_iter().map(|v| v.read_value(rt)).collect()
371 }
372}
373
374impl<T: ClaimVar> ClaimVar for Option<T> {
375 type Claimed = Option<T::Claimed>;
376
377 fn claim(self, ctx: &mut StepCtx<'_>) -> Option<T::Claimed> {
378 self.map(|x| x.claim(ctx))
379 }
380}
381
382impl<T: ReadVarValue> ReadVarValue for Option<T> {
383 type Value = Option<T::Value>;
384
385 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
386 self.map(|x| x.read_value(rt))
387 }
388}
389
390impl<U: Ord, T: ClaimVar> ClaimVar for BTreeMap<U, T> {
391 type Claimed = BTreeMap<U, T::Claimed>;
392
393 fn claim(self, ctx: &mut StepCtx<'_>) -> BTreeMap<U, T::Claimed> {
394 self.into_iter().map(|(k, v)| (k, v.claim(ctx))).collect()
395 }
396}
397
398impl<U: Ord, T: ReadVarValue> ReadVarValue for BTreeMap<U, T> {
399 type Value = BTreeMap<U, T::Value>;
400
401 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
402 self.into_iter()
403 .map(|(k, v)| (k, v.read_value(rt)))
404 .collect()
405 }
406}
407
408macro_rules! impl_tuple_claim {
409 ($($T:tt)*) => {
410 impl<$($T,)*> $crate::node::ClaimVar for ($($T,)*)
411 where
412 $($T: $crate::node::ClaimVar,)*
413 {
414 type Claimed = ($($T::Claimed,)*);
415
416 #[expect(non_snake_case)]
417 fn claim(self, ctx: &mut $crate::node::StepCtx<'_>) -> Self::Claimed {
418 let ($($T,)*) = self;
419 ($($T.claim(ctx),)*)
420 }
421 }
422
423 impl<$($T,)*> $crate::node::ReadVarValue for ($($T,)*)
424 where
425 $($T: $crate::node::ReadVarValue,)*
426 {
427 type Value = ($($T::Value,)*);
428
429 #[expect(non_snake_case)]
430 fn read_value(self, rt: &mut $crate::node::RustRuntimeServices<'_>) -> Self::Value {
431 let ($($T,)*) = self;
432 ($($T.read_value(rt),)*)
433 }
434 }
435 };
436}
437
438impl_tuple_claim!(A B C D E F G H I J);
439impl_tuple_claim!(A B C D E F G H I);
440impl_tuple_claim!(A B C D E F G H);
441impl_tuple_claim!(A B C D E F G);
442impl_tuple_claim!(A B C D E F);
443impl_tuple_claim!(A B C D E);
444impl_tuple_claim!(A B C D);
445impl_tuple_claim!(A B C);
446impl_tuple_claim!(A B);
447impl_tuple_claim!(A);
448
449impl ClaimVar for () {
450 type Claimed = ();
451
452 fn claim(self, _ctx: &mut StepCtx<'_>) -> Self::Claimed {}
453}
454
455impl ReadVarValue for () {
456 type Value = ();
457
458 fn read_value(self, _rt: &mut RustRuntimeServices<'_>) -> Self::Value {}
459}
460
461/// Read a custom, user-defined secret by passing in the secret name.
462///
463/// Intended usage is to get a secret using the [`crate::pipeline::Pipeline::gh_use_secret`] API
464/// and to use the returned value through the [`NodeCtx::get_gh_context_var`] API.
465#[derive(Serialize, Deserialize, Clone)]
466pub struct GhUserSecretVar(pub(crate) String);
467
468/// Read a value from a flowey Var at runtime, returning the value written by
469/// the Var's corresponding [`WriteVar`].
470///
471/// Vars in flowey must be serde de/serializable, in order to be de/serialized
472/// between multiple steps/nodes.
473///
474/// In order to read the value contained within a `ReadVar`, it must first be
475/// _claimed_ by a particular step (using the [`ClaimVar::claim`] API). Once
476/// claimed, the Var can be read using APIs such as
477/// [`RustRuntimeServices::read`], or [`AdoStepServices::get_var`]
478///
479/// Note that all `ReadVar`s in flowey are _immutable_. In other words:
480/// reading the value of a `ReadVar` multiple times from multiple nodes will
481/// _always_ return the same value.
482///
483/// This is a natural consequence `ReadVar` obtaining its value from the result
484/// of a write into [`WriteVar`], whose API enforces that there can only ever be
485/// a single Write to a `WriteVar`.
486#[derive(Debug, Serialize, Deserialize)]
487pub struct ReadVar<T, C = VarNotClaimed> {
488 backing_var: ReadVarBacking<T>,
489 #[serde(skip)]
490 _kind: std::marker::PhantomData<C>,
491}
492
493/// A [`ReadVar`] which has been claimed by a particular step, allowing it to
494/// be read at runtime.
495pub type ClaimedReadVar<T> = ReadVar<T, VarClaimed>;
496
497// cloning is fine, since you can totally have multiple dependents
498impl<T: Serialize + DeserializeOwned, C> Clone for ReadVar<T, C> {
499 fn clone(&self) -> Self {
500 ReadVar {
501 backing_var: self.backing_var.clone(),
502 _kind: std::marker::PhantomData,
503 }
504 }
505}
506
507#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
508enum ReadVarBacking<T> {
509 RuntimeVar {
510 var: String,
511 /// If true, then don't try to parse this variable--it was converted
512 /// into a side effect (of type `()`) from another type, so the
513 /// serialization will not match.
514 ///
515 /// If false, it may still be a "side effect" variable, but type `T`
516 /// matches its serialization.
517 is_side_effect: bool,
518 },
519 Inline(T),
520}
521
522// avoid requiring types to include an explicit clone bound
523impl<T: Serialize + DeserializeOwned> Clone for ReadVarBacking<T> {
524 fn clone(&self) -> Self {
525 match self {
526 Self::RuntimeVar {
527 var,
528 is_side_effect,
529 } => Self::RuntimeVar {
530 var: var.clone(),
531 is_side_effect: *is_side_effect,
532 },
533 Self::Inline(v) => {
534 Self::Inline(serde_json::from_value(serde_json::to_value(v).unwrap()).unwrap())
535 }
536 }
537 }
538}
539
540impl<T: Serialize + DeserializeOwned> ReadVar<T> {
541 /// (Internal API) Switch the claim marker to "claimed".
542 fn into_claimed(self) -> ReadVar<T, VarClaimed> {
543 let Self { backing_var, _kind } = self;
544
545 ReadVar {
546 backing_var,
547 _kind: std::marker::PhantomData,
548 }
549 }
550
551 /// Discard any type information associated with the Var, and treat the Var
552 /// as through it was only a side effect.
553 ///
554 /// e.g: if a Node returns a `ReadVar<PathBuf>`, but you know that the mere
555 /// act of having _run_ the node has ensured the file is placed in a "magic
556 /// location" for some other node, then it may be useful to treat the
557 /// `ReadVar<PathBuf>` as a simple `ReadVar<SideEffect>`, which can be
558 /// passed along as part of a larger bundle of `Vec<ReadVar<SideEffect>>`.
559 #[must_use]
560 pub fn into_side_effect(self) -> ReadVar<SideEffect> {
561 ReadVar {
562 backing_var: match self.backing_var {
563 ReadVarBacking::RuntimeVar {
564 var,
565 is_side_effect: _,
566 } => ReadVarBacking::RuntimeVar {
567 var,
568 is_side_effect: true,
569 },
570 ReadVarBacking::Inline(_) => ReadVarBacking::Inline(()),
571 },
572 _kind: std::marker::PhantomData,
573 }
574 }
575
576 /// Maps a `ReadVar<T>` to a new `ReadVar<U>`, by applying a function to the
577 /// Var at runtime.
578 #[track_caller]
579 #[must_use]
580 pub fn map<F, U>(&self, ctx: &mut NodeCtx<'_>, f: F) -> ReadVar<U>
581 where
582 T: 'static,
583 U: Serialize + DeserializeOwned + 'static,
584 F: FnOnce(T) -> U + 'static,
585 {
586 let (read_from, write_into) = ctx.new_var();
587 self.write_into(ctx, write_into, f);
588 read_from
589 }
590
591 /// Maps a `ReadVar<T>` into an existing `WriteVar<U>` by applying a
592 /// function to the Var at runtime.
593 #[track_caller]
594 pub fn write_into<F, U>(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<U>, f: F)
595 where
596 T: 'static,
597 U: Serialize + DeserializeOwned + 'static,
598 F: FnOnce(T) -> U + 'static,
599 {
600 let this = self.clone();
601 ctx.emit_minor_rust_step("🌼 write_into Var", move |ctx| {
602 let this = this.claim(ctx);
603 let write_into = write_into.claim(ctx);
604 move |rt| {
605 let this = rt.read(this);
606 rt.write(write_into, &f(this));
607 }
608 });
609 }
610
611 /// Zips self (`ReadVar<T>`) with another `ReadVar<U>`, returning a new
612 /// `ReadVar<(T, U)>`
613 #[track_caller]
614 #[must_use]
615 pub fn zip<U>(&self, ctx: &mut NodeCtx<'_>, other: ReadVar<U>) -> ReadVar<(T, U)>
616 where
617 T: 'static,
618 U: Serialize + DeserializeOwned + 'static,
619 {
620 let (read_from, write_into) = ctx.new_var();
621 let this = self.clone();
622 ctx.emit_minor_rust_step("🌼 Zip Vars", move |ctx| {
623 let this = this.claim(ctx);
624 let other = other.claim(ctx);
625 let write_into = write_into.claim(ctx);
626 move |rt| {
627 let this = rt.read(this);
628 let other = rt.read(other);
629 rt.write(write_into, &(this, other));
630 }
631 });
632 read_from
633 }
634
635 /// Create a new `ReadVar` from a static value.
636 ///
637 /// **WARNING:** Static values **CANNOT BE SECRETS**, as they are encoded as
638 /// plain-text in the output flow.
639 #[track_caller]
640 #[must_use]
641 pub fn from_static(val: T) -> ReadVar<T>
642 where
643 T: 'static,
644 {
645 ReadVar {
646 backing_var: ReadVarBacking::Inline(val),
647 _kind: std::marker::PhantomData,
648 }
649 }
650
651 /// If this [`ReadVar`] contains a static value, return it.
652 ///
653 /// Nodes can opt-in to using this method as a way to generate optimized
654 /// steps in cases where the value of a variable is known ahead of time.
655 ///
656 /// e.g: a node doing a git checkout could leverage this method to decide
657 /// whether its ADO backend should emit a conditional step for checking out
658 /// a repo, or if it can statically include / exclude the checkout request.
659 pub fn get_static(&self) -> Option<T> {
660 match self.clone().backing_var {
661 ReadVarBacking::Inline(v) => Some(v),
662 _ => None,
663 }
664 }
665
666 /// Transpose a `Vec<ReadVar<T>>` into a `ReadVar<Vec<T>>`
667 #[track_caller]
668 #[must_use]
669 pub fn transpose_vec(ctx: &mut NodeCtx<'_>, vec: Vec<ReadVar<T>>) -> ReadVar<Vec<T>>
670 where
671 T: 'static,
672 {
673 let (read_from, write_into) = ctx.new_var();
674 ctx.emit_minor_rust_step("🌼 Transpose Vec<ReadVar<T>>", move |ctx| {
675 let vec = vec.claim(ctx);
676 let write_into = write_into.claim(ctx);
677 move |rt| {
678 let mut v = Vec::new();
679 for var in vec {
680 v.push(rt.read(var));
681 }
682 rt.write(write_into, &v);
683 }
684 });
685 read_from
686 }
687
688 /// Returns a new instance of this variable with an artificial dependency on
689 /// `other`.
690 ///
691 /// This is useful for making explicit a non-explicit dependency between the
692 /// two variables. For example, if `self` contains a path to a file, and
693 /// `other` is only written once that file has been created, then this
694 /// method can be used to return a new `ReadVar` which depends on `other`
695 /// but is otherwise identical to `self`. This ensures that when the new
696 /// variable is read, the file has been created.
697 ///
698 /// In general, it is better to ensure that the dependency is explicit, so
699 /// that if you have a variable with a path, then you know that the file
700 /// exists when you read it. This method is useful in cases where this is
701 /// not naturally the case, e.g., when you are providing a path as part of a
702 /// request, as opposed to the path being returned to you.
703 #[must_use]
704 pub fn depending_on<U>(&self, ctx: &mut NodeCtx<'_>, other: &ReadVar<U>) -> Self
705 where
706 T: 'static,
707 U: Serialize + DeserializeOwned + 'static,
708 {
709 // This could probably be handled without an additional Rust step with some
710 // additional work in the backend, but this is simple enough for now.
711 ctx.emit_minor_rust_stepv("🌼 Add dependency", |ctx| {
712 let this = self.clone().claim(ctx);
713 other.clone().claim(ctx);
714 move |rt| rt.read(this)
715 })
716 }
717
718 /// Consume this `ReadVar` outside the context of a step, signalling that it
719 /// won't be used.
720 pub fn claim_unused(self, ctx: &mut NodeCtx<'_>) {
721 match self.backing_var {
722 ReadVarBacking::RuntimeVar {
723 var,
724 is_side_effect: _,
725 } => ctx.backend.borrow_mut().on_unused_read_var(&var),
726 ReadVarBacking::Inline(_) => {}
727 }
728 }
729
730 pub(crate) fn into_json(self) -> ReadVar<serde_json::Value> {
731 match self.backing_var {
732 ReadVarBacking::RuntimeVar {
733 var,
734 is_side_effect,
735 } => ReadVar {
736 backing_var: ReadVarBacking::RuntimeVar {
737 var,
738 is_side_effect,
739 },
740 _kind: std::marker::PhantomData,
741 },
742 ReadVarBacking::Inline(v) => ReadVar {
743 backing_var: ReadVarBacking::Inline(serde_json::to_value(v).unwrap()),
744 _kind: std::marker::PhantomData,
745 },
746 }
747 }
748}
749
750/// DANGER: obtain a handle to a [`ReadVar`] "out of thin air".
751///
752/// This should NEVER be used from within a flowey node. This is a sharp tool,
753/// and should only be used by code implementing flow / pipeline resolution
754/// logic.
755#[must_use]
756pub fn thin_air_read_runtime_var<T>(backing_var: String) -> ReadVar<T>
757where
758 T: Serialize + DeserializeOwned,
759{
760 ReadVar {
761 backing_var: ReadVarBacking::RuntimeVar {
762 var: backing_var,
763 is_side_effect: false,
764 },
765 _kind: std::marker::PhantomData,
766 }
767}
768
769/// DANGER: obtain a handle to a [`WriteVar`] "out of thin air".
770///
771/// This should NEVER be used from within a flowey node. This is a sharp tool,
772/// and should only be used by code implementing flow / pipeline resolution
773/// logic.
774#[must_use]
775pub fn thin_air_write_runtime_var<T>(backing_var: String) -> WriteVar<T>
776where
777 T: Serialize + DeserializeOwned,
778{
779 WriteVar {
780 backing_var,
781 is_side_effect: false,
782 _kind: std::marker::PhantomData,
783 }
784}
785
786/// DANGER: obtain a [`ReadVar`] backing variable and side effect status.
787///
788/// This should NEVER be used from within a flowey node. This relies on
789/// flowey variable implementation details, and should only be used by code
790/// implementing flow / pipeline resolution logic.
791pub fn read_var_internals<T: Serialize + DeserializeOwned, C>(
792 var: &ReadVar<T, C>,
793) -> (Option<String>, bool) {
794 match var.backing_var {
795 ReadVarBacking::RuntimeVar {
796 var: ref s,
797 is_side_effect,
798 } => (Some(s.clone()), is_side_effect),
799 ReadVarBacking::Inline(_) => (None, false),
800 }
801}
802
803pub trait ImportCtxBackend {
804 fn on_possible_dep(&mut self, node_handle: NodeHandle);
805}
806
807/// Context passed to [`FlowNode::imports`].
808pub struct ImportCtx<'a> {
809 backend: &'a mut dyn ImportCtxBackend,
810}
811
812impl ImportCtx<'_> {
813 /// Declare that a Node can be referenced in [`FlowNode::emit`]
814 pub fn import<N: FlowNodeBase + 'static>(&mut self) {
815 self.backend.on_possible_dep(NodeHandle::from_type::<N>())
816 }
817}
818
819pub fn new_import_ctx(backend: &mut dyn ImportCtxBackend) -> ImportCtx<'_> {
820 ImportCtx { backend }
821}
822
823#[derive(Debug)]
824pub enum CtxAnchor {
825 PostJob,
826}
827
828pub trait NodeCtxBackend {
829 /// Handle to the current node this `ctx` corresponds to
830 fn current_node(&self) -> NodeHandle;
831
832 /// Return a string which uniquely identifies this particular Var
833 /// registration.
834 ///
835 /// Typically consists of `{current node handle}{ordinal}`
836 fn on_new_var(&mut self) -> String;
837
838 /// Invoked when a node claims a particular runtime variable
839 fn on_claimed_runtime_var(&mut self, var: &str, is_read: bool);
840
841 /// Invoked when a node marks a particular runtime variable as unused
842 fn on_unused_read_var(&mut self, var: &str);
843
844 /// Invoked when a node sets a request on a node.
845 ///
846 /// - `node_typeid` will always correspond to a node that was previously
847 /// passed to `on_register`.
848 /// - `req` may be an error, in the case where the NodeCtx failed to
849 /// serialize the provided request.
850 // FIXME: this should be using type-erased serde
851 fn on_request(&mut self, node_handle: NodeHandle, req: anyhow::Result<Box<[u8]>>);
852
853 fn on_emit_rust_step(
854 &mut self,
855 label: &str,
856 can_merge: bool,
857 code: Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
858 );
859
860 fn on_emit_ado_step(
861 &mut self,
862 label: &str,
863 yaml_snippet: Box<dyn for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String>,
864 inline_script: Option<
865 Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
866 >,
867 condvar: Option<String>,
868 );
869
870 fn on_emit_gh_step(
871 &mut self,
872 label: &str,
873 uses: &str,
874 with: BTreeMap<String, ClaimedGhParam>,
875 condvar: Option<String>,
876 outputs: BTreeMap<String, Vec<GhOutput>>,
877 permissions: BTreeMap<GhPermission, GhPermissionValue>,
878 gh_to_rust: Vec<GhToRust>,
879 rust_to_gh: Vec<RustToGh>,
880 );
881
882 fn on_emit_side_effect_step(&mut self);
883
884 fn backend(&mut self) -> FlowBackend;
885 fn platform(&mut self) -> FlowPlatform;
886 fn arch(&mut self) -> FlowArch;
887
888 /// Return a node-specific persistent store path. The backend does not need
889 /// to ensure that the path exists - flowey will automatically emit a step
890 /// to construct the directory at runtime.
891 fn persistent_dir_path_var(&mut self) -> Option<String>;
892}
893
894pub fn new_node_ctx(backend: &mut dyn NodeCtxBackend) -> NodeCtx<'_> {
895 NodeCtx {
896 backend: Rc::new(RefCell::new(backend)),
897 }
898}
899
900/// What backend the flow is being running on.
901#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
902pub enum FlowBackend {
903 /// Running locally.
904 Local,
905 /// Running on ADO.
906 Ado,
907 /// Running on GitHub Actions
908 Github,
909}
910
911/// The kind platform the flow is being running on, Windows or Unix.
912#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
913pub enum FlowPlatformKind {
914 Windows,
915 Unix,
916}
917
918/// The kind platform the flow is being running on, Windows or Unix.
919#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
920pub enum FlowPlatformLinuxDistro {
921 /// Fedora (including WSL2)
922 Fedora,
923 /// Ubuntu (including WSL2)
924 Ubuntu,
925 /// Arch Linux (including WSL2)
926 Arch,
927 /// An unknown distribution
928 Unknown,
929}
930
931/// What platform the flow is being running on.
932#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
933#[non_exhaustive]
934pub enum FlowPlatform {
935 /// Windows
936 Windows,
937 /// Linux (including WSL2)
938 Linux(FlowPlatformLinuxDistro),
939 /// macOS
940 MacOs,
941}
942
943impl FlowPlatform {
944 pub fn kind(&self) -> FlowPlatformKind {
945 match self {
946 Self::Windows => FlowPlatformKind::Windows,
947 Self::Linux(_) | Self::MacOs => FlowPlatformKind::Unix,
948 }
949 }
950
951 fn as_str(&self) -> &'static str {
952 match self {
953 Self::Windows => "windows",
954 Self::Linux(_) => "linux",
955 Self::MacOs => "macos",
956 }
957 }
958
959 /// The suffix to use for executables on this platform.
960 pub fn exe_suffix(&self) -> &'static str {
961 if self == &Self::Windows { ".exe" } else { "" }
962 }
963
964 /// The full name for a binary on this platform (i.e. `name + self.exe_suffix()`).
965 pub fn binary(&self, name: &str) -> String {
966 format!("{}{}", name, self.exe_suffix())
967 }
968}
969
970impl std::fmt::Display for FlowPlatform {
971 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
972 f.pad(self.as_str())
973 }
974}
975
976/// What architecture the flow is being running on.
977#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
978#[non_exhaustive]
979pub enum FlowArch {
980 X86_64,
981 Aarch64,
982}
983
984impl FlowArch {
985 fn as_str(&self) -> &'static str {
986 match self {
987 Self::X86_64 => "x86_64",
988 Self::Aarch64 => "aarch64",
989 }
990 }
991}
992
993impl std::fmt::Display for FlowArch {
994 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
995 f.pad(self.as_str())
996 }
997}
998
999/// Context object for an individual step.
1000pub struct StepCtx<'a> {
1001 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1002}
1003
1004impl StepCtx<'_> {
1005 /// What backend the flow is being running on (e.g: locally, ADO, GitHub,
1006 /// etc...)
1007 pub fn backend(&self) -> FlowBackend {
1008 self.backend.borrow_mut().backend()
1009 }
1010
1011 /// What platform the flow is being running on (e.g: windows, linux, wsl2,
1012 /// etc...).
1013 pub fn platform(&self) -> FlowPlatform {
1014 self.backend.borrow_mut().platform()
1015 }
1016}
1017
1018const NO_ADO_INLINE_SCRIPT: Option<
1019 for<'a> fn(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>,
1020> = None;
1021
1022/// Context object for a `FlowNode`.
1023pub struct NodeCtx<'a> {
1024 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1025}
1026
1027impl<'ctx> NodeCtx<'ctx> {
1028 /// Emit a Rust-based step.
1029 ///
1030 /// As a convenience feature, this function returns a special _optional_
1031 /// [`ReadVar<SideEffect>`], which will not result in a "unused variable"
1032 /// error if no subsequent step ends up claiming it.
1033 pub fn emit_rust_step<F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<SideEffect>
1034 where
1035 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1036 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1037 {
1038 self.emit_rust_step_inner(label.as_ref(), false, code)
1039 }
1040
1041 /// Emit a Rust-based step that cannot fail.
1042 ///
1043 /// This is equivalent to `emit_rust_step`, but it is for steps that cannot
1044 /// fail and that do not need to be emitted as a separate step in a YAML
1045 /// pipeline. This simplifies the pipeline logs.
1046 ///
1047 /// As a convenience feature, this function returns a special _optional_
1048 /// [`ReadVar<SideEffect>`], which will not result in a "unused variable"
1049 /// error if no subsequent step ends up claiming it.
1050 pub fn emit_minor_rust_step<F, G>(
1051 &mut self,
1052 label: impl AsRef<str>,
1053 code: F,
1054 ) -> ReadVar<SideEffect>
1055 where
1056 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1057 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) + 'static,
1058 {
1059 self.emit_rust_step_inner(label.as_ref(), true, |ctx| {
1060 let f = code(ctx);
1061 |rt| {
1062 f(rt);
1063 Ok(())
1064 }
1065 })
1066 }
1067
1068 /// Emit a Rust-based step, creating a new `ReadVar<T>` from the step's
1069 /// return value.
1070 ///
1071 /// This is a convenience function that streamlines the following common
1072 /// flowey pattern:
1073 ///
1074 /// ```ignore
1075 /// // creating a new Var explicitly
1076 /// let (read_foo, write_foo) = ctx.new_var();
1077 /// ctx.emit_rust_step("foo", |ctx| {
1078 /// let write_foo = write_foo.claim(ctx);
1079 /// |rt| {
1080 /// rt.write(write_foo, &get_foo());
1081 /// Ok(())
1082 /// }
1083 /// });
1084 ///
1085 /// // creating a new Var automatically
1086 /// let read_foo = ctx.emit_rust_stepv("foo", |ctx| |rt| Ok(get_foo()));
1087 /// ```
1088 #[must_use]
1089 #[track_caller]
1090 pub fn emit_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1091 where
1092 T: Serialize + DeserializeOwned + 'static,
1093 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1094 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1095 {
1096 self.emit_rust_stepv_inner(label.as_ref(), false, code)
1097 }
1098
1099 /// Emit a Rust-based step, creating a new `ReadVar<T>` from the step's
1100 /// return value.
1101 ///
1102 /// This is equivalent to `emit_rust_stepv`, but it is for steps that cannot
1103 /// fail and that do not need to be emitted as a separate step in a YAML
1104 /// pipeline. This simplifies the pipeline logs.
1105 ///
1106 /// This is a convenience function that streamlines the following common
1107 /// flowey pattern:
1108 ///
1109 /// ```ignore
1110 /// // creating a new Var explicitly
1111 /// let (read_foo, write_foo) = ctx.new_var();
1112 /// ctx.emit_minor_rust_step("foo", |ctx| {
1113 /// let write_foo = write_foo.claim(ctx);
1114 /// |rt| {
1115 /// rt.write(write_foo, &get_foo());
1116 /// }
1117 /// });
1118 ///
1119 /// // creating a new Var automatically
1120 /// let read_foo = ctx.emit_minor_rust_stepv("foo", |ctx| |rt| get_foo());
1121 /// ```
1122 #[must_use]
1123 #[track_caller]
1124 pub fn emit_minor_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1125 where
1126 T: Serialize + DeserializeOwned + 'static,
1127 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1128 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> T + 'static,
1129 {
1130 self.emit_rust_stepv_inner(label.as_ref(), true, |ctx| {
1131 let f = code(ctx);
1132 |rt| Ok(f(rt))
1133 })
1134 }
1135
1136 fn emit_rust_step_inner<F, G>(
1137 &mut self,
1138 label: &str,
1139 can_merge: bool,
1140 code: F,
1141 ) -> ReadVar<SideEffect>
1142 where
1143 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1144 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1145 {
1146 let (read, write) = self.new_prefixed_var("auto_se");
1147
1148 let ctx = &mut StepCtx {
1149 backend: self.backend.clone(),
1150 };
1151 write.claim(ctx);
1152
1153 let code = code(ctx);
1154 self.backend
1155 .borrow_mut()
1156 .on_emit_rust_step(label.as_ref(), can_merge, Box::new(code));
1157 read
1158 }
1159
1160 #[must_use]
1161 #[track_caller]
1162 fn emit_rust_stepv_inner<T, F, G>(
1163 &mut self,
1164 label: impl AsRef<str>,
1165 can_merge: bool,
1166 code: F,
1167 ) -> ReadVar<T>
1168 where
1169 T: Serialize + DeserializeOwned + 'static,
1170 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1171 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1172 {
1173 let (read, write) = self.new_var();
1174
1175 let ctx = &mut StepCtx {
1176 backend: self.backend.clone(),
1177 };
1178 let write = write.claim(ctx);
1179
1180 let code = code(ctx);
1181 self.backend.borrow_mut().on_emit_rust_step(
1182 label.as_ref(),
1183 can_merge,
1184 Box::new(|rt| {
1185 let val = code(rt)?;
1186 rt.write(write, &val);
1187 Ok(())
1188 }),
1189 );
1190 read
1191 }
1192
1193 /// Load an ADO global runtime variable into a flowey [`ReadVar`].
1194 #[track_caller]
1195 #[must_use]
1196 pub fn get_ado_variable(&mut self, ado_var: AdoRuntimeVar) -> ReadVar<String> {
1197 let (var, write_var) = self.new_var();
1198 self.emit_ado_step(format!("🌼 read {}", ado_var.as_raw_var_name()), |ctx| {
1199 let write_var = write_var.claim(ctx);
1200 |rt| {
1201 rt.set_var(write_var, ado_var);
1202 "".into()
1203 }
1204 });
1205 var
1206 }
1207
1208 /// Emit an ADO step.
1209 pub fn emit_ado_step<F, G>(&mut self, display_name: impl AsRef<str>, yaml_snippet: F)
1210 where
1211 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1212 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1213 {
1214 self.emit_ado_step_inner(display_name, None, |ctx| {
1215 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1216 })
1217 }
1218
1219 /// Emit an ADO step, conditionally executed based on the value of `cond` at
1220 /// runtime.
1221 pub fn emit_ado_step_with_condition<F, G>(
1222 &mut self,
1223 display_name: impl AsRef<str>,
1224 cond: ReadVar<bool>,
1225 yaml_snippet: F,
1226 ) where
1227 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1228 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1229 {
1230 self.emit_ado_step_inner(display_name, Some(cond), |ctx| {
1231 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1232 })
1233 }
1234
1235 /// Emit an ADO step, conditionally executed based on the value of`cond` at
1236 /// runtime.
1237 pub fn emit_ado_step_with_condition_optional<F, G>(
1238 &mut self,
1239 display_name: impl AsRef<str>,
1240 cond: Option<ReadVar<bool>>,
1241 yaml_snippet: F,
1242 ) where
1243 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1244 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1245 {
1246 self.emit_ado_step_inner(display_name, cond, |ctx| {
1247 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1248 })
1249 }
1250
1251 /// Emit an ADO step which invokes a rust callback using an inline script.
1252 ///
1253 /// By using the `{{FLOWEY_INLINE_SCRIPT}}` template in the returned yaml
1254 /// snippet, flowey will interpolate a command ~roughly akin to `flowey
1255 /// exec-snippet <rust-snippet-id>` into the generated yaml.
1256 ///
1257 /// e.g: if we wanted to _manually_ wrap the bash ADO snippet for whatever
1258 /// reason:
1259 ///
1260 /// ```text
1261 /// - bash: |
1262 /// echo "hello there!"
1263 /// {{FLOWEY_INLINE_SCRIPT}}
1264 /// echo echo "bye!"
1265 /// ```
1266 ///
1267 /// # Limitations
1268 ///
1269 /// At the moment, due to flowey API limitations, it is only possible to
1270 /// embed a single inline script into a YAML step.
1271 ///
1272 /// In the future, rather than having separate methods for "emit step with X
1273 /// inline scripts", flowey should support declaring "first-class" callbacks
1274 /// via a (hypothetical) `ctx.new_callback_var(|ctx| |rt, input: Input| ->
1275 /// Output { ... })` API, at which point.
1276 ///
1277 /// If such an API were to exist, one could simply use the "vanilla" emit
1278 /// yaml step functions with these first-class callbacks.
1279 pub fn emit_ado_step_with_inline_script<F, G, H>(
1280 &mut self,
1281 display_name: impl AsRef<str>,
1282 yaml_snippet: F,
1283 ) where
1284 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, H),
1285 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1286 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1287 {
1288 self.emit_ado_step_inner(display_name, None, |ctx| {
1289 let (f, g) = yaml_snippet(ctx);
1290 (f, Some(g))
1291 })
1292 }
1293
1294 fn emit_ado_step_inner<F, G, H>(
1295 &mut self,
1296 display_name: impl AsRef<str>,
1297 cond: Option<ReadVar<bool>>,
1298 yaml_snippet: F,
1299 ) where
1300 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, Option<H>),
1301 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1302 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1303 {
1304 let condvar = match cond.map(|c| c.backing_var) {
1305 // it seems silly to allow this... but it's not hard so why not?
1306 Some(ReadVarBacking::Inline(cond)) => {
1307 if !cond {
1308 return;
1309 } else {
1310 None
1311 }
1312 }
1313 Some(ReadVarBacking::RuntimeVar {
1314 var,
1315 is_side_effect,
1316 }) => {
1317 assert!(!is_side_effect);
1318 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1319 Some(var)
1320 }
1321 None => None,
1322 };
1323
1324 let (yaml_snippet, inline_script) = yaml_snippet(&mut StepCtx {
1325 backend: self.backend.clone(),
1326 });
1327 self.backend.borrow_mut().on_emit_ado_step(
1328 display_name.as_ref(),
1329 Box::new(yaml_snippet),
1330 if let Some(inline_script) = inline_script {
1331 Some(Box::new(inline_script))
1332 } else {
1333 None
1334 },
1335 condvar,
1336 );
1337 }
1338
1339 /// Load a GitHub context variable into a flowey [`ReadVar`].
1340 #[track_caller]
1341 #[must_use]
1342 pub fn get_gh_context_var(&mut self) -> GhContextVarReader<'ctx, Root> {
1343 GhContextVarReader {
1344 ctx: NodeCtx {
1345 backend: self.backend.clone(),
1346 },
1347 _state: std::marker::PhantomData,
1348 }
1349 }
1350
1351 /// Emit a GitHub Actions action step.
1352 pub fn emit_gh_step(
1353 &mut self,
1354 display_name: impl AsRef<str>,
1355 uses: impl AsRef<str>,
1356 ) -> GhStepBuilder {
1357 GhStepBuilder::new(display_name, uses)
1358 }
1359
1360 fn emit_gh_step_inner(
1361 &mut self,
1362 display_name: impl AsRef<str>,
1363 cond: Option<ReadVar<bool>>,
1364 uses: impl AsRef<str>,
1365 with: Option<BTreeMap<String, GhParam>>,
1366 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
1367 run_after: Vec<ReadVar<SideEffect>>,
1368 permissions: BTreeMap<GhPermission, GhPermissionValue>,
1369 ) {
1370 let condvar = match cond.map(|c| c.backing_var) {
1371 // it seems silly to allow this... but it's not hard so why not?
1372 Some(ReadVarBacking::Inline(cond)) => {
1373 if !cond {
1374 return;
1375 } else {
1376 None
1377 }
1378 }
1379 Some(ReadVarBacking::RuntimeVar {
1380 var,
1381 is_side_effect,
1382 }) => {
1383 assert!(!is_side_effect);
1384 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1385 Some(var)
1386 }
1387 None => None,
1388 };
1389
1390 let with = with
1391 .unwrap_or_default()
1392 .into_iter()
1393 .map(|(k, v)| {
1394 (
1395 k.clone(),
1396 v.claim(&mut StepCtx {
1397 backend: self.backend.clone(),
1398 }),
1399 )
1400 })
1401 .collect();
1402
1403 for var in run_after {
1404 var.claim(&mut StepCtx {
1405 backend: self.backend.clone(),
1406 });
1407 }
1408
1409 let outputvars = outputs
1410 .into_iter()
1411 .map(|(name, vars)| {
1412 (
1413 name,
1414 vars.into_iter()
1415 .map(|var| {
1416 let var = var.claim(&mut StepCtx {
1417 backend: self.backend.clone(),
1418 });
1419 GhOutput {
1420 backing_var: var.backing_var,
1421 is_secret: false,
1422 is_object: false,
1423 }
1424 })
1425 .collect(),
1426 )
1427 })
1428 .collect();
1429
1430 self.backend.borrow_mut().on_emit_gh_step(
1431 display_name.as_ref(),
1432 uses.as_ref(),
1433 with,
1434 condvar,
1435 outputvars,
1436 permissions,
1437 Vec::new(),
1438 Vec::new(),
1439 );
1440 }
1441
1442 /// Emit a "side-effect" step, which simply claims a set of side-effects in
1443 /// order to resolve another set of side effects.
1444 ///
1445 /// The same functionality could be achieved (less efficiently) by emitting
1446 /// a Rust step (or ADO step, or github step, etc...) that claims both sets
1447 /// of side-effects, and then does nothing. By using this method - flowey is
1448 /// able to avoid emitting that additional noop step at runtime.
1449 pub fn emit_side_effect_step(
1450 &mut self,
1451 use_side_effects: impl IntoIterator<Item = ReadVar<SideEffect>>,
1452 resolve_side_effects: impl IntoIterator<Item = WriteVar<SideEffect>>,
1453 ) {
1454 let mut backend = self.backend.borrow_mut();
1455 for var in use_side_effects.into_iter() {
1456 if let ReadVarBacking::RuntimeVar {
1457 var,
1458 is_side_effect: _,
1459 } = &var.backing_var
1460 {
1461 backend.on_claimed_runtime_var(var, true);
1462 }
1463 }
1464
1465 for var in resolve_side_effects.into_iter() {
1466 backend.on_claimed_runtime_var(&var.backing_var, false);
1467 }
1468
1469 backend.on_emit_side_effect_step();
1470 }
1471
1472 /// What backend the flow is being running on (e.g: locally, ADO, GitHub,
1473 /// etc...)
1474 pub fn backend(&self) -> FlowBackend {
1475 self.backend.borrow_mut().backend()
1476 }
1477
1478 /// What platform the flow is being running on (e.g: windows, linux, wsl2,
1479 /// etc...).
1480 pub fn platform(&self) -> FlowPlatform {
1481 self.backend.borrow_mut().platform()
1482 }
1483
1484 /// What architecture the flow is being running on (x86_64 or Aarch64)
1485 pub fn arch(&self) -> FlowArch {
1486 self.backend.borrow_mut().arch()
1487 }
1488
1489 /// Set a request on a particular node.
1490 pub fn req<R>(&mut self, req: R)
1491 where
1492 R: IntoRequest + 'static,
1493 {
1494 let mut backend = self.backend.borrow_mut();
1495 backend.on_request(
1496 NodeHandle::from_type::<R::Node>(),
1497 serde_json::to_vec(&req.into_request())
1498 .map(Into::into)
1499 .map_err(Into::into),
1500 );
1501 }
1502
1503 /// Set a request on a particular node, simultaneously creating a new flowey
1504 /// Var in the process.
1505 #[track_caller]
1506 #[must_use]
1507 pub fn reqv<T, R>(&mut self, f: impl FnOnce(WriteVar<T>) -> R) -> ReadVar<T>
1508 where
1509 T: Serialize + DeserializeOwned,
1510 R: IntoRequest + 'static,
1511 {
1512 let (read, write) = self.new_var();
1513 self.req::<R>(f(write));
1514 read
1515 }
1516
1517 /// Set multiple requests on a particular node.
1518 pub fn requests<N>(&mut self, reqs: impl IntoIterator<Item = N::Request>)
1519 where
1520 N: FlowNodeBase + 'static,
1521 {
1522 let mut backend = self.backend.borrow_mut();
1523 for req in reqs.into_iter() {
1524 backend.on_request(
1525 NodeHandle::from_type::<N>(),
1526 serde_json::to_vec(&req).map(Into::into).map_err(Into::into),
1527 );
1528 }
1529 }
1530
1531 /// Allocate a new flowey Var, returning two handles: one for reading the
1532 /// value, and another for writing the value.
1533 #[track_caller]
1534 #[must_use]
1535 pub fn new_var<T>(&self) -> (ReadVar<T>, WriteVar<T>)
1536 where
1537 T: Serialize + DeserializeOwned,
1538 {
1539 self.new_prefixed_var("")
1540 }
1541
1542 #[track_caller]
1543 #[must_use]
1544 fn new_prefixed_var<T>(&self, prefix: &'static str) -> (ReadVar<T>, WriteVar<T>)
1545 where
1546 T: Serialize + DeserializeOwned,
1547 {
1548 // normalize call path to ensure determinism between windows and linux
1549 let caller = std::panic::Location::caller()
1550 .to_string()
1551 .replace('\\', "/");
1552
1553 // until we have a proper way to "split" debug info related to vars, we
1554 // kinda just lump it in with the var name itself.
1555 //
1556 // HACK: to work around cases where - depending on what the
1557 // current-working-dir is when incoking flowey - the returned
1558 // caller.file() path may leak the full path of the file (as opposed to
1559 // the relative path), resulting in inconsistencies between build
1560 // environments.
1561 //
1562 // For expediency, and to preserve some semblance of useful error
1563 // messages, we decided to play some sketchy games with the resulting
1564 // string to only preserve the _consistent_ bit of the path for a human
1565 // to use as reference.
1566 //
1567 // This is not ideal in the slightest, but it works OK for now
1568 let caller = caller
1569 .split_once("flowey/")
1570 .expect("due to a known limitation with flowey, all flowey code must have an ancestor dir called 'flowey/' somewhere in its full path")
1571 .1;
1572
1573 let colon = if prefix.is_empty() { "" } else { ":" };
1574 let ordinal = self.backend.borrow_mut().on_new_var();
1575 let backing_var = format!("{prefix}{colon}{ordinal}:{caller}");
1576
1577 (
1578 ReadVar {
1579 backing_var: ReadVarBacking::RuntimeVar {
1580 var: backing_var.clone(),
1581 is_side_effect: false,
1582 },
1583 _kind: std::marker::PhantomData,
1584 },
1585 WriteVar {
1586 backing_var,
1587 is_side_effect: false,
1588 _kind: std::marker::PhantomData,
1589 },
1590 )
1591 }
1592
1593 /// Allocate special [`SideEffect`] var which can be used to schedule a
1594 /// "post-job" step associated with some existing step.
1595 ///
1596 /// This "post-job" step will then only run after all other regular steps
1597 /// have run (i.e: steps required to complete any top-level objectives
1598 /// passed in via [`crate::pipeline::PipelineJob::dep_on`]). This makes it
1599 /// useful for implementing various "cleanup" or "finalize" tasks.
1600 ///
1601 /// e.g: the Cache node uses this to upload the contents of a cache
1602 /// directory at the end of a Job.
1603 #[track_caller]
1604 #[must_use]
1605 pub fn new_post_job_side_effect(&self) -> (ReadVar<SideEffect>, WriteVar<SideEffect>) {
1606 self.new_prefixed_var("post_job")
1607 }
1608
1609 /// Return a flowey Var pointing to a **node-specific** directory which
1610 /// will be persisted between runs, if such a directory is available.
1611 ///
1612 /// WARNING: this method is _very likely_ to return None when running on CI
1613 /// machines, as most CI agents are wiped between jobs!
1614 ///
1615 /// As such, it is NOT recommended that node authors reach for this method
1616 /// directly, and instead use abstractions such as the
1617 /// `flowey_lib_common::cache` Node, which implements node-level persistence
1618 /// in a way that works _regardless_ if a persistent_dir is available (e.g:
1619 /// by falling back to uploading / downloading artifacts to a "cache store"
1620 /// on platforms like ADO or Github Actions).
1621 #[track_caller]
1622 #[must_use]
1623 pub fn persistent_dir(&mut self) -> Option<ReadVar<PathBuf>> {
1624 let path: ReadVar<PathBuf> = ReadVar {
1625 backing_var: ReadVarBacking::RuntimeVar {
1626 var: self.backend.borrow_mut().persistent_dir_path_var()?,
1627 is_side_effect: false,
1628 },
1629 _kind: std::marker::PhantomData,
1630 };
1631
1632 let folder_name = self
1633 .backend
1634 .borrow_mut()
1635 .current_node()
1636 .modpath()
1637 .replace("::", "__");
1638
1639 Some(
1640 self.emit_rust_stepv("🌼 Create persistent store dir", |ctx| {
1641 let path = path.claim(ctx);
1642 |rt| {
1643 let dir = rt.read(path).join(folder_name);
1644 fs_err::create_dir_all(&dir)?;
1645 Ok(dir)
1646 }
1647 }),
1648 )
1649 }
1650
1651 /// Check to see if a persistent dir is available, without yet creating it.
1652 pub fn supports_persistent_dir(&mut self) -> bool {
1653 self.backend
1654 .borrow_mut()
1655 .persistent_dir_path_var()
1656 .is_some()
1657 }
1658}
1659
1660// FUTURE: explore using type-erased serde here, instead of relying on
1661// `serde_json` in `flowey_core`.
1662pub trait RuntimeVarDb {
1663 fn get_var(&mut self, var_name: &str) -> (Vec<u8>, bool) {
1664 self.try_get_var(var_name)
1665 .unwrap_or_else(|| panic!("db is missing var {}", var_name))
1666 }
1667
1668 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)>;
1669 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>);
1670}
1671
1672impl RuntimeVarDb for Box<dyn RuntimeVarDb> {
1673 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)> {
1674 (**self).try_get_var(var_name)
1675 }
1676
1677 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>) {
1678 (**self).set_var(var_name, is_secret, value)
1679 }
1680}
1681
1682pub mod steps {
1683 pub mod ado {
1684 use crate::node::ClaimedReadVar;
1685 use crate::node::ClaimedWriteVar;
1686 use crate::node::ReadVarBacking;
1687 use serde::Deserialize;
1688 use serde::Serialize;
1689 use std::borrow::Cow;
1690
1691 /// An ADO repository declared as a resource in the top-level pipeline.
1692 ///
1693 /// Created via [`crate::pipeline::Pipeline::ado_add_resources_repository`].
1694 ///
1695 /// Consumed via [`AdoStepServices::resolve_repository_id`].
1696 #[derive(Debug, Clone, Serialize, Deserialize)]
1697 pub struct AdoResourcesRepositoryId {
1698 pub(crate) repo_id: String,
1699 }
1700
1701 impl AdoResourcesRepositoryId {
1702 /// Create a `AdoResourcesRepositoryId` corresponding to `self`
1703 /// (i.e: the repo which stores the current pipeline).
1704 ///
1705 /// This is safe to do from any context, as the `self` resource will
1706 /// _always_ be available.
1707 pub fn new_self() -> Self {
1708 Self {
1709 repo_id: "self".into(),
1710 }
1711 }
1712
1713 /// (dangerous) get the raw ID associated with this resource.
1714 ///
1715 /// It is highly recommended to avoid losing type-safety, and
1716 /// sticking to [`AdoStepServices::resolve_repository_id`].in order
1717 /// to resolve this type to a String.
1718 pub fn dangerous_get_raw_id(&self) -> &str {
1719 &self.repo_id
1720 }
1721
1722 /// (dangerous) create a new ID out of thin air.
1723 ///
1724 /// It is highly recommended to avoid losing type-safety, and
1725 /// sticking to [`AdoStepServices::resolve_repository_id`].in order
1726 /// to resolve this type to a String.
1727 pub fn dangerous_new(repo_id: &str) -> Self {
1728 Self {
1729 repo_id: repo_id.into(),
1730 }
1731 }
1732 }
1733
1734 /// Handle to an ADO variable.
1735 ///
1736 /// Includes a (non-exhaustive) list of associated constants
1737 /// corresponding to global ADO vars which are _always_ available.
1738 #[derive(Clone, Debug, Serialize, Deserialize)]
1739 pub struct AdoRuntimeVar {
1740 is_secret: bool,
1741 ado_var: Cow<'static, str>,
1742 }
1743
1744 #[allow(non_upper_case_globals)]
1745 impl AdoRuntimeVar {
1746 /// `build.SourceBranch`
1747 ///
1748 /// NOTE: Includes the full branch ref (ex: `refs/heads/main`) so
1749 /// unlike `build.SourceBranchName`, a branch like `user/foo/bar`
1750 /// won't be stripped to just `bar`
1751 pub const BUILD__SOURCE_BRANCH: AdoRuntimeVar =
1752 AdoRuntimeVar::new("build.SourceBranch");
1753
1754 /// `build.BuildNumber`
1755 pub const BUILD__BUILD_NUMBER: AdoRuntimeVar = AdoRuntimeVar::new("build.BuildNumber");
1756
1757 /// `System.AccessToken`
1758 pub const SYSTEM__ACCESS_TOKEN: AdoRuntimeVar =
1759 AdoRuntimeVar::new_secret("System.AccessToken");
1760
1761 /// `System.System.JobAttempt`
1762 pub const SYSTEM__JOB_ATTEMPT: AdoRuntimeVar =
1763 AdoRuntimeVar::new_secret("System.JobAttempt");
1764 }
1765
1766 impl AdoRuntimeVar {
1767 const fn new(s: &'static str) -> Self {
1768 Self {
1769 is_secret: false,
1770 ado_var: Cow::Borrowed(s),
1771 }
1772 }
1773
1774 const fn new_secret(s: &'static str) -> Self {
1775 Self {
1776 is_secret: true,
1777 ado_var: Cow::Borrowed(s),
1778 }
1779 }
1780
1781 /// Check if the ADO var is tagged as being a secret
1782 pub fn is_secret(&self) -> bool {
1783 self.is_secret
1784 }
1785
1786 /// Get the raw underlying ADO variable name
1787 pub fn as_raw_var_name(&self) -> String {
1788 self.ado_var.as_ref().into()
1789 }
1790
1791 /// Get a handle to an ADO runtime variable corresponding to a
1792 /// global ADO variable with the given name.
1793 ///
1794 /// This method should be used rarely and with great care!
1795 ///
1796 /// ADO variables are global, and sidestep the type-safe data flow
1797 /// between flowey nodes entirely!
1798 pub fn dangerous_from_global(ado_var_name: impl AsRef<str>, is_secret: bool) -> Self {
1799 Self {
1800 is_secret,
1801 ado_var: ado_var_name.as_ref().to_owned().into(),
1802 }
1803 }
1804 }
1805
1806 pub fn new_ado_step_services(
1807 fresh_ado_var: &mut dyn FnMut() -> String,
1808 ) -> AdoStepServices<'_> {
1809 AdoStepServices {
1810 fresh_ado_var,
1811 ado_to_rust: Vec::new(),
1812 rust_to_ado: Vec::new(),
1813 }
1814 }
1815
1816 pub struct CompletedAdoStepServices {
1817 pub ado_to_rust: Vec<(String, String, bool)>,
1818 pub rust_to_ado: Vec<(String, String)>,
1819 }
1820
1821 impl CompletedAdoStepServices {
1822 pub fn from_ado_step_services(access: AdoStepServices<'_>) -> Self {
1823 let AdoStepServices {
1824 fresh_ado_var: _,
1825 ado_to_rust,
1826 rust_to_ado,
1827 } = access;
1828
1829 Self {
1830 ado_to_rust,
1831 rust_to_ado,
1832 }
1833 }
1834 }
1835
1836 pub struct AdoStepServices<'a> {
1837 fresh_ado_var: &'a mut dyn FnMut() -> String,
1838 ado_to_rust: Vec<(String, String, bool)>,
1839 rust_to_ado: Vec<(String, String)>,
1840 }
1841
1842 impl AdoStepServices<'_> {
1843 /// Return the raw string identifier for the given
1844 /// [`AdoResourcesRepositoryId`].
1845 pub fn resolve_repository_id(&self, repo_id: AdoResourcesRepositoryId) -> String {
1846 repo_id.repo_id
1847 }
1848
1849 /// Set the specified flowey Var using the value of the given ADO var.
1850 // TODO: is there a good way to allow auto-casting the ADO var back
1851 // to a WriteVar<T>, instead of just a String? It's complicated by
1852 // the fact that the ADO var to flowey bridge is handled by the ADO
1853 // backend, which itself needs to know type info...
1854 pub fn set_var(&mut self, var: ClaimedWriteVar<String>, from_ado_var: AdoRuntimeVar) {
1855 self.ado_to_rust.push((
1856 from_ado_var.ado_var.into(),
1857 var.backing_var,
1858 from_ado_var.is_secret,
1859 ))
1860 }
1861
1862 /// Get the value of a flowey Var as a ADO runtime variable.
1863 pub fn get_var(&mut self, var: ClaimedReadVar<String>) -> AdoRuntimeVar {
1864 let backing_var = if let ReadVarBacking::RuntimeVar {
1865 var,
1866 is_side_effect,
1867 } = &var.backing_var
1868 {
1869 assert!(!is_side_effect);
1870 var
1871 } else {
1872 todo!("support inline ado read vars")
1873 };
1874
1875 let new_ado_var_name = (self.fresh_ado_var)();
1876
1877 self.rust_to_ado
1878 .push((backing_var.clone(), new_ado_var_name.clone()));
1879 AdoRuntimeVar::dangerous_from_global(new_ado_var_name, false)
1880 }
1881 }
1882 }
1883
1884 pub mod github {
1885 use crate::node::ClaimVar;
1886 use crate::node::NodeCtx;
1887 use crate::node::ReadVar;
1888 use crate::node::ReadVarBacking;
1889 use crate::node::SideEffect;
1890 use crate::node::StepCtx;
1891 use crate::node::VarClaimed;
1892 use crate::node::VarNotClaimed;
1893 use crate::node::WriteVar;
1894 use std::collections::BTreeMap;
1895
1896 pub struct GhStepBuilder {
1897 display_name: String,
1898 cond: Option<ReadVar<bool>>,
1899 uses: String,
1900 with: Option<BTreeMap<String, GhParam>>,
1901 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
1902 run_after: Vec<ReadVar<SideEffect>>,
1903 permissions: BTreeMap<GhPermission, GhPermissionValue>,
1904 }
1905
1906 impl GhStepBuilder {
1907 /// Creates a new GitHub step builder, with the given display name and
1908 /// action to use. For example, the following code generates the following yaml:
1909 ///
1910 /// ```ignore
1911 /// GhStepBuilder::new("Check out repository code", "actions/checkout@v4").finish()
1912 /// ```
1913 ///
1914 /// ```ignore
1915 /// - name: Check out repository code
1916 /// uses: actions/checkout@v4
1917 /// ```
1918 ///
1919 /// For more information on the yaml syntax for the `name` and `uses` parameters,
1920 /// see <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsname>
1921 pub fn new(display_name: impl AsRef<str>, uses: impl AsRef<str>) -> Self {
1922 Self {
1923 display_name: display_name.as_ref().into(),
1924 cond: None,
1925 uses: uses.as_ref().into(),
1926 with: None,
1927 outputs: BTreeMap::new(),
1928 run_after: Vec::new(),
1929 permissions: BTreeMap::new(),
1930 }
1931 }
1932
1933 /// Adds a condition [`ReadVar<bool>`] to the step,
1934 /// such that the step only executes if the condition is true.
1935 /// This is equivalent to using an `if` conditional in the yaml.
1936 ///
1937 /// For more information on the yaml syntax for `if` conditionals, see
1938 /// <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsname>
1939 pub fn condition(mut self, cond: ReadVar<bool>) -> Self {
1940 self.cond = Some(cond);
1941 self
1942 }
1943
1944 /// Adds a parameter to the step, specified as a key-value pair corresponding
1945 /// to the param name and value. For example the following code generates the following yaml:
1946 ///
1947 /// ```rust,ignore
1948 /// let (client_id, write_client_id) = ctx.new_var();
1949 /// let (tenant_id, write_tenant_id) = ctx.new_var();
1950 /// let (subscription_id, write_subscription_id) = ctx.new_var();
1951 /// // ... insert rust step writing to each of those secrets ...
1952 /// GhStepBuilder::new("Azure Login", "Azure/login@v2")
1953 /// .with("client-id", client_id)
1954 /// .with("tenant-id", tenant_id)
1955 /// .with("subscription-id", subscription_id)
1956 /// ```
1957 ///
1958 /// ```text
1959 /// - name: Azure Login
1960 /// uses: Azure/login@v2
1961 /// with:
1962 /// client-id: ${{ env.floweyvar1 }} // Assuming the backend wrote client_id to floweyvar1
1963 /// tenant-id: ${{ env.floweyvar2 }} // Assuming the backend wrote tenant-id to floweyvar2
1964 /// subscription-id: ${{ env.floweyvar3 }} // Assuming the backend wrote subscription-id to floweyvar3
1965 /// ```
1966 ///
1967 /// For more information on the yaml syntax for the `with` parameters,
1968 /// see <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith>
1969 pub fn with(mut self, k: impl AsRef<str>, v: impl Into<GhParam>) -> Self {
1970 self.with.get_or_insert_with(BTreeMap::new);
1971 if let Some(with) = &mut self.with {
1972 with.insert(k.as_ref().to_string(), v.into());
1973 }
1974 self
1975 }
1976
1977 /// Specifies an output to read from the step, specified as a key-value pair
1978 /// corresponding to the output name and the flowey var to write the output to.
1979 ///
1980 /// This is equivalent to writing into `v` the output of a step in the yaml using:
1981 /// `${{ steps.<backend-assigned-step-id>.outputs.<k> }}`
1982 ///
1983 /// For more information on step outputs, see
1984 /// <https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#outputs-for-composite-actions>
1985 pub fn output(mut self, k: impl AsRef<str>, v: WriteVar<String>) -> Self {
1986 self.outputs
1987 .entry(k.as_ref().to_string())
1988 .or_default()
1989 .push(v);
1990 self
1991 }
1992
1993 /// Specifies a side-effect that must be resolved before this step can run.
1994 pub fn run_after(mut self, side_effect: ReadVar<SideEffect>) -> Self {
1995 self.run_after.push(side_effect);
1996 self
1997 }
1998
1999 /// Declare that this step requires a certain GITHUB_TOKEN permission in order to run.
2000 ///
2001 /// For more info about Github Actions permissions, see [`gh_grant_permissions`](crate::pipeline::PipelineJob::gh_grant_permissions) and
2002 /// <https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/assigning-permissions-to-jobs>
2003 pub fn requires_permission(
2004 mut self,
2005 perm: GhPermission,
2006 value: GhPermissionValue,
2007 ) -> Self {
2008 self.permissions.insert(perm, value);
2009 self
2010 }
2011
2012 /// Finish building the step, emitting it to the backend and returning a side-effect.
2013 #[track_caller]
2014 pub fn finish(self, ctx: &mut NodeCtx<'_>) -> ReadVar<SideEffect> {
2015 let (side_effect, claim_side_effect) = ctx.new_prefixed_var("auto_se");
2016 ctx.backend
2017 .borrow_mut()
2018 .on_claimed_runtime_var(&claim_side_effect.backing_var, false);
2019
2020 ctx.emit_gh_step_inner(
2021 self.display_name,
2022 self.cond,
2023 self.uses,
2024 self.with,
2025 self.outputs,
2026 self.run_after,
2027 self.permissions,
2028 );
2029
2030 side_effect
2031 }
2032 }
2033
2034 #[derive(Clone, Debug)]
2035 pub enum GhParam<C = VarNotClaimed> {
2036 Static(String),
2037 FloweyVar(ReadVar<String, C>),
2038 }
2039
2040 impl From<String> for GhParam {
2041 fn from(param: String) -> GhParam {
2042 GhParam::Static(param)
2043 }
2044 }
2045
2046 impl From<&str> for GhParam {
2047 fn from(param: &str) -> GhParam {
2048 GhParam::Static(param.to_string())
2049 }
2050 }
2051
2052 impl From<ReadVar<String>> for GhParam {
2053 fn from(param: ReadVar<String>) -> GhParam {
2054 GhParam::FloweyVar(param)
2055 }
2056 }
2057
2058 pub type ClaimedGhParam = GhParam<VarClaimed>;
2059
2060 impl ClaimVar for GhParam {
2061 type Claimed = ClaimedGhParam;
2062
2063 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedGhParam {
2064 match self {
2065 GhParam::Static(s) => ClaimedGhParam::Static(s),
2066 GhParam::FloweyVar(var) => match &var.backing_var {
2067 ReadVarBacking::RuntimeVar { is_side_effect, .. } => {
2068 assert!(!is_side_effect);
2069 ClaimedGhParam::FloweyVar(var.claim(ctx))
2070 }
2071 ReadVarBacking::Inline(var) => ClaimedGhParam::Static(var.clone()),
2072 },
2073 }
2074 }
2075 }
2076
2077 /// The assigned permission value for a scope.
2078 ///
2079 /// For more details on how these values affect a particular scope, refer to:
2080 /// <https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs>
2081 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2082 pub enum GhPermissionValue {
2083 Read,
2084 Write,
2085 None,
2086 }
2087
2088 /// Refers to the scope of a permission granted to the GITHUB_TOKEN
2089 /// for a job.
2090 ///
2091 /// For more details on each scope, refer to:
2092 /// <https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs>
2093 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2094 pub enum GhPermission {
2095 Actions,
2096 Attestations,
2097 Checks,
2098 Contents,
2099 Deployments,
2100 Discussions,
2101 IdToken,
2102 Issues,
2103 Packages,
2104 Pages,
2105 PullRequests,
2106 RepositoryProjects,
2107 SecurityEvents,
2108 Statuses,
2109 }
2110 }
2111
2112 pub mod rust {
2113 use crate::node::ClaimedWriteVar;
2114 use crate::node::FlowArch;
2115 use crate::node::FlowBackend;
2116 use crate::node::FlowPlatform;
2117 use crate::node::ReadVarValue;
2118 use crate::node::RuntimeVarDb;
2119 use serde::Serialize;
2120 use serde::de::DeserializeOwned;
2121
2122 pub fn new_rust_runtime_services(
2123 runtime_var_db: &mut dyn RuntimeVarDb,
2124 backend: FlowBackend,
2125 platform: FlowPlatform,
2126 arch: FlowArch,
2127 ) -> RustRuntimeServices<'_> {
2128 RustRuntimeServices {
2129 runtime_var_db,
2130 backend,
2131 platform,
2132 arch,
2133 has_read_secret: false,
2134 }
2135 }
2136
2137 pub struct RustRuntimeServices<'a> {
2138 runtime_var_db: &'a mut dyn RuntimeVarDb,
2139 backend: FlowBackend,
2140 platform: FlowPlatform,
2141 arch: FlowArch,
2142 has_read_secret: bool,
2143 }
2144
2145 impl RustRuntimeServices<'_> {
2146 /// What backend the flow is being running on (e.g: locally, ADO,
2147 /// GitHub, etc...)
2148 pub fn backend(&self) -> FlowBackend {
2149 self.backend
2150 }
2151
2152 /// What platform the flow is being running on (e.g: windows, linux,
2153 /// etc...).
2154 pub fn platform(&self) -> FlowPlatform {
2155 self.platform
2156 }
2157
2158 /// What arch the flow is being running on (X86_64 or Aarch64)
2159 pub fn arch(&self) -> FlowArch {
2160 self.arch
2161 }
2162
2163 /// Write a value.
2164 ///
2165 /// If this step has already read a secret value, then this will be
2166 /// written as a secret value, as a conservative estimate to avoid
2167 /// leaking secrets. Use [`write_secret`](Self::write_secret) or
2168 /// [`write_not_secret`](Self::write_not_secret) to override this
2169 /// behavior.
2170 pub fn write<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2171 where
2172 T: Serialize + DeserializeOwned,
2173 {
2174 self.write_maybe_secret(var, val, self.has_read_secret)
2175 }
2176
2177 /// Write a secret value, such as a key or token.
2178 ///
2179 /// Flowey will avoid logging this value, and if the value is
2180 /// converted to a CI environment variable, the CI system will be
2181 /// told not to print the value either.
2182 pub fn write_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2183 where
2184 T: Serialize + DeserializeOwned,
2185 {
2186 self.write_maybe_secret(var, val, true)
2187 }
2188
2189 /// Write a value that is not secret, even if this step has already
2190 /// read secret values.
2191 ///
2192 /// Usually [`write`](Self::write) is preferred--use this only when
2193 /// your step reads secret values and you explicitly want to write a
2194 /// non-secret value.
2195 pub fn write_not_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2196 where
2197 T: Serialize + DeserializeOwned,
2198 {
2199 self.write_maybe_secret(var, val, false)
2200 }
2201
2202 fn write_maybe_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T, is_secret: bool)
2203 where
2204 T: Serialize + DeserializeOwned,
2205 {
2206 let val = if var.is_side_effect {
2207 b"null".to_vec()
2208 } else {
2209 serde_json::to_vec(val).expect("improve this error path")
2210 };
2211 self.runtime_var_db
2212 .set_var(&var.backing_var, is_secret, val);
2213 }
2214
2215 pub fn write_all<T>(
2216 &mut self,
2217 vars: impl IntoIterator<Item = ClaimedWriteVar<T>>,
2218 val: &T,
2219 ) where
2220 T: Serialize + DeserializeOwned,
2221 {
2222 for var in vars {
2223 self.write(var, val)
2224 }
2225 }
2226
2227 pub fn read<T: ReadVarValue>(&mut self, var: T) -> T::Value {
2228 var.read_value(self)
2229 }
2230
2231 pub(crate) fn get_var(&mut self, var: &str, is_side_effect: bool) -> Vec<u8> {
2232 let (v, is_secret) = self.runtime_var_db.get_var(var);
2233 self.has_read_secret |= is_secret && !is_side_effect;
2234 v
2235 }
2236
2237 /// DANGEROUS: Set the value of _Global_ Environment Variable (GitHub Actions only).
2238 ///
2239 /// It is up to the caller to ensure that the variable does not get
2240 /// unintentionally overwritten or used.
2241 ///
2242 /// This method should be used rarely and with great care!
2243 pub fn dangerous_gh_set_global_env_var(
2244 &mut self,
2245 var: String,
2246 gh_env_var: String,
2247 ) -> anyhow::Result<()> {
2248 if !matches!(self.backend, FlowBackend::Github) {
2249 return Err(anyhow::anyhow!(
2250 "dangerous_set_gh_env_var can only be used on GitHub Actions"
2251 ));
2252 }
2253
2254 let gh_env_file_path = std::env::var("GITHUB_ENV")?;
2255 let mut gh_env_file = fs_err::OpenOptions::new()
2256 .append(true)
2257 .open(gh_env_file_path)?;
2258 let gh_env_var_assignment = format!(
2259 r#"{}<<EOF
2260{}
2261EOF
2262"#,
2263 gh_env_var, var
2264 );
2265 std::io::Write::write_all(&mut gh_env_file, gh_env_var_assignment.as_bytes())?;
2266
2267 Ok(())
2268 }
2269 }
2270 }
2271}
2272
2273/// The base underlying implementation of all FlowNode variants.
2274///
2275/// Do not implement this directly! Use the `new_flow_node!` family of macros
2276/// instead!
2277pub trait FlowNodeBase {
2278 type Request: Serialize + DeserializeOwned;
2279
2280 fn imports(&mut self, ctx: &mut ImportCtx<'_>);
2281 fn emit(&mut self, requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2282
2283 /// A noop method that all human-written impls of `FlowNodeBase` are
2284 /// required to implement.
2285 ///
2286 /// By implementing this method, you're stating that you "know what you're
2287 /// doing" by having this manual impl.
2288 fn i_know_what_im_doing_with_this_manual_impl(&mut self);
2289}
2290
2291pub mod erased {
2292 use crate::node::FlowNodeBase;
2293 use crate::node::NodeCtx;
2294 use crate::node::user_facing::*;
2295
2296 pub struct ErasedNode<N: FlowNodeBase>(pub N);
2297
2298 impl<N: FlowNodeBase> ErasedNode<N> {
2299 pub fn from_node(node: N) -> Self {
2300 Self(node)
2301 }
2302 }
2303
2304 impl<N> FlowNodeBase for ErasedNode<N>
2305 where
2306 N: FlowNodeBase,
2307 {
2308 // FIXME: this should be using type-erased serde
2309 type Request = Box<[u8]>;
2310
2311 fn imports(&mut self, ctx: &mut ImportCtx<'_>) {
2312 self.0.imports(ctx)
2313 }
2314
2315 fn emit(&mut self, requests: Vec<Box<[u8]>>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
2316 let mut converted_requests = Vec::new();
2317 for req in requests {
2318 converted_requests.push(serde_json::from_slice(&req)?)
2319 }
2320
2321 self.0.emit(converted_requests, ctx)
2322 }
2323
2324 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2325 }
2326}
2327
2328/// Cheap handle to a registered [`FlowNode`]
2329#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2330pub struct NodeHandle(std::any::TypeId);
2331
2332impl Ord for NodeHandle {
2333 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2334 self.modpath().cmp(other.modpath())
2335 }
2336}
2337
2338impl PartialOrd for NodeHandle {
2339 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2340 Some(self.cmp(other))
2341 }
2342}
2343
2344impl std::fmt::Debug for NodeHandle {
2345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2346 std::fmt::Debug::fmt(&self.try_modpath(), f)
2347 }
2348}
2349
2350impl NodeHandle {
2351 pub fn from_type<N: FlowNodeBase + 'static>() -> NodeHandle {
2352 NodeHandle(std::any::TypeId::of::<N>())
2353 }
2354
2355 pub fn from_modpath(modpath: &str) -> NodeHandle {
2356 node_luts::erased_node_by_modpath().get(modpath).unwrap().0
2357 }
2358
2359 pub fn try_from_modpath(modpath: &str) -> Option<NodeHandle> {
2360 node_luts::erased_node_by_modpath()
2361 .get(modpath)
2362 .map(|(s, _)| *s)
2363 }
2364
2365 pub fn new_erased_node(&self) -> Box<dyn FlowNodeBase<Request = Box<[u8]>>> {
2366 let ctor = node_luts::erased_node_by_typeid().get(self).unwrap();
2367 ctor()
2368 }
2369
2370 pub fn modpath(&self) -> &'static str {
2371 node_luts::modpath_by_node_typeid().get(self).unwrap()
2372 }
2373
2374 pub fn try_modpath(&self) -> Option<&'static str> {
2375 node_luts::modpath_by_node_typeid().get(self).cloned()
2376 }
2377
2378 /// Return a dummy NodeHandle, which will panic if `new_erased_node` is ever
2379 /// called on it.
2380 pub fn dummy() -> NodeHandle {
2381 NodeHandle(std::any::TypeId::of::<()>())
2382 }
2383}
2384
2385pub fn list_all_registered_nodes() -> impl Iterator<Item = NodeHandle> {
2386 node_luts::modpath_by_node_typeid().keys().cloned()
2387}
2388
2389// Encapsulate these look up tables in their own module to limit the scope of
2390// the HashMap import.
2391//
2392// In general, using HashMap in flowey is a recipe for disaster, given that
2393// iterating through the hash-map will result in non-deterministic orderings,
2394// which can cause annoying ordering churn.
2395//
2396// That said, in this case, it's OK since the code using these LUTs won't ever
2397// iterate through the map.
2398//
2399// Why is the HashMap even necessary vs. a BTreeMap?
2400//
2401// Well... NodeHandle's `Ord` impl does a `modpath` comparison instead of a
2402// TypeId comparison, since TypeId will vary between compilations.
2403mod node_luts {
2404 use super::FlowNodeBase;
2405 use super::NodeHandle;
2406 use std::collections::HashMap;
2407 use std::sync::OnceLock;
2408
2409 pub(super) fn modpath_by_node_typeid() -> &'static HashMap<NodeHandle, &'static str> {
2410 static TYPEID_TO_MODPATH: OnceLock<HashMap<NodeHandle, &'static str>> = OnceLock::new();
2411
2412 TYPEID_TO_MODPATH.get_or_init(|| {
2413 let mut lookup = HashMap::new();
2414 for crate::node::private::FlowNodeMeta {
2415 module_path,
2416 ctor: _,
2417 get_typeid,
2418 } in crate::node::private::FLOW_NODES
2419 {
2420 let existing = lookup.insert(
2421 NodeHandle(get_typeid()),
2422 module_path
2423 .strip_suffix("::_only_one_call_to_flowey_node_per_module")
2424 .unwrap(),
2425 );
2426 // if this were to fire for an array where the key is a TypeId...
2427 // something has gone _terribly_ wrong
2428 assert!(existing.is_none())
2429 }
2430
2431 lookup
2432 })
2433 }
2434
2435 pub(super) fn erased_node_by_typeid()
2436 -> &'static HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>> {
2437 static LOOKUP: OnceLock<
2438 HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>>,
2439 > = OnceLock::new();
2440
2441 LOOKUP.get_or_init(|| {
2442 let mut lookup = HashMap::new();
2443 for crate::node::private::FlowNodeMeta {
2444 module_path: _,
2445 ctor,
2446 get_typeid,
2447 } in crate::node::private::FLOW_NODES
2448 {
2449 let existing = lookup.insert(NodeHandle(get_typeid()), *ctor);
2450 // if this were to fire for an array where the key is a TypeId...
2451 // something has gone _terribly_ wrong
2452 assert!(existing.is_none())
2453 }
2454
2455 lookup
2456 })
2457 }
2458
2459 pub(super) fn erased_node_by_modpath() -> &'static HashMap<
2460 &'static str,
2461 (
2462 NodeHandle,
2463 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2464 ),
2465 > {
2466 static MODPATH_LOOKUP: OnceLock<
2467 HashMap<
2468 &'static str,
2469 (
2470 NodeHandle,
2471 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2472 ),
2473 >,
2474 > = OnceLock::new();
2475
2476 MODPATH_LOOKUP.get_or_init(|| {
2477 let mut lookup = HashMap::new();
2478 for crate::node::private::FlowNodeMeta { module_path, ctor, get_typeid } in crate::node::private::FLOW_NODES {
2479 let existing = lookup.insert(module_path.strip_suffix("::_only_one_call_to_flowey_node_per_module").unwrap(), (NodeHandle(get_typeid()), *ctor));
2480 if existing.is_some() {
2481 panic!("conflicting node registrations at {module_path}! please ensure there is a single node per module!")
2482 }
2483 }
2484 lookup
2485 })
2486 }
2487}
2488
2489#[doc(hidden)]
2490pub mod private {
2491 pub use linkme;
2492
2493 pub struct FlowNodeMeta {
2494 pub module_path: &'static str,
2495 pub ctor: fn() -> Box<dyn super::FlowNodeBase<Request = Box<[u8]>>>,
2496 // FUTURE: there is a RFC to make this const
2497 pub get_typeid: fn() -> std::any::TypeId,
2498 }
2499
2500 #[linkme::distributed_slice]
2501 pub static FLOW_NODES: [FlowNodeMeta] = [..];
2502
2503 // UNSAFETY: linkme uses manual link sections, which are unsafe.
2504 #[expect(unsafe_code)]
2505 #[linkme::distributed_slice(FLOW_NODES)]
2506 static DUMMY_FLOW_NODE: FlowNodeMeta = FlowNodeMeta {
2507 module_path: "<dummy>::_only_one_call_to_flowey_node_per_module",
2508 ctor: || unreachable!(),
2509 get_typeid: std::any::TypeId::of::<()>,
2510 };
2511}
2512
2513#[doc(hidden)]
2514#[macro_export]
2515macro_rules! new_flow_node_base {
2516 (struct Node) => {
2517 /// (see module-level docs)
2518 #[non_exhaustive]
2519 pub struct Node;
2520
2521 mod _only_one_call_to_flowey_node_per_module {
2522 const _: () = {
2523 use $crate::node::private::linkme;
2524
2525 fn new_erased() -> Box<dyn $crate::node::FlowNodeBase<Request = Box<[u8]>>> {
2526 Box::new($crate::node::erased::ErasedNode(super::Node))
2527 }
2528
2529 #[linkme::distributed_slice($crate::node::private::FLOW_NODES)]
2530 #[linkme(crate = linkme)]
2531 static FLOW_NODE: $crate::node::private::FlowNodeMeta =
2532 $crate::node::private::FlowNodeMeta {
2533 module_path: module_path!(),
2534 ctor: new_erased,
2535 get_typeid: std::any::TypeId::of::<super::Node>,
2536 };
2537 };
2538 }
2539 };
2540}
2541
2542/// TODO: clearly verbalize what a `FlowNode` encompasses
2543pub trait FlowNode {
2544 /// TODO: clearly verbalize what a Request encompasses
2545 type Request: Serialize + DeserializeOwned;
2546
2547 /// A list of nodes that this node is capable of taking a dependency on.
2548 ///
2549 /// Attempting to take a dep on a node that wasn't imported via this method
2550 /// will result in an error during flow resolution time.
2551 ///
2552 /// * * *
2553 ///
2554 /// To put it bluntly: This is boilerplate.
2555 ///
2556 /// We (the flowey devs) are thinking about ways to avoid requiring this
2557 /// method, but do not have a good solution at this time.
2558 fn imports(ctx: &mut ImportCtx<'_>);
2559
2560 /// Given a set of incoming `requests`, emit various steps to run, set
2561 /// various dependencies, etc...
2562 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2563}
2564
2565#[macro_export]
2566macro_rules! new_flow_node {
2567 (struct Node) => {
2568 $crate::new_flow_node_base!(struct Node);
2569
2570 impl $crate::node::FlowNodeBase for Node
2571 where
2572 Node: FlowNode,
2573 {
2574 type Request = <Node as FlowNode>::Request;
2575
2576 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2577 <Node as FlowNode>::imports(dep)
2578 }
2579
2580 fn emit(
2581 &mut self,
2582 requests: Vec<Self::Request>,
2583 ctx: &mut $crate::node::NodeCtx<'_>,
2584 ) -> anyhow::Result<()> {
2585 <Node as FlowNode>::emit(requests, ctx)
2586 }
2587
2588 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2589 }
2590 };
2591}
2592
2593/// A helper trait to streamline implementing [`FlowNode`] instances that only
2594/// ever operate on a single request at a time.
2595///
2596/// In essence, [`SimpleFlowNode`] handles the boilerplate (and rightward-drift)
2597/// of manually writing:
2598///
2599/// ```ignore
2600/// impl FlowNode for Node {
2601/// fn imports(dep: &mut ImportCtx<'_>) { ... }
2602/// fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) {
2603/// for req in requests {
2604/// Node::process_request(req, ctx)
2605/// }
2606/// }
2607/// }
2608/// ```
2609///
2610/// Nodes which accept a `struct Request` often fall into this pattern, whereas
2611/// nodes which accept a `enum Request` typically require additional logic to
2612/// aggregate / resolve incoming requests.
2613pub trait SimpleFlowNode {
2614 type Request: Serialize + DeserializeOwned;
2615
2616 /// A list of nodes that this node is capable of taking a dependency on.
2617 ///
2618 /// Attempting to take a dep on a node that wasn't imported via this method
2619 /// will result in an error during flow resolution time.
2620 ///
2621 /// * * *
2622 ///
2623 /// To put it bluntly: This is boilerplate.
2624 ///
2625 /// We (the flowey devs) are thinking about ways to avoid requiring this
2626 /// method, but do not have a good solution at this time.
2627 fn imports(ctx: &mut ImportCtx<'_>);
2628
2629 /// Process a single incoming `Self::Request`
2630 fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2631}
2632
2633#[macro_export]
2634macro_rules! new_simple_flow_node {
2635 (struct Node) => {
2636 $crate::new_flow_node_base!(struct Node);
2637
2638 impl $crate::node::FlowNodeBase for Node
2639 where
2640 Node: $crate::node::SimpleFlowNode,
2641 {
2642 type Request = <Node as $crate::node::SimpleFlowNode>::Request;
2643
2644 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2645 <Node as $crate::node::SimpleFlowNode>::imports(dep)
2646 }
2647
2648 fn emit(&mut self, requests: Vec<Self::Request>, ctx: &mut $crate::node::NodeCtx<'_>) -> anyhow::Result<()> {
2649 for req in requests {
2650 <Node as $crate::node::SimpleFlowNode>::process_request(req, ctx)?
2651 }
2652
2653 Ok(())
2654 }
2655
2656 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2657 }
2658 };
2659}
2660
2661/// A "glue" trait which improves [`NodeCtx::req`] ergonomics, by tying a
2662/// particular `Request` type to its corresponding [`FlowNode`].
2663///
2664/// This trait should be autogenerated via [`flowey_request!`] - do not try to
2665/// implement it manually!
2666///
2667/// [`flowey_request!`]: crate::flowey_request
2668pub trait IntoRequest {
2669 type Node: FlowNodeBase;
2670 fn into_request(self) -> <Self::Node as FlowNodeBase>::Request;
2671
2672 /// By implementing this method manually, you're indicating that you know what you're
2673 /// doing,
2674 #[doc(hidden)]
2675 #[expect(nonstandard_style)]
2676 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self);
2677}
2678
2679#[doc(hidden)]
2680#[macro_export]
2681macro_rules! __flowey_request_inner {
2682 //
2683 // @emit_struct: emit structs for each variant of the request enum
2684 //
2685 (@emit_struct [$req:ident]
2686 $(#[$a:meta])*
2687 $variant:ident($($tt:tt)*),
2688 $($rest:tt)*
2689 ) => {
2690 $(#[$a])*
2691 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
2692 pub struct $variant($($tt)*);
2693
2694 impl IntoRequest for $variant {
2695 type Node = Node;
2696 fn into_request(self) -> $req {
2697 $req::$variant(self)
2698 }
2699 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2700 }
2701
2702 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
2703 };
2704 (@emit_struct [$req:ident]
2705 $(#[$a:meta])*
2706 $variant:ident { $($tt:tt)* },
2707 $($rest:tt)*
2708 ) => {
2709 $(#[$a])*
2710 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
2711 pub struct $variant {
2712 $($tt)*
2713 }
2714
2715 impl IntoRequest for $variant {
2716 type Node = Node;
2717 fn into_request(self) -> $req {
2718 $req::$variant(self)
2719 }
2720 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2721 }
2722
2723 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
2724 };
2725 (@emit_struct [$req:ident]
2726 $(#[$a:meta])*
2727 $variant:ident,
2728 $($rest:tt)*
2729 ) => {
2730 $(#[$a])*
2731 #[derive(Serialize, Deserialize)]
2732 pub struct $variant;
2733
2734 impl IntoRequest for $variant {
2735 type Node = Node;
2736 fn into_request(self) -> $req {
2737 $req::$variant(self)
2738 }
2739 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2740 }
2741
2742 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
2743 };
2744 (@emit_struct [$req:ident]
2745 ) => {};
2746
2747 //
2748 // @emit_req_enum: build up root request enum
2749 //
2750 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
2751 $(#[$a:meta])*
2752 $variant:ident($($tt:tt)*),
2753 $($rest:tt)*
2754 ) => {
2755 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
2756 };
2757 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
2758 $(#[$a:meta])*
2759 $variant:ident { $($tt:tt)* },
2760 $($rest:tt)*
2761 ) => {
2762 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
2763 };
2764 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
2765 $(#[$a:meta])*
2766 $variant:ident,
2767 $($rest:tt)*
2768 ) => {
2769 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
2770 };
2771 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
2772 ) => {
2773 #[derive(Serialize, Deserialize)]
2774 pub enum $req {$(
2775 $(#[$prev_a])*
2776 $prev(self::req::$prev),
2777 )*}
2778
2779 impl IntoRequest for $req {
2780 type Node = Node;
2781 fn into_request(self) -> $req {
2782 self
2783 }
2784 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2785 }
2786 };
2787}
2788
2789/// Declare a new `Request` type for the current `Node`.
2790///
2791/// ## `struct` and `enum` Requests
2792///
2793/// When wrapping a vanilla Rust `struct` and `enum` declaration, this macro
2794/// simply derives [`Serialize`], [`Deserialize`], and [`IntoRequest`] for the
2795/// type, and does nothing else.
2796///
2797/// ## `enum_struct` Requests
2798///
2799/// This macro also supports a special kind of `enum_struct` derive, which
2800/// allows declaring a Request enum where each variant is split off into its own
2801/// separate (named) `struct`.
2802///
2803/// e.g:
2804///
2805/// ```ignore
2806/// flowey_request! {
2807/// pub enum_struct Foo {
2808/// Bar,
2809/// Baz(pub usize),
2810/// Qux(pub String),
2811/// }
2812/// }
2813/// ```
2814///
2815/// will be expanded into:
2816///
2817/// ```ignore
2818/// #[derive(Serialize, Deserialize)]
2819/// pub enum Foo {
2820/// Bar(req::Bar),
2821/// Baz(req::Baz),
2822/// Qux(req::Qux),
2823/// }
2824///
2825/// pud mod req {
2826/// #[derive(Serialize, Deserialize)]
2827/// pub struct Bar;
2828///
2829/// #[derive(Serialize, Deserialize)]
2830/// pub struct Baz(pub usize);
2831///
2832/// #[derive(Serialize, Deserialize)]
2833/// pub struct Qux(pub String);
2834/// }
2835/// ```
2836#[macro_export]
2837macro_rules! flowey_request {
2838 (
2839 $(#[$root_a:meta])*
2840 pub enum_struct $req:ident {
2841 $($tt:tt)*
2842 }
2843 ) => {
2844 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*),] $($tt)*);
2845 pub mod req {
2846 use super::*;
2847 $crate::__flowey_request_inner!(@emit_struct [$req] $($tt)*);
2848 }
2849 };
2850
2851 (
2852 $(#[$a:meta])*
2853 pub enum $req:ident {
2854 $($tt:tt)*
2855 }
2856 ) => {
2857 $(#[$a])*
2858 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
2859 pub enum $req {
2860 $($tt)*
2861 }
2862
2863 impl $crate::node::IntoRequest for $req {
2864 type Node = Node;
2865 fn into_request(self) -> $req {
2866 self
2867 }
2868 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2869 }
2870 };
2871
2872 (
2873 $(#[$a:meta])*
2874 pub struct $req:ident {
2875 $($tt:tt)*
2876 }
2877 ) => {
2878 $(#[$a])*
2879 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
2880 pub struct $req {
2881 $($tt)*
2882 }
2883
2884 impl $crate::node::IntoRequest for $req {
2885 type Node = Node;
2886 fn into_request(self) -> $req {
2887 self
2888 }
2889 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2890 }
2891 };
2892
2893 (
2894 $(#[$a:meta])*
2895 pub struct $req:ident($($tt:tt)*);
2896 ) => {
2897 $(#[$a])*
2898 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
2899 pub struct $req($($tt)*);
2900
2901 impl $crate::node::IntoRequest for $req {
2902 type Node = Node;
2903 fn into_request(self) -> $req {
2904 self
2905 }
2906 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
2907 }
2908 };
2909}