microsoft/openvmm

Public

mirrored from https://github.com/microsoft/openvmmAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
72af67f6f4d9d042a32318a024b4ccb612bbd2db

Branches

Tags

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

Clone

HTTPS

Download ZIP

flowey/flowey_core/src/node.rs

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