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