Skip to main content

flowey_core/
node.rs

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