microsoft/openvmm

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
93af13fed5d5fc7a8a08fbf37c0ea1e155c4160a

Branches

Tags

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

Clone

HTTPS

Download ZIP

flowey/flowey_core/src/node.rs

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