microsoft/openvmm

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6d9f566744d8d46492a564b6663d29dfacfc3fcc

Branches

Tags

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

Clone

HTTPS

Download ZIP

flowey/flowey_core/src/node.rs

2899lines · modecode

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