1mod github_context;
7mod spec;
8
9pub use github_context::GhOutput;
10pub use github_context::GhToRust;
11pub use github_context::RustToGh;
12
13use self::steps::ado::AdoRuntimeVar;
14use self::steps::ado::AdoStepServices;
15use self::steps::github::GhStepBuilder;
16use self::steps::rust::RustRuntimeServices;
17use self::user_facing::ClaimedGhParam;
18use self::user_facing::GhPermission;
19use self::user_facing::GhPermissionValue;
20use crate::node::github_context::GhContextVarReader;
21use github_context::state::Root;
22use serde::Deserialize;
23use serde::Serialize;
24use serde::de::DeserializeOwned;
25use std::cell::RefCell;
26use std::collections::BTreeMap;
27use std::path::PathBuf;
28use std::rc::Rc;
29use user_facing::GhParam;
30
31pub mod user_facing {
34 pub use super::ClaimVar;
35 pub use super::ClaimedReadVar;
36 pub use super::ClaimedWriteVar;
37 pub use super::ConfigField;
38 pub use super::ConfigMerge;
39 pub use super::ConfigVar;
40 pub use super::FlowArch;
41 pub use super::FlowBackend;
42 pub use super::FlowNode;
43 pub use super::FlowNodeWithConfig;
44 pub use super::FlowPlatform;
45 pub use super::FlowPlatformKind;
46 pub use super::GhUserSecretVar;
47 pub use super::ImportCtx;
48 pub use super::IntoConfig;
49 pub use super::IntoRequest;
50 pub use super::NodeCtx;
51 pub use super::ReadVar;
52 pub use super::SideEffect;
53 pub use super::SimpleFlowNode;
54 pub use super::StepCtx;
55 pub use super::VarClaimed;
56 pub use super::VarEqBacking;
57 pub use super::VarNotClaimed;
58 pub use super::WriteVar;
59 pub use super::steps::ado::AdoResourcesRepositoryId;
60 pub use super::steps::ado::AdoRuntimeVar;
61 pub use super::steps::ado::AdoStepServices;
62 pub use super::steps::github::ClaimedGhParam;
63 pub use super::steps::github::GhParam;
64 pub use super::steps::github::GhPermission;
65 pub use super::steps::github::GhPermissionValue;
66 pub use super::steps::rust::RustRuntimeServices;
67 pub use crate::flowey_config;
68 pub use crate::flowey_request;
69 pub use crate::new_flow_node;
70 pub use crate::new_flow_node_with_config;
71 pub use crate::new_simple_flow_node;
72 pub use crate::node::FlowPlatformLinuxDistro;
73 pub use crate::pipeline::Artifact;
74 pub use crate::pipeline::ArtifactType;
75
76 pub fn same_across_all_reqs<T: PartialEq>(
108 req_name: &str,
109 var: &mut Option<T>,
110 new: T,
111 ) -> anyhow::Result<()> {
112 match (var.as_ref(), new) {
113 (None, v) => *var = Some(v),
114 (Some(old), new) => {
115 if *old != new {
116 anyhow::bail!("`{}` must be consistent across requests", req_name);
117 }
118 }
119 }
120
121 Ok(())
122 }
123
124 pub fn same_across_all_reqs_backing_var<V: VarEqBacking>(
128 req_name: &str,
129 var: &mut Option<V>,
130 new: V,
131 ) -> anyhow::Result<()> {
132 match (var.as_ref(), new) {
133 (None, v) => *var = Some(v),
134 (Some(old), new) => {
135 if !old.eq(&new) {
136 anyhow::bail!("`{}` must be consistent across requests", req_name);
137 }
138 }
139 }
140
141 Ok(())
142 }
143
144 #[macro_export]
148 macro_rules! match_arch {
149 ($host_arch:expr, $match_arch:pat, $expr:expr) => {
150 if matches!($host_arch, $match_arch) {
151 $expr
152 } else {
153 anyhow::bail!("Linux distro not supported on host arch {}", $host_arch);
154 }
155 };
156 }
157
158 #[macro_export]
160 macro_rules! claim_vars {
161 ($ctx:ident, ($($var:ident),* $(,)?)) => {
162 $(let $var = $var.claim($ctx);)*
163 };
164 }
165
166 #[macro_export]
168 macro_rules! read_vars {
169 ($rt:ident, ($($var:ident),* $(,)?)) => {
170 $(let $var = $rt.read($var);)*
171 };
172 }
173}
174
175pub trait VarEqBacking {
199 fn eq(&self, other: &Self) -> bool;
201}
202
203impl<T> VarEqBacking for WriteVar<T>
204where
205 T: Serialize + DeserializeOwned,
206{
207 fn eq(&self, other: &Self) -> bool {
208 self.backing_var == other.backing_var
209 }
210}
211
212impl<T> VarEqBacking for ReadVar<T>
213where
214 T: Serialize + DeserializeOwned + PartialEq + Eq + Clone,
215{
216 fn eq(&self, other: &Self) -> bool {
217 self.backing_var == other.backing_var
218 }
219}
220
221impl<T, U> VarEqBacking for (T, U)
223where
224 T: VarEqBacking,
225 U: VarEqBacking,
226{
227 fn eq(&self, other: &Self) -> bool {
228 (self.0.eq(&other.0)) && (self.1.eq(&other.1))
229 }
230}
231
232#[derive(Serialize, Deserialize)]
250#[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))]
251pub struct ConfigVar<T>(pub ReadVar<T>);
252
253impl<T: Serialize + DeserializeOwned> Clone for ConfigVar<T> {
254 fn clone(&self) -> Self {
255 ConfigVar(self.0.clone())
256 }
257}
258
259impl<T> std::fmt::Debug for ConfigVar<T> {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 f.debug_tuple("ConfigVar").finish()
262 }
263}
264
265impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> PartialEq for ConfigVar<T> {
266 fn eq(&self, other: &Self) -> bool {
267 VarEqBacking::eq(&self.0, &other.0)
268 }
269}
270
271impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> ClaimVar for ConfigVar<T> {
272 type Claimed = ClaimedReadVar<T>;
273
274 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
275 self.0.claim(ctx)
276 }
277}
278
279impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> From<ReadVar<T>> for ConfigVar<T> {
280 fn from(v: ReadVar<T>) -> Self {
281 ConfigVar(v)
282 }
283}
284
285pub type SideEffect = ();
292
293#[derive(Clone, Debug, Serialize, Deserialize)]
296pub enum VarNotClaimed {}
297
298#[derive(Clone, Debug, Serialize, Deserialize)]
301pub enum VarClaimed {}
302
303#[derive(Debug, Serialize, Deserialize)]
323pub struct WriteVar<T: Serialize + DeserializeOwned, C = VarNotClaimed> {
324 backing_var: String,
325 is_side_effect: bool,
328
329 #[serde(skip)]
330 _kind: core::marker::PhantomData<(T, C)>,
331}
332
333pub type ClaimedWriteVar<T> = WriteVar<T, VarClaimed>;
336
337impl<T: Serialize + DeserializeOwned> WriteVar<T, VarNotClaimed> {
338 fn into_claimed(self) -> WriteVar<T, VarClaimed> {
340 let Self {
341 backing_var,
342 is_side_effect,
343 _kind,
344 } = self;
345
346 WriteVar {
347 backing_var,
348 is_side_effect,
349 _kind: std::marker::PhantomData,
350 }
351 }
352
353 #[track_caller]
355 pub fn write_static(self, ctx: &mut NodeCtx<'_>, val: T)
356 where
357 T: 'static,
358 {
359 let val = ReadVar::from_static(val);
360 val.write_into(ctx, self);
361 }
362
363 pub(crate) fn into_json(self) -> WriteVar<serde_json::Value> {
364 WriteVar {
365 backing_var: self.backing_var,
366 is_side_effect: self.is_side_effect,
367 _kind: std::marker::PhantomData,
368 }
369 }
370}
371
372impl WriteVar<SideEffect, VarNotClaimed> {
373 pub fn discard_result<T: Serialize + DeserializeOwned>(self) -> WriteVar<T> {
378 WriteVar {
379 backing_var: self.backing_var,
380 is_side_effect: true,
381 _kind: std::marker::PhantomData,
382 }
383 }
384}
385
386pub trait ClaimVar {
394 type Claimed;
396 fn claim(self, ctx: &mut StepCtx<'_>) -> Self::Claimed;
398}
399
400pub trait ReadVarValue {
406 type Value;
408 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value;
410}
411
412impl<T: Serialize + DeserializeOwned> ClaimVar for ReadVar<T> {
413 type Claimed = ClaimedReadVar<T>;
414
415 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
416 if let ReadVarBacking::RuntimeVar {
417 var,
418 is_side_effect: _,
419 } = &self.backing_var
420 {
421 ctx.backend.borrow_mut().on_claimed_runtime_var(var, true);
422 }
423 self.into_claimed()
424 }
425}
426
427impl<T: Serialize + DeserializeOwned> ClaimVar for WriteVar<T> {
428 type Claimed = ClaimedWriteVar<T>;
429
430 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedWriteVar<T> {
431 ctx.backend
432 .borrow_mut()
433 .on_claimed_runtime_var(&self.backing_var, false);
434 self.into_claimed()
435 }
436}
437
438impl<T: Serialize + DeserializeOwned> ReadVarValue for ClaimedReadVar<T> {
439 type Value = T;
440
441 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
442 match self.backing_var {
443 ReadVarBacking::RuntimeVar {
444 var,
445 is_side_effect,
446 } => {
447 let data = rt.get_var(&var, is_side_effect);
449 if is_side_effect {
450 serde_json::from_slice(b"null").expect("should be deserializing into ()")
454 } else {
455 serde_json::from_slice(&data).expect("improve this error path")
457 }
458 }
459 ReadVarBacking::Inline(val) => val,
460 }
461 }
462}
463
464impl<T: ClaimVar> ClaimVar for Vec<T> {
465 type Claimed = Vec<T::Claimed>;
466
467 fn claim(self, ctx: &mut StepCtx<'_>) -> Vec<T::Claimed> {
468 self.into_iter().map(|v| v.claim(ctx)).collect()
469 }
470}
471
472impl<T: ReadVarValue> ReadVarValue for Vec<T> {
473 type Value = Vec<T::Value>;
474
475 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
476 self.into_iter().map(|v| v.read_value(rt)).collect()
477 }
478}
479
480impl<T: ClaimVar> ClaimVar for Option<T> {
481 type Claimed = Option<T::Claimed>;
482
483 fn claim(self, ctx: &mut StepCtx<'_>) -> Option<T::Claimed> {
484 self.map(|x| x.claim(ctx))
485 }
486}
487
488impl<T: ReadVarValue> ReadVarValue for Option<T> {
489 type Value = Option<T::Value>;
490
491 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
492 self.map(|x| x.read_value(rt))
493 }
494}
495
496impl<U: Ord, T: ClaimVar> ClaimVar for BTreeMap<U, T> {
497 type Claimed = BTreeMap<U, T::Claimed>;
498
499 fn claim(self, ctx: &mut StepCtx<'_>) -> BTreeMap<U, T::Claimed> {
500 self.into_iter().map(|(k, v)| (k, v.claim(ctx))).collect()
501 }
502}
503
504impl<U: Ord, T: ReadVarValue> ReadVarValue for BTreeMap<U, T> {
505 type Value = BTreeMap<U, T::Value>;
506
507 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
508 self.into_iter()
509 .map(|(k, v)| (k, v.read_value(rt)))
510 .collect()
511 }
512}
513
514macro_rules! impl_tuple_claim {
515 ($($T:tt)*) => {
516 impl<$($T,)*> $crate::node::ClaimVar for ($($T,)*)
517 where
518 $($T: $crate::node::ClaimVar,)*
519 {
520 type Claimed = ($($T::Claimed,)*);
521
522 #[expect(non_snake_case)]
523 fn claim(self, ctx: &mut $crate::node::StepCtx<'_>) -> Self::Claimed {
524 let ($($T,)*) = self;
525 ($($T.claim(ctx),)*)
526 }
527 }
528
529 impl<$($T,)*> $crate::node::ReadVarValue for ($($T,)*)
530 where
531 $($T: $crate::node::ReadVarValue,)*
532 {
533 type Value = ($($T::Value,)*);
534
535 #[expect(non_snake_case)]
536 fn read_value(self, rt: &mut $crate::node::RustRuntimeServices<'_>) -> Self::Value {
537 let ($($T,)*) = self;
538 ($($T.read_value(rt),)*)
539 }
540 }
541 };
542}
543
544impl_tuple_claim!(A B C D E F G H I J);
545impl_tuple_claim!(A B C D E F G H I);
546impl_tuple_claim!(A B C D E F G H);
547impl_tuple_claim!(A B C D E F G);
548impl_tuple_claim!(A B C D E F);
549impl_tuple_claim!(A B C D E);
550impl_tuple_claim!(A B C D);
551impl_tuple_claim!(A B C);
552impl_tuple_claim!(A B);
553impl_tuple_claim!(A);
554
555impl ClaimVar for () {
556 type Claimed = ();
557
558 fn claim(self, _ctx: &mut StepCtx<'_>) -> Self::Claimed {}
559}
560
561impl ReadVarValue for () {
562 type Value = ();
563
564 fn read_value(self, _rt: &mut RustRuntimeServices<'_>) -> Self::Value {}
565}
566
567#[derive(Serialize, Deserialize, Clone)]
572pub struct GhUserSecretVar(pub(crate) String);
573
574#[derive(Debug, Serialize, Deserialize)]
593pub struct ReadVar<T, C = VarNotClaimed> {
594 backing_var: ReadVarBacking<T>,
595 #[serde(skip)]
596 _kind: std::marker::PhantomData<C>,
597}
598
599pub type ClaimedReadVar<T> = ReadVar<T, VarClaimed>;
602
603impl<T: Serialize + DeserializeOwned, C> Clone for ReadVar<T, C> {
605 fn clone(&self) -> Self {
606 ReadVar {
607 backing_var: self.backing_var.clone(),
608 _kind: std::marker::PhantomData,
609 }
610 }
611}
612
613#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
614enum ReadVarBacking<T> {
615 RuntimeVar {
616 var: String,
617 is_side_effect: bool,
624 },
625 Inline(T),
626}
627
628impl<T: Serialize + DeserializeOwned> Clone for ReadVarBacking<T> {
630 fn clone(&self) -> Self {
631 match self {
632 Self::RuntimeVar {
633 var,
634 is_side_effect,
635 } => Self::RuntimeVar {
636 var: var.clone(),
637 is_side_effect: *is_side_effect,
638 },
639 Self::Inline(v) => {
640 Self::Inline(serde_json::from_value(serde_json::to_value(v).unwrap()).unwrap())
641 }
642 }
643 }
644}
645
646impl<T: Serialize + DeserializeOwned> ReadVar<T> {
647 fn into_claimed(self) -> ReadVar<T, VarClaimed> {
649 let Self { backing_var, _kind } = self;
650
651 ReadVar {
652 backing_var,
653 _kind: std::marker::PhantomData,
654 }
655 }
656
657 #[must_use]
666 pub fn into_side_effect(self) -> ReadVar<SideEffect> {
667 ReadVar {
668 backing_var: match self.backing_var {
669 ReadVarBacking::RuntimeVar {
670 var,
671 is_side_effect: _,
672 } => ReadVarBacking::RuntimeVar {
673 var,
674 is_side_effect: true,
675 },
676 ReadVarBacking::Inline(_) => ReadVarBacking::Inline(()),
677 },
678 _kind: std::marker::PhantomData,
679 }
680 }
681
682 #[track_caller]
685 #[must_use]
686 pub fn map<F, U>(&self, ctx: &mut NodeCtx<'_>, f: F) -> ReadVar<U>
687 where
688 T: 'static,
689 U: Serialize + DeserializeOwned + 'static,
690 F: FnOnce(T) -> U + 'static,
691 {
692 let (read_from, write_into) = ctx.new_var();
693 self.write_into_with(ctx, write_into, f);
694 read_from
695 }
696
697 #[track_caller]
700 pub fn write_into_with<F, U>(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<U>, f: F)
701 where
702 T: 'static,
703 U: Serialize + DeserializeOwned + 'static,
704 F: FnOnce(T) -> U + 'static,
705 {
706 let this = self.clone();
707 ctx.emit_minor_rust_step("🌼 write_into Var", move |ctx| {
708 let this = this.claim(ctx);
709 let write_into = write_into.claim(ctx);
710 move |rt| {
711 let this = rt.read(this);
712 rt.write(write_into, &f(this));
713 }
714 });
715 }
716
717 #[track_caller]
719 pub fn write_into(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<T>)
720 where
721 T: 'static,
722 {
723 self.write_into_with(ctx, write_into, |x| x);
724 }
725
726 #[track_caller]
729 #[must_use]
730 pub fn zip<U>(&self, ctx: &mut NodeCtx<'_>, other: ReadVar<U>) -> ReadVar<(T, U)>
731 where
732 T: 'static,
733 U: Serialize + DeserializeOwned + 'static,
734 {
735 let (read_from, write_into) = ctx.new_var();
736 let this = self.clone();
737 ctx.emit_minor_rust_step("🌼 Zip Vars", move |ctx| {
738 let this = this.claim(ctx);
739 let other = other.claim(ctx);
740 let write_into = write_into.claim(ctx);
741 move |rt| {
742 let this = rt.read(this);
743 let other = rt.read(other);
744 rt.write(write_into, &(this, other));
745 }
746 });
747 read_from
748 }
749
750 #[track_caller]
755 #[must_use]
756 pub fn from_static(val: T) -> ReadVar<T>
757 where
758 T: 'static,
759 {
760 ReadVar {
761 backing_var: ReadVarBacking::Inline(val),
762 _kind: std::marker::PhantomData,
763 }
764 }
765
766 pub fn get_static(&self) -> Option<T> {
775 match self.clone().backing_var {
776 ReadVarBacking::Inline(v) => Some(v),
777 _ => None,
778 }
779 }
780
781 #[track_caller]
783 #[must_use]
784 pub fn transpose_vec(ctx: &mut NodeCtx<'_>, vec: Vec<ReadVar<T>>) -> ReadVar<Vec<T>>
785 where
786 T: 'static,
787 {
788 let (read_from, write_into) = ctx.new_var();
789 ctx.emit_minor_rust_step("🌼 Transpose Vec<ReadVar<T>>", move |ctx| {
790 let vec = vec.claim(ctx);
791 let write_into = write_into.claim(ctx);
792 move |rt| {
793 let mut v = Vec::new();
794 for var in vec {
795 v.push(rt.read(var));
796 }
797 rt.write(write_into, &v);
798 }
799 });
800 read_from
801 }
802
803 #[must_use]
819 pub fn depending_on<U>(&self, ctx: &mut NodeCtx<'_>, other: &ReadVar<U>) -> Self
820 where
821 T: 'static,
822 U: Serialize + DeserializeOwned + 'static,
823 {
824 ctx.emit_minor_rust_stepv("🌼 Add dependency", |ctx| {
827 let this = self.clone().claim(ctx);
828 other.clone().claim(ctx);
829 move |rt| rt.read(this)
830 })
831 }
832
833 pub fn claim_unused(self, ctx: &mut NodeCtx<'_>) {
836 match self.backing_var {
837 ReadVarBacking::RuntimeVar {
838 var,
839 is_side_effect: _,
840 } => ctx.backend.borrow_mut().on_unused_read_var(&var),
841 ReadVarBacking::Inline(_) => {}
842 }
843 }
844
845 pub(crate) fn into_json(self) -> ReadVar<serde_json::Value> {
846 match self.backing_var {
847 ReadVarBacking::RuntimeVar {
848 var,
849 is_side_effect,
850 } => ReadVar {
851 backing_var: ReadVarBacking::RuntimeVar {
852 var,
853 is_side_effect,
854 },
855 _kind: std::marker::PhantomData,
856 },
857 ReadVarBacking::Inline(v) => ReadVar {
858 backing_var: ReadVarBacking::Inline(serde_json::to_value(v).unwrap()),
859 _kind: std::marker::PhantomData,
860 },
861 }
862 }
863}
864
865#[must_use]
871pub fn thin_air_read_runtime_var<T>(backing_var: String) -> ReadVar<T>
872where
873 T: Serialize + DeserializeOwned,
874{
875 ReadVar {
876 backing_var: ReadVarBacking::RuntimeVar {
877 var: backing_var,
878 is_side_effect: false,
879 },
880 _kind: std::marker::PhantomData,
881 }
882}
883
884#[must_use]
890pub fn thin_air_write_runtime_var<T>(backing_var: String) -> WriteVar<T>
891where
892 T: Serialize + DeserializeOwned,
893{
894 WriteVar {
895 backing_var,
896 is_side_effect: false,
897 _kind: std::marker::PhantomData,
898 }
899}
900
901pub fn read_var_internals<T: Serialize + DeserializeOwned, C>(
907 var: &ReadVar<T, C>,
908) -> (Option<String>, bool) {
909 match var.backing_var {
910 ReadVarBacking::RuntimeVar {
911 var: ref s,
912 is_side_effect,
913 } => (Some(s.clone()), is_side_effect),
914 ReadVarBacking::Inline(_) => (None, false),
915 }
916}
917
918pub trait ImportCtxBackend {
919 fn on_possible_dep(&mut self, node_handle: NodeHandle);
920}
921
922pub struct ImportCtx<'a> {
924 backend: &'a mut dyn ImportCtxBackend,
925}
926
927impl ImportCtx<'_> {
928 pub fn import<N: FlowNodeBase + 'static>(&mut self) {
930 self.backend.on_possible_dep(NodeHandle::from_type::<N>())
931 }
932}
933
934pub fn new_import_ctx(backend: &mut dyn ImportCtxBackend) -> ImportCtx<'_> {
935 ImportCtx { backend }
936}
937
938#[derive(Debug)]
939pub enum CtxAnchor {
940 PostJob,
941}
942
943pub trait NodeCtxBackend {
944 fn current_node(&self) -> NodeHandle;
946
947 fn on_new_var(&mut self) -> String;
952
953 fn on_claimed_runtime_var(&mut self, var: &str, is_read: bool);
955
956 fn on_unused_read_var(&mut self, var: &str);
958
959 fn on_request(&mut self, node_handle: NodeHandle, req: anyhow::Result<Box<[u8]>>);
967
968 fn on_config(&mut self, node_handle: NodeHandle, config: anyhow::Result<Box<[u8]>>);
972
973 fn on_emit_rust_step(
974 &mut self,
975 label: &str,
976 can_merge: bool,
977 code: Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
978 );
979
980 fn on_emit_ado_step(
981 &mut self,
982 label: &str,
983 yaml_snippet: Box<dyn for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String>,
984 inline_script: Option<
985 Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
986 >,
987 condvar: Option<String>,
988 );
989
990 fn on_emit_gh_step(
991 &mut self,
992 label: &str,
993 uses: &str,
994 with: BTreeMap<String, ClaimedGhParam>,
995 condvar: Option<String>,
996 outputs: BTreeMap<String, Vec<GhOutput>>,
997 permissions: BTreeMap<GhPermission, GhPermissionValue>,
998 gh_to_rust: Vec<GhToRust>,
999 rust_to_gh: Vec<RustToGh>,
1000 );
1001
1002 fn on_emit_side_effect_step(&mut self);
1003
1004 fn backend(&mut self) -> FlowBackend;
1005 fn platform(&mut self) -> FlowPlatform;
1006 fn arch(&mut self) -> FlowArch;
1007
1008 fn persistent_dir_path_var(&mut self) -> Option<String>;
1012}
1013
1014pub fn new_node_ctx(backend: &mut dyn NodeCtxBackend) -> NodeCtx<'_> {
1015 NodeCtx {
1016 backend: Rc::new(RefCell::new(backend)),
1017 }
1018}
1019
1020#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1022pub enum FlowBackend {
1023 Local,
1025 Ado,
1027 Github,
1029}
1030
1031#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1033pub enum FlowPlatformKind {
1034 Windows,
1035 Unix,
1036}
1037
1038#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1040pub enum FlowPlatformLinuxDistro {
1041 Fedora,
1043 Ubuntu,
1045 AzureLinux,
1047 Arch,
1049 Nix,
1051 Unknown,
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1057#[non_exhaustive]
1058pub enum FlowPlatform {
1059 Windows,
1061 Linux(FlowPlatformLinuxDistro),
1063 MacOs,
1065}
1066
1067impl FlowPlatform {
1068 pub fn kind(&self) -> FlowPlatformKind {
1069 match self {
1070 Self::Windows => FlowPlatformKind::Windows,
1071 Self::Linux(_) | Self::MacOs => FlowPlatformKind::Unix,
1072 }
1073 }
1074
1075 fn as_str(&self) -> &'static str {
1076 match self {
1077 Self::Windows => "windows",
1078 Self::Linux(_) => "linux",
1079 Self::MacOs => "macos",
1080 }
1081 }
1082
1083 pub fn exe_suffix(&self) -> &'static str {
1085 if self == &Self::Windows { ".exe" } else { "" }
1086 }
1087
1088 pub fn binary(&self, name: &str) -> String {
1090 format!("{}{}", name, self.exe_suffix())
1091 }
1092}
1093
1094impl std::fmt::Display for FlowPlatform {
1095 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1096 f.pad(self.as_str())
1097 }
1098}
1099
1100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1102#[non_exhaustive]
1103pub enum FlowArch {
1104 X86_64,
1105 Aarch64,
1106}
1107
1108impl FlowArch {
1109 fn as_str(&self) -> &'static str {
1110 match self {
1111 Self::X86_64 => "x86_64",
1112 Self::Aarch64 => "aarch64",
1113 }
1114 }
1115}
1116
1117impl std::fmt::Display for FlowArch {
1118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1119 f.pad(self.as_str())
1120 }
1121}
1122
1123pub struct StepCtx<'a> {
1125 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1126}
1127
1128impl StepCtx<'_> {
1129 pub fn backend(&self) -> FlowBackend {
1132 self.backend.borrow_mut().backend()
1133 }
1134
1135 pub fn platform(&self) -> FlowPlatform {
1138 self.backend.borrow_mut().platform()
1139 }
1140}
1141
1142const NO_ADO_INLINE_SCRIPT: Option<
1143 for<'a> fn(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>,
1144> = None;
1145
1146pub struct NodeCtx<'a> {
1148 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1149}
1150
1151impl<'ctx> NodeCtx<'ctx> {
1152 pub fn emit_rust_step<F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<SideEffect>
1158 where
1159 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1160 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1161 {
1162 self.emit_rust_step_inner(label.as_ref(), false, code)
1163 }
1164
1165 pub fn emit_minor_rust_step<F, G>(
1171 &mut self,
1172 label: impl AsRef<str>,
1173 code: F,
1174 ) -> ReadVar<SideEffect>
1175 where
1176 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1177 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) + 'static,
1178 {
1179 self.emit_rust_step_inner(label.as_ref(), true, |ctx| {
1180 let f = code(ctx);
1181 |rt| {
1182 f(rt);
1183 Ok(())
1184 }
1185 })
1186 }
1187
1188 #[must_use]
1209 #[track_caller]
1210 pub fn emit_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1211 where
1212 T: Serialize + DeserializeOwned + 'static,
1213 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1214 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1215 {
1216 self.emit_rust_stepv_inner(label.as_ref(), false, code)
1217 }
1218
1219 #[must_use]
1243 #[track_caller]
1244 pub fn emit_minor_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1245 where
1246 T: Serialize + DeserializeOwned + 'static,
1247 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1248 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> T + 'static,
1249 {
1250 self.emit_rust_stepv_inner(label.as_ref(), true, |ctx| {
1251 let f = code(ctx);
1252 |rt| Ok(f(rt))
1253 })
1254 }
1255
1256 fn emit_rust_step_inner<F, G>(
1257 &mut self,
1258 label: &str,
1259 can_merge: bool,
1260 code: F,
1261 ) -> ReadVar<SideEffect>
1262 where
1263 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1264 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1265 {
1266 let (read, write) = self.new_prefixed_var("auto_se");
1267
1268 let ctx = &mut StepCtx {
1269 backend: self.backend.clone(),
1270 };
1271 write.claim(ctx);
1272
1273 let code = code(ctx);
1274 self.backend
1275 .borrow_mut()
1276 .on_emit_rust_step(label.as_ref(), can_merge, Box::new(code));
1277 read
1278 }
1279
1280 #[must_use]
1281 #[track_caller]
1282 fn emit_rust_stepv_inner<T, F, G>(
1283 &mut self,
1284 label: impl AsRef<str>,
1285 can_merge: bool,
1286 code: F,
1287 ) -> ReadVar<T>
1288 where
1289 T: Serialize + DeserializeOwned + 'static,
1290 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1291 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1292 {
1293 let (read, write) = self.new_var();
1294
1295 let ctx = &mut StepCtx {
1296 backend: self.backend.clone(),
1297 };
1298 let write = write.claim(ctx);
1299
1300 let code = code(ctx);
1301 self.backend.borrow_mut().on_emit_rust_step(
1302 label.as_ref(),
1303 can_merge,
1304 Box::new(|rt| {
1305 let val = code(rt)?;
1306 rt.write(write, &val);
1307 Ok(())
1308 }),
1309 );
1310 read
1311 }
1312
1313 #[track_caller]
1315 #[must_use]
1316 pub fn get_ado_variable(&mut self, ado_var: AdoRuntimeVar) -> ReadVar<String> {
1317 let (var, write_var) = self.new_var();
1318 self.emit_ado_step(format!("🌼 read {}", ado_var.as_raw_var_name()), |ctx| {
1319 let write_var = write_var.claim(ctx);
1320 |rt| {
1321 rt.set_var(write_var, ado_var);
1322 "".into()
1323 }
1324 });
1325 var
1326 }
1327
1328 pub fn emit_ado_step<F, G>(&mut self, display_name: impl AsRef<str>, yaml_snippet: F)
1330 where
1331 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1332 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1333 {
1334 self.emit_ado_step_inner(display_name, None, |ctx| {
1335 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1336 })
1337 }
1338
1339 pub fn emit_ado_step_with_condition<F, G>(
1342 &mut self,
1343 display_name: impl AsRef<str>,
1344 cond: ReadVar<bool>,
1345 yaml_snippet: F,
1346 ) where
1347 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1348 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1349 {
1350 self.emit_ado_step_inner(display_name, Some(cond), |ctx| {
1351 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1352 })
1353 }
1354
1355 pub fn emit_ado_step_with_condition_optional<F, G>(
1358 &mut self,
1359 display_name: impl AsRef<str>,
1360 cond: Option<ReadVar<bool>>,
1361 yaml_snippet: F,
1362 ) where
1363 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1364 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1365 {
1366 self.emit_ado_step_inner(display_name, cond, |ctx| {
1367 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1368 })
1369 }
1370
1371 pub fn emit_ado_step_with_inline_script<F, G, H>(
1400 &mut self,
1401 display_name: impl AsRef<str>,
1402 yaml_snippet: F,
1403 ) where
1404 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, H),
1405 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1406 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1407 {
1408 self.emit_ado_step_inner(display_name, None, |ctx| {
1409 let (f, g) = yaml_snippet(ctx);
1410 (f, Some(g))
1411 })
1412 }
1413
1414 fn emit_ado_step_inner<F, G, H>(
1415 &mut self,
1416 display_name: impl AsRef<str>,
1417 cond: Option<ReadVar<bool>>,
1418 yaml_snippet: F,
1419 ) where
1420 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, Option<H>),
1421 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1422 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1423 {
1424 let condvar = match cond.map(|c| c.backing_var) {
1425 Some(ReadVarBacking::Inline(cond)) => {
1427 if !cond {
1428 return;
1429 } else {
1430 None
1431 }
1432 }
1433 Some(ReadVarBacking::RuntimeVar {
1434 var,
1435 is_side_effect,
1436 }) => {
1437 assert!(!is_side_effect);
1438 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1439 Some(var)
1440 }
1441 None => None,
1442 };
1443
1444 let (yaml_snippet, inline_script) = yaml_snippet(&mut StepCtx {
1445 backend: self.backend.clone(),
1446 });
1447 self.backend.borrow_mut().on_emit_ado_step(
1448 display_name.as_ref(),
1449 Box::new(yaml_snippet),
1450 if let Some(inline_script) = inline_script {
1451 Some(Box::new(inline_script))
1452 } else {
1453 None
1454 },
1455 condvar,
1456 );
1457 }
1458
1459 #[track_caller]
1461 #[must_use]
1462 pub fn get_gh_context_var(&mut self) -> GhContextVarReader<'ctx, Root> {
1463 GhContextVarReader {
1464 ctx: NodeCtx {
1465 backend: self.backend.clone(),
1466 },
1467 _state: std::marker::PhantomData,
1468 }
1469 }
1470
1471 pub fn emit_gh_step(
1473 &mut self,
1474 display_name: impl AsRef<str>,
1475 uses: impl AsRef<str>,
1476 ) -> GhStepBuilder {
1477 GhStepBuilder::new(display_name, uses)
1478 }
1479
1480 fn emit_gh_step_inner(
1481 &mut self,
1482 display_name: impl AsRef<str>,
1483 cond: Option<ReadVar<bool>>,
1484 uses: impl AsRef<str>,
1485 with: Option<BTreeMap<String, GhParam>>,
1486 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
1487 run_after: Vec<ReadVar<SideEffect>>,
1488 permissions: BTreeMap<GhPermission, GhPermissionValue>,
1489 ) {
1490 let condvar = match cond.map(|c| c.backing_var) {
1491 Some(ReadVarBacking::Inline(cond)) => {
1493 if !cond {
1494 return;
1495 } else {
1496 None
1497 }
1498 }
1499 Some(ReadVarBacking::RuntimeVar {
1500 var,
1501 is_side_effect,
1502 }) => {
1503 assert!(!is_side_effect);
1504 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1505 Some(var)
1506 }
1507 None => None,
1508 };
1509
1510 let with = with
1511 .unwrap_or_default()
1512 .into_iter()
1513 .map(|(k, v)| {
1514 (
1515 k.clone(),
1516 v.claim(&mut StepCtx {
1517 backend: self.backend.clone(),
1518 }),
1519 )
1520 })
1521 .collect();
1522
1523 for var in run_after {
1524 var.claim(&mut StepCtx {
1525 backend: self.backend.clone(),
1526 });
1527 }
1528
1529 let outputvars = outputs
1530 .into_iter()
1531 .map(|(name, vars)| {
1532 (
1533 name,
1534 vars.into_iter()
1535 .map(|var| {
1536 let var = var.claim(&mut StepCtx {
1537 backend: self.backend.clone(),
1538 });
1539 GhOutput {
1540 backing_var: var.backing_var,
1541 is_secret: false,
1542 is_object: false,
1543 }
1544 })
1545 .collect(),
1546 )
1547 })
1548 .collect();
1549
1550 self.backend.borrow_mut().on_emit_gh_step(
1551 display_name.as_ref(),
1552 uses.as_ref(),
1553 with,
1554 condvar,
1555 outputvars,
1556 permissions,
1557 Vec::new(),
1558 Vec::new(),
1559 );
1560 }
1561
1562 pub fn emit_side_effect_step(
1570 &mut self,
1571 use_side_effects: impl IntoIterator<Item = ReadVar<SideEffect>>,
1572 resolve_side_effects: impl IntoIterator<Item = WriteVar<SideEffect>>,
1573 ) {
1574 let mut backend = self.backend.borrow_mut();
1575 for var in use_side_effects.into_iter() {
1576 if let ReadVarBacking::RuntimeVar {
1577 var,
1578 is_side_effect: _,
1579 } = &var.backing_var
1580 {
1581 backend.on_claimed_runtime_var(var, true);
1582 }
1583 }
1584
1585 for var in resolve_side_effects.into_iter() {
1586 backend.on_claimed_runtime_var(&var.backing_var, false);
1587 }
1588
1589 backend.on_emit_side_effect_step();
1590 }
1591
1592 pub fn backend(&self) -> FlowBackend {
1595 self.backend.borrow_mut().backend()
1596 }
1597
1598 pub fn platform(&self) -> FlowPlatform {
1601 self.backend.borrow_mut().platform()
1602 }
1603
1604 pub fn arch(&self) -> FlowArch {
1606 self.backend.borrow_mut().arch()
1607 }
1608
1609 pub fn req<R>(&mut self, req: R)
1611 where
1612 R: IntoRequest + 'static,
1613 {
1614 let mut backend = self.backend.borrow_mut();
1615 backend.on_request(
1616 NodeHandle::from_type::<R::Node>(),
1617 serde_json::to_vec(&req.into_request())
1618 .map(Into::into)
1619 .map_err(Into::into),
1620 );
1621 }
1622
1623 pub fn config<C>(&mut self, config: C)
1628 where
1629 C: IntoConfig + 'static,
1630 {
1631 let mut backend = self.backend.borrow_mut();
1632 backend.on_config(
1633 NodeHandle::from_type::<C::Node>(),
1634 serde_json::to_vec(&config)
1635 .map(Into::into)
1636 .map_err(Into::into),
1637 );
1638 }
1639
1640 #[track_caller]
1643 #[must_use]
1644 pub fn reqv<T, R>(&mut self, f: impl FnOnce(WriteVar<T>) -> R) -> ReadVar<T>
1645 where
1646 T: Serialize + DeserializeOwned,
1647 R: IntoRequest + 'static,
1648 {
1649 let (read, write) = self.new_var();
1650 self.req::<R>(f(write));
1651 read
1652 }
1653
1654 pub fn requests<N>(&mut self, reqs: impl IntoIterator<Item = N::Request>)
1656 where
1657 N: FlowNodeBase + 'static,
1658 {
1659 let mut backend = self.backend.borrow_mut();
1660 for req in reqs.into_iter() {
1661 backend.on_request(
1662 NodeHandle::from_type::<N>(),
1663 serde_json::to_vec(&req).map(Into::into).map_err(Into::into),
1664 );
1665 }
1666 }
1667
1668 #[track_caller]
1671 #[must_use]
1672 pub fn new_var<T>(&self) -> (ReadVar<T>, WriteVar<T>)
1673 where
1674 T: Serialize + DeserializeOwned,
1675 {
1676 self.new_prefixed_var("")
1677 }
1678
1679 #[track_caller]
1680 #[must_use]
1681 fn new_prefixed_var<T>(&self, prefix: &'static str) -> (ReadVar<T>, WriteVar<T>)
1682 where
1683 T: Serialize + DeserializeOwned,
1684 {
1685 let caller = std::panic::Location::caller()
1687 .to_string()
1688 .replace('\\', "/");
1689
1690 let caller = caller
1706 .split_once("flowey/")
1707 .expect("due to a known limitation with flowey, all flowey code must have an ancestor dir called 'flowey/' somewhere in its full path")
1708 .1;
1709
1710 let colon = if prefix.is_empty() { "" } else { ":" };
1711 let ordinal = self.backend.borrow_mut().on_new_var();
1712 let backing_var = format!("{prefix}{colon}{ordinal}:{caller}");
1713
1714 (
1715 ReadVar {
1716 backing_var: ReadVarBacking::RuntimeVar {
1717 var: backing_var.clone(),
1718 is_side_effect: false,
1719 },
1720 _kind: std::marker::PhantomData,
1721 },
1722 WriteVar {
1723 backing_var,
1724 is_side_effect: false,
1725 _kind: std::marker::PhantomData,
1726 },
1727 )
1728 }
1729
1730 #[track_caller]
1741 #[must_use]
1742 pub fn new_post_job_side_effect(&self) -> (ReadVar<SideEffect>, WriteVar<SideEffect>) {
1743 self.new_prefixed_var("post_job")
1744 }
1745
1746 #[track_caller]
1759 #[must_use]
1760 pub fn persistent_dir(&mut self) -> Option<ReadVar<PathBuf>> {
1761 let path: ReadVar<PathBuf> = ReadVar {
1762 backing_var: ReadVarBacking::RuntimeVar {
1763 var: self.backend.borrow_mut().persistent_dir_path_var()?,
1764 is_side_effect: false,
1765 },
1766 _kind: std::marker::PhantomData,
1767 };
1768
1769 let folder_name = self
1770 .backend
1771 .borrow_mut()
1772 .current_node()
1773 .modpath()
1774 .replace("::", "__");
1775
1776 Some(
1777 self.emit_rust_stepv("🌼 Create persistent store dir", |ctx| {
1778 let path = path.claim(ctx);
1779 |rt| {
1780 let dir = rt.read(path).join(folder_name);
1781 fs_err::create_dir_all(&dir)?;
1782 Ok(dir)
1783 }
1784 }),
1785 )
1786 }
1787
1788 pub fn supports_persistent_dir(&mut self) -> bool {
1790 self.backend
1791 .borrow_mut()
1792 .persistent_dir_path_var()
1793 .is_some()
1794 }
1795}
1796
1797pub trait RuntimeVarDb {
1800 fn get_var(&mut self, var_name: &str) -> (Vec<u8>, bool) {
1801 self.try_get_var(var_name)
1802 .unwrap_or_else(|| panic!("db is missing var {}", var_name))
1803 }
1804
1805 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)>;
1806 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>);
1807}
1808
1809impl RuntimeVarDb for Box<dyn RuntimeVarDb> {
1810 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)> {
1811 (**self).try_get_var(var_name)
1812 }
1813
1814 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>) {
1815 (**self).set_var(var_name, is_secret, value)
1816 }
1817}
1818
1819pub mod steps {
1820 pub mod ado {
1821 use crate::node::ClaimedReadVar;
1822 use crate::node::ClaimedWriteVar;
1823 use crate::node::ReadVarBacking;
1824 use serde::Deserialize;
1825 use serde::Serialize;
1826 use std::borrow::Cow;
1827
1828 #[derive(Debug, Clone, Serialize, Deserialize)]
1834 pub struct AdoResourcesRepositoryId {
1835 pub(crate) repo_id: String,
1836 }
1837
1838 impl AdoResourcesRepositoryId {
1839 pub fn new_self() -> Self {
1845 Self {
1846 repo_id: "self".into(),
1847 }
1848 }
1849
1850 pub fn dangerous_get_raw_id(&self) -> &str {
1856 &self.repo_id
1857 }
1858
1859 pub fn dangerous_new(repo_id: &str) -> Self {
1865 Self {
1866 repo_id: repo_id.into(),
1867 }
1868 }
1869 }
1870
1871 #[derive(Clone, Debug, Serialize, Deserialize)]
1876 pub struct AdoRuntimeVar {
1877 is_secret: bool,
1878 ado_var: Cow<'static, str>,
1879 }
1880
1881 impl AdoRuntimeVar {
1882 pub const BUILD_SOURCE_BRANCH: AdoRuntimeVar = AdoRuntimeVar::new("build.SourceBranch");
1888
1889 pub const BUILD_BUILD_NUMBER: AdoRuntimeVar = AdoRuntimeVar::new("build.BuildNumber");
1891
1892 pub const SYSTEM_ACCESS_TOKEN: AdoRuntimeVar =
1894 AdoRuntimeVar::new_secret("System.AccessToken");
1895
1896 pub const SYSTEM_JOB_ATTEMPT: AdoRuntimeVar =
1898 AdoRuntimeVar::new_secret("System.JobAttempt");
1899
1900 pub const PIPELINE_WORKSPACE: AdoRuntimeVar = AdoRuntimeVar::new("Pipeline.Workspace");
1902 }
1903
1904 impl AdoRuntimeVar {
1905 const fn new(s: &'static str) -> Self {
1906 Self {
1907 is_secret: false,
1908 ado_var: Cow::Borrowed(s),
1909 }
1910 }
1911
1912 const fn new_secret(s: &'static str) -> Self {
1913 Self {
1914 is_secret: true,
1915 ado_var: Cow::Borrowed(s),
1916 }
1917 }
1918
1919 pub fn is_secret(&self) -> bool {
1921 self.is_secret
1922 }
1923
1924 pub fn as_raw_var_name(&self) -> String {
1926 self.ado_var.as_ref().into()
1927 }
1928
1929 pub fn dangerous_from_global(ado_var_name: impl AsRef<str>, is_secret: bool) -> Self {
1937 Self {
1938 is_secret,
1939 ado_var: ado_var_name.as_ref().to_owned().into(),
1940 }
1941 }
1942 }
1943
1944 pub fn new_ado_step_services(
1945 fresh_ado_var: &mut dyn FnMut() -> String,
1946 ) -> AdoStepServices<'_> {
1947 AdoStepServices {
1948 fresh_ado_var,
1949 ado_to_rust: Vec::new(),
1950 rust_to_ado: Vec::new(),
1951 }
1952 }
1953
1954 pub struct CompletedAdoStepServices {
1955 pub ado_to_rust: Vec<(String, String, bool)>,
1956 pub rust_to_ado: Vec<(String, String)>,
1957 }
1958
1959 impl CompletedAdoStepServices {
1960 pub fn from_ado_step_services(access: AdoStepServices<'_>) -> Self {
1961 let AdoStepServices {
1962 fresh_ado_var: _,
1963 ado_to_rust,
1964 rust_to_ado,
1965 } = access;
1966
1967 Self {
1968 ado_to_rust,
1969 rust_to_ado,
1970 }
1971 }
1972 }
1973
1974 pub struct AdoStepServices<'a> {
1975 fresh_ado_var: &'a mut dyn FnMut() -> String,
1976 ado_to_rust: Vec<(String, String, bool)>,
1977 rust_to_ado: Vec<(String, String)>,
1978 }
1979
1980 impl AdoStepServices<'_> {
1981 pub fn resolve_repository_id(&self, repo_id: AdoResourcesRepositoryId) -> String {
1984 repo_id.repo_id
1985 }
1986
1987 pub fn set_var(&mut self, var: ClaimedWriteVar<String>, from_ado_var: AdoRuntimeVar) {
1993 self.ado_to_rust.push((
1994 from_ado_var.ado_var.into(),
1995 var.backing_var,
1996 from_ado_var.is_secret,
1997 ))
1998 }
1999
2000 pub fn get_var(&mut self, var: ClaimedReadVar<String>) -> AdoRuntimeVar {
2002 let backing_var = if let ReadVarBacking::RuntimeVar {
2003 var,
2004 is_side_effect,
2005 } = &var.backing_var
2006 {
2007 assert!(!is_side_effect);
2008 var
2009 } else {
2010 todo!("support inline ado read vars")
2011 };
2012
2013 let new_ado_var_name = (self.fresh_ado_var)();
2014
2015 self.rust_to_ado
2016 .push((backing_var.clone(), new_ado_var_name.clone()));
2017 AdoRuntimeVar::dangerous_from_global(new_ado_var_name, false)
2018 }
2019 }
2020 }
2021
2022 pub mod github {
2023 use crate::node::ClaimVar;
2024 use crate::node::NodeCtx;
2025 use crate::node::ReadVar;
2026 use crate::node::ReadVarBacking;
2027 use crate::node::SideEffect;
2028 use crate::node::StepCtx;
2029 use crate::node::VarClaimed;
2030 use crate::node::VarNotClaimed;
2031 use crate::node::WriteVar;
2032 use std::collections::BTreeMap;
2033
2034 pub struct GhStepBuilder {
2035 display_name: String,
2036 cond: Option<ReadVar<bool>>,
2037 uses: String,
2038 with: Option<BTreeMap<String, GhParam>>,
2039 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
2040 run_after: Vec<ReadVar<SideEffect>>,
2041 permissions: BTreeMap<GhPermission, GhPermissionValue>,
2042 }
2043
2044 impl GhStepBuilder {
2045 pub fn new(display_name: impl AsRef<str>, uses: impl AsRef<str>) -> Self {
2060 Self {
2061 display_name: display_name.as_ref().into(),
2062 cond: None,
2063 uses: uses.as_ref().into(),
2064 with: None,
2065 outputs: BTreeMap::new(),
2066 run_after: Vec::new(),
2067 permissions: BTreeMap::new(),
2068 }
2069 }
2070
2071 pub fn condition(mut self, cond: ReadVar<bool>) -> Self {
2078 self.cond = Some(cond);
2079 self
2080 }
2081
2082 pub fn with(mut self, k: impl AsRef<str>, v: impl Into<GhParam>) -> Self {
2108 self.with.get_or_insert_with(BTreeMap::new);
2109 if let Some(with) = &mut self.with {
2110 with.insert(k.as_ref().to_string(), v.into());
2111 }
2112 self
2113 }
2114
2115 pub fn output(mut self, k: impl AsRef<str>, v: WriteVar<String>) -> Self {
2124 self.outputs
2125 .entry(k.as_ref().to_string())
2126 .or_default()
2127 .push(v);
2128 self
2129 }
2130
2131 pub fn run_after(mut self, side_effect: ReadVar<SideEffect>) -> Self {
2133 self.run_after.push(side_effect);
2134 self
2135 }
2136
2137 pub fn requires_permission(
2142 mut self,
2143 perm: GhPermission,
2144 value: GhPermissionValue,
2145 ) -> Self {
2146 self.permissions.insert(perm, value);
2147 self
2148 }
2149
2150 #[track_caller]
2152 pub fn finish(self, ctx: &mut NodeCtx<'_>) -> ReadVar<SideEffect> {
2153 let (side_effect, claim_side_effect) = ctx.new_prefixed_var("auto_se");
2154 ctx.backend
2155 .borrow_mut()
2156 .on_claimed_runtime_var(&claim_side_effect.backing_var, false);
2157
2158 ctx.emit_gh_step_inner(
2159 self.display_name,
2160 self.cond,
2161 self.uses,
2162 self.with,
2163 self.outputs,
2164 self.run_after,
2165 self.permissions,
2166 );
2167
2168 side_effect
2169 }
2170 }
2171
2172 #[derive(Clone, Debug)]
2173 pub enum GhParam<C = VarNotClaimed> {
2174 Static(String),
2175 FloweyVar(ReadVar<String, C>),
2176 }
2177
2178 impl From<String> for GhParam {
2179 fn from(param: String) -> GhParam {
2180 GhParam::Static(param)
2181 }
2182 }
2183
2184 impl From<&str> for GhParam {
2185 fn from(param: &str) -> GhParam {
2186 GhParam::Static(param.to_string())
2187 }
2188 }
2189
2190 impl From<ReadVar<String>> for GhParam {
2191 fn from(param: ReadVar<String>) -> GhParam {
2192 GhParam::FloweyVar(param)
2193 }
2194 }
2195
2196 pub type ClaimedGhParam = GhParam<VarClaimed>;
2197
2198 impl ClaimVar for GhParam {
2199 type Claimed = ClaimedGhParam;
2200
2201 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedGhParam {
2202 match self {
2203 GhParam::Static(s) => ClaimedGhParam::Static(s),
2204 GhParam::FloweyVar(var) => match &var.backing_var {
2205 ReadVarBacking::RuntimeVar { is_side_effect, .. } => {
2206 assert!(!is_side_effect);
2207 ClaimedGhParam::FloweyVar(var.claim(ctx))
2208 }
2209 ReadVarBacking::Inline(var) => ClaimedGhParam::Static(var.clone()),
2210 },
2211 }
2212 }
2213 }
2214
2215 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
2220 pub enum GhPermissionValue {
2221 None = 0,
2222 Read = 1,
2223 Write = 2,
2224 }
2225
2226 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2232 pub enum GhPermission {
2233 Actions,
2234 Attestations,
2235 Checks,
2236 Contents,
2237 Deployments,
2238 Discussions,
2239 IdToken,
2240 Issues,
2241 Packages,
2242 Pages,
2243 PullRequests,
2244 RepositoryProjects,
2245 SecurityEvents,
2246 Statuses,
2247 }
2248 }
2249
2250 pub mod rust {
2251 use crate::node::ClaimedWriteVar;
2252 use crate::node::FlowArch;
2253 use crate::node::FlowBackend;
2254 use crate::node::FlowPlatform;
2255 use crate::node::ReadVarValue;
2256 use crate::node::RuntimeVarDb;
2257 use crate::shell::FloweyShell;
2258 use serde::Serialize;
2259 use serde::de::DeserializeOwned;
2260
2261 pub fn new_rust_runtime_services(
2262 runtime_var_db: &mut dyn RuntimeVarDb,
2263 backend: FlowBackend,
2264 platform: FlowPlatform,
2265 arch: FlowArch,
2266 ) -> anyhow::Result<RustRuntimeServices<'_>> {
2267 Ok(RustRuntimeServices {
2268 runtime_var_db,
2269 backend,
2270 platform,
2271 arch,
2272 has_read_secret: false,
2273 sh: FloweyShell::new()?,
2274 })
2275 }
2276
2277 pub struct RustRuntimeServices<'a> {
2278 runtime_var_db: &'a mut dyn RuntimeVarDb,
2279 backend: FlowBackend,
2280 platform: FlowPlatform,
2281 arch: FlowArch,
2282 has_read_secret: bool,
2283 pub sh: FloweyShell,
2289 }
2290
2291 impl RustRuntimeServices<'_> {
2292 pub fn backend(&self) -> FlowBackend {
2295 self.backend
2296 }
2297
2298 pub fn platform(&self) -> FlowPlatform {
2301 self.platform
2302 }
2303
2304 pub fn arch(&self) -> FlowArch {
2306 self.arch
2307 }
2308
2309 pub fn write<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2317 where
2318 T: Serialize + DeserializeOwned,
2319 {
2320 self.write_maybe_secret(var, val, self.has_read_secret)
2321 }
2322
2323 pub fn write_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2329 where
2330 T: Serialize + DeserializeOwned,
2331 {
2332 self.write_maybe_secret(var, val, true)
2333 }
2334
2335 pub fn write_not_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2342 where
2343 T: Serialize + DeserializeOwned,
2344 {
2345 self.write_maybe_secret(var, val, false)
2346 }
2347
2348 fn write_maybe_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T, is_secret: bool)
2349 where
2350 T: Serialize + DeserializeOwned,
2351 {
2352 let val = if var.is_side_effect {
2353 b"null".to_vec()
2354 } else {
2355 serde_json::to_vec(val).expect("improve this error path")
2356 };
2357 self.runtime_var_db
2358 .set_var(&var.backing_var, is_secret, val);
2359 }
2360
2361 pub fn write_all<T>(
2362 &mut self,
2363 vars: impl IntoIterator<Item = ClaimedWriteVar<T>>,
2364 val: &T,
2365 ) where
2366 T: Serialize + DeserializeOwned,
2367 {
2368 for var in vars {
2369 self.write(var, val)
2370 }
2371 }
2372
2373 pub fn read<T: ReadVarValue>(&mut self, var: T) -> T::Value {
2374 var.read_value(self)
2375 }
2376
2377 pub(crate) fn get_var(&mut self, var: &str, is_side_effect: bool) -> Vec<u8> {
2378 let (v, is_secret) = self.runtime_var_db.get_var(var);
2379 self.has_read_secret |= is_secret && !is_side_effect;
2380 v
2381 }
2382
2383 pub fn dangerous_gh_set_global_env_var(
2390 &mut self,
2391 var: String,
2392 gh_env_var: String,
2393 ) -> anyhow::Result<()> {
2394 if !matches!(self.backend, FlowBackend::Github) {
2395 return Err(anyhow::anyhow!(
2396 "dangerous_set_gh_env_var can only be used on GitHub Actions"
2397 ));
2398 }
2399
2400 let gh_env_file_path = std::env::var("GITHUB_ENV")?;
2401 let mut gh_env_file = fs_err::OpenOptions::new()
2402 .append(true)
2403 .open(gh_env_file_path)?;
2404 let gh_env_var_assignment = format!(
2405 r#"{}<<EOF
2406{}
2407EOF
2408"#,
2409 gh_env_var, var
2410 );
2411 std::io::Write::write_all(&mut gh_env_file, gh_env_var_assignment.as_bytes())?;
2412
2413 Ok(())
2414 }
2415 }
2416 }
2417}
2418
2419pub trait FlowNodeBase {
2424 type Request: Serialize + DeserializeOwned;
2425
2426 fn imports(&mut self, ctx: &mut ImportCtx<'_>);
2427 fn emit(
2428 &mut self,
2429 config_bytes: Vec<Box<[u8]>>,
2430 requests: Vec<Self::Request>,
2431 ctx: &mut NodeCtx<'_>,
2432 ) -> anyhow::Result<()>;
2433
2434 fn i_know_what_im_doing_with_this_manual_impl(&mut self);
2440}
2441
2442pub mod erased {
2443 use crate::node::FlowNodeBase;
2444 use crate::node::NodeCtx;
2445 use crate::node::user_facing::*;
2446
2447 pub struct ErasedNode<N: FlowNodeBase>(pub N);
2448
2449 impl<N: FlowNodeBase> ErasedNode<N> {
2450 pub fn from_node(node: N) -> Self {
2451 Self(node)
2452 }
2453 }
2454
2455 impl<N> FlowNodeBase for ErasedNode<N>
2456 where
2457 N: FlowNodeBase,
2458 {
2459 type Request = Box<[u8]>;
2461
2462 fn imports(&mut self, ctx: &mut ImportCtx<'_>) {
2463 self.0.imports(ctx)
2464 }
2465
2466 fn emit(
2467 &mut self,
2468 config_bytes: Vec<Box<[u8]>>,
2469 requests: Vec<Box<[u8]>>,
2470 ctx: &mut NodeCtx<'_>,
2471 ) -> anyhow::Result<()> {
2472 let mut converted_requests = Vec::new();
2473 for req in requests {
2474 converted_requests.push(serde_json::from_slice(&req)?)
2475 }
2476
2477 self.0.emit(config_bytes, converted_requests, ctx)
2478 }
2479
2480 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2481 }
2482}
2483
2484#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2486pub struct NodeHandle(std::any::TypeId);
2487
2488impl Ord for NodeHandle {
2489 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2490 self.modpath().cmp(other.modpath())
2491 }
2492}
2493
2494impl PartialOrd for NodeHandle {
2495 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2496 Some(self.cmp(other))
2497 }
2498}
2499
2500impl std::fmt::Debug for NodeHandle {
2501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2502 std::fmt::Debug::fmt(&self.try_modpath(), f)
2503 }
2504}
2505
2506impl NodeHandle {
2507 pub fn from_type<N: FlowNodeBase + 'static>() -> NodeHandle {
2508 NodeHandle(std::any::TypeId::of::<N>())
2509 }
2510
2511 pub fn from_modpath(modpath: &str) -> NodeHandle {
2512 node_luts::erased_node_by_modpath().get(modpath).unwrap().0
2513 }
2514
2515 pub fn try_from_modpath(modpath: &str) -> Option<NodeHandle> {
2516 node_luts::erased_node_by_modpath()
2517 .get(modpath)
2518 .map(|(s, _)| *s)
2519 }
2520
2521 pub fn new_erased_node(&self) -> Box<dyn FlowNodeBase<Request = Box<[u8]>>> {
2522 let ctor = node_luts::erased_node_by_typeid().get(self).unwrap();
2523 ctor()
2524 }
2525
2526 pub fn modpath(&self) -> &'static str {
2527 node_luts::modpath_by_node_typeid().get(self).unwrap()
2528 }
2529
2530 pub fn try_modpath(&self) -> Option<&'static str> {
2531 node_luts::modpath_by_node_typeid().get(self).cloned()
2532 }
2533
2534 pub fn dummy() -> NodeHandle {
2537 NodeHandle(std::any::TypeId::of::<()>())
2538 }
2539}
2540
2541pub fn list_all_registered_nodes() -> impl Iterator<Item = NodeHandle> {
2542 node_luts::modpath_by_node_typeid().keys().cloned()
2543}
2544
2545mod node_luts {
2560 use super::FlowNodeBase;
2561 use super::NodeHandle;
2562 use std::collections::HashMap;
2563 use std::sync::OnceLock;
2564
2565 pub(super) fn modpath_by_node_typeid() -> &'static HashMap<NodeHandle, &'static str> {
2566 static TYPEID_TO_MODPATH: OnceLock<HashMap<NodeHandle, &'static str>> = OnceLock::new();
2567
2568 TYPEID_TO_MODPATH.get_or_init(|| {
2569 let mut lookup = HashMap::new();
2570 for crate::node::private::FlowNodeMeta {
2571 module_path,
2572 ctor: _,
2573 typeid,
2574 } in crate::node::private::FLOW_NODES
2575 {
2576 let existing = lookup.insert(
2577 NodeHandle(*typeid),
2578 module_path
2579 .strip_suffix("::_only_one_call_to_flowey_node_per_module")
2580 .unwrap(),
2581 );
2582 assert!(existing.is_none())
2585 }
2586
2587 lookup
2588 })
2589 }
2590
2591 pub(super) fn erased_node_by_typeid()
2592 -> &'static HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>> {
2593 static LOOKUP: OnceLock<
2594 HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>>,
2595 > = OnceLock::new();
2596
2597 LOOKUP.get_or_init(|| {
2598 let mut lookup = HashMap::new();
2599 for crate::node::private::FlowNodeMeta {
2600 module_path: _,
2601 ctor,
2602 typeid,
2603 } in crate::node::private::FLOW_NODES
2604 {
2605 let existing = lookup.insert(NodeHandle(*typeid), *ctor);
2606 assert!(existing.is_none())
2609 }
2610
2611 lookup
2612 })
2613 }
2614
2615 pub(super) fn erased_node_by_modpath() -> &'static HashMap<
2616 &'static str,
2617 (
2618 NodeHandle,
2619 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2620 ),
2621 > {
2622 static MODPATH_LOOKUP: OnceLock<
2623 HashMap<
2624 &'static str,
2625 (
2626 NodeHandle,
2627 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2628 ),
2629 >,
2630 > = OnceLock::new();
2631
2632 MODPATH_LOOKUP.get_or_init(|| {
2633 let mut lookup = HashMap::new();
2634 for crate::node::private::FlowNodeMeta { module_path, ctor, typeid } in crate::node::private::FLOW_NODES {
2635 let existing = lookup.insert(module_path.strip_suffix("::_only_one_call_to_flowey_node_per_module").unwrap(), (NodeHandle(*typeid), *ctor));
2636 if existing.is_some() {
2637 panic!("conflicting node registrations at {module_path}! please ensure there is a single node per module!")
2638 }
2639 }
2640 lookup
2641 })
2642 }
2643}
2644
2645#[doc(hidden)]
2646pub mod private {
2647 pub use linkme;
2648
2649 pub struct FlowNodeMeta {
2650 pub module_path: &'static str,
2651 pub ctor: fn() -> Box<dyn super::FlowNodeBase<Request = Box<[u8]>>>,
2652 pub typeid: std::any::TypeId,
2653 }
2654
2655 #[linkme::distributed_slice]
2656 pub static FLOW_NODES: [FlowNodeMeta] = [..];
2657
2658 #[expect(unsafe_code)]
2660 #[linkme::distributed_slice(FLOW_NODES)]
2661 static DUMMY_FLOW_NODE: FlowNodeMeta = FlowNodeMeta {
2662 module_path: "<dummy>::_only_one_call_to_flowey_node_per_module",
2663 ctor: || unreachable!(),
2664 typeid: std::any::TypeId::of::<()>(),
2665 };
2666}
2667
2668#[doc(hidden)]
2669#[macro_export]
2670macro_rules! new_flow_node_base {
2671 (struct Node) => {
2672 #[non_exhaustive]
2674 pub struct Node;
2675
2676 mod _only_one_call_to_flowey_node_per_module {
2677 const _: () = {
2678 use $crate::node::private::linkme;
2679
2680 fn new_erased() -> Box<dyn $crate::node::FlowNodeBase<Request = Box<[u8]>>> {
2681 Box::new($crate::node::erased::ErasedNode(super::Node))
2682 }
2683
2684 #[linkme::distributed_slice($crate::node::private::FLOW_NODES)]
2685 #[linkme(crate = linkme)]
2686 static FLOW_NODE: $crate::node::private::FlowNodeMeta =
2687 $crate::node::private::FlowNodeMeta {
2688 module_path: module_path!(),
2689 ctor: new_erased,
2690 typeid: std::any::TypeId::of::<super::Node>(),
2691 };
2692 };
2693 }
2694 };
2695}
2696
2697pub trait FlowNode {
2784 type Request: Serialize + DeserializeOwned;
2788
2789 fn imports(ctx: &mut ImportCtx<'_>);
2801
2802 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2805}
2806
2807#[macro_export]
2808macro_rules! new_flow_node {
2809 (struct Node) => {
2810 $crate::new_flow_node_base!(struct Node);
2811
2812 impl $crate::node::FlowNodeBase for Node
2813 where
2814 Node: FlowNode,
2815 {
2816 type Request = <Node as FlowNode>::Request;
2817
2818 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2819 <Node as FlowNode>::imports(dep)
2820 }
2821
2822 fn emit(
2823 &mut self,
2824 _config_bytes: Vec<Box<[u8]>>,
2825 requests: Vec<Self::Request>,
2826 ctx: &mut $crate::node::NodeCtx<'_>,
2827 ) -> anyhow::Result<()> {
2828 <Node as FlowNode>::emit(requests, ctx)
2829 }
2830
2831 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2832 }
2833 };
2834}
2835
2836pub trait SimpleFlowNode {
2857 type Request: Serialize + DeserializeOwned;
2858
2859 fn imports(ctx: &mut ImportCtx<'_>);
2871
2872 fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2874}
2875
2876#[macro_export]
2877macro_rules! new_simple_flow_node {
2878 (struct Node) => {
2879 $crate::new_flow_node_base!(struct Node);
2880
2881 impl $crate::node::FlowNodeBase for Node
2882 where
2883 Node: $crate::node::SimpleFlowNode,
2884 {
2885 type Request = <Node as $crate::node::SimpleFlowNode>::Request;
2886
2887 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2888 <Node as $crate::node::SimpleFlowNode>::imports(dep)
2889 }
2890
2891 fn emit(
2892 &mut self,
2893 _config_bytes: Vec<Box<[u8]>>,
2894 requests: Vec<Self::Request>,
2895 ctx: &mut $crate::node::NodeCtx<'_>,
2896 ) -> anyhow::Result<()> {
2897 for req in requests {
2898 <Node as $crate::node::SimpleFlowNode>::process_request(req, ctx)?
2899 }
2900
2901 Ok(())
2902 }
2903
2904 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2905 }
2906 };
2907}
2908
2909pub trait FlowNodeWithConfig {
2956 type Request: Serialize + DeserializeOwned;
2958
2959 type Config: ConfigMerge;
2966
2967 fn imports(ctx: &mut ImportCtx<'_>);
2969
2970 fn emit(
2972 config: Self::Config,
2973 requests: Vec<Self::Request>,
2974 ctx: &mut NodeCtx<'_>,
2975 ) -> anyhow::Result<()>;
2976}
2977
2978#[macro_export]
2979macro_rules! new_flow_node_with_config {
2980 (struct Node) => {
2981 $crate::new_flow_node_base!(struct Node);
2982
2983 impl $crate::node::FlowNodeBase for Node
2984 where
2985 Node: $crate::node::FlowNodeWithConfig,
2986 {
2987 type Request = <Node as $crate::node::FlowNodeWithConfig>::Request;
2988
2989 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2990 <Node as $crate::node::FlowNodeWithConfig>::imports(dep)
2991 }
2992
2993 fn emit(
2994 &mut self,
2995 config_bytes: Vec<Box<[u8]>>,
2996 requests: Vec<Self::Request>,
2997 ctx: &mut $crate::node::NodeCtx<'_>,
2998 ) -> anyhow::Result<()> {
2999 use $crate::node::ConfigMerge;
3000
3001 type C = <Node as $crate::node::FlowNodeWithConfig>::Config;
3002
3003 let mut merged = <C as Default>::default();
3004 for bytes in config_bytes {
3005 let partial: C = serde_json::from_slice(&bytes)?;
3006 merged.merge(partial)?;
3007 }
3008
3009 <Node as $crate::node::FlowNodeWithConfig>::emit(merged, requests, ctx)
3010 }
3011
3012 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
3013 }
3014 };
3015}
3016
3017pub trait IntoRequest {
3025 type Node: FlowNodeBase;
3026 fn into_request(self) -> <Self::Node as FlowNodeBase>::Request;
3027
3028 #[doc(hidden)]
3031 #[expect(nonstandard_style)]
3032 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self);
3033}
3034
3035pub trait IntoConfig: Serialize {
3041 type Node: FlowNodeBase;
3042
3043 #[doc(hidden)]
3046 #[expect(nonstandard_style)]
3047 fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self);
3048}
3049
3050pub trait ConfigMerge: Serialize + DeserializeOwned + Default {
3053 fn merge(&mut self, other: Self) -> anyhow::Result<()>;
3056}
3057
3058pub trait ConfigField {
3065 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()>;
3066}
3067
3068impl<T: PartialEq> ConfigField for Option<T> {
3069 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3070 if let Some(new) = other {
3071 match self {
3072 None => *self = Some(new),
3073 Some(old) if *old == new => {}
3074 Some(_) => {
3075 anyhow::bail!("config field `{field_name}` mismatch");
3076 }
3077 }
3078 }
3079 Ok(())
3080 }
3081}
3082
3083impl<K: Ord + std::fmt::Debug, V: PartialEq> ConfigField for BTreeMap<K, V> {
3084 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3085 for (k, v) in other {
3086 use std::collections::btree_map::Entry;
3087 match self.entry(k) {
3088 Entry::Vacant(e) => {
3089 e.insert(v);
3090 }
3091 Entry::Occupied(e) if *e.get() == v => {}
3092 Entry::Occupied(e) => {
3093 anyhow::bail!("config field `{field_name}` mismatch for key {:?}", e.key(),);
3094 }
3095 }
3096 }
3097 Ok(())
3098 }
3099}
3100
3101#[doc(hidden)]
3102#[macro_export]
3103macro_rules! __flowey_request_inner {
3104 (@emit_struct [$req:ident]
3108 $(#[$a:meta])*
3109 $variant:ident($($tt:tt)*),
3110 $($rest:tt)*
3111 ) => {
3112 $(#[$a])*
3113 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3114 pub struct $variant($($tt)*);
3115
3116 impl IntoRequest for $variant {
3117 type Node = Node;
3118 fn into_request(self) -> $req {
3119 $req::$variant(self)
3120 }
3121 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3122 }
3123
3124 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3125 };
3126 (@emit_struct [$req:ident]
3127 $(#[$a:meta])*
3128 $variant:ident { $($tt:tt)* },
3129 $($rest:tt)*
3130 ) => {
3131 $(#[$a])*
3132 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3133 pub struct $variant {
3134 $($tt)*
3135 }
3136
3137 impl IntoRequest for $variant {
3138 type Node = Node;
3139 fn into_request(self) -> $req {
3140 $req::$variant(self)
3141 }
3142 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3143 }
3144
3145 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3146 };
3147 (@emit_struct [$req:ident]
3148 $(#[$a:meta])*
3149 $variant:ident,
3150 $($rest:tt)*
3151 ) => {
3152 $(#[$a])*
3153 #[derive(Serialize, Deserialize)]
3154 pub struct $variant;
3155
3156 impl IntoRequest for $variant {
3157 type Node = Node;
3158 fn into_request(self) -> $req {
3159 $req::$variant(self)
3160 }
3161 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3162 }
3163
3164 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3165 };
3166 (@emit_struct [$req:ident]
3167 ) => {};
3168
3169 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3173 $(#[$a:meta])*
3174 $variant:ident($($tt:tt)*),
3175 $($rest:tt)*
3176 ) => {
3177 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3178 };
3179 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3180 $(#[$a:meta])*
3181 $variant:ident { $($tt:tt)* },
3182 $($rest:tt)*
3183 ) => {
3184 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3185 };
3186 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3187 $(#[$a:meta])*
3188 $variant:ident,
3189 $($rest:tt)*
3190 ) => {
3191 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3192 };
3193 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3194 ) => {
3195 #[derive(Serialize, Deserialize)]
3196 pub enum $req {$(
3197 $(#[$prev_a])*
3198 $prev(self::req::$prev),
3199 )*}
3200
3201 impl IntoRequest for $req {
3202 type Node = Node;
3203 fn into_request(self) -> $req {
3204 self
3205 }
3206 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3207 }
3208 };
3209}
3210
3211#[macro_export]
3259macro_rules! flowey_request {
3260 (
3261 $(#[$root_a:meta])*
3262 pub enum_struct $req:ident {
3263 $($tt:tt)*
3264 }
3265 ) => {
3266 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*),] $($tt)*);
3267 pub mod req {
3268 use super::*;
3269 $crate::__flowey_request_inner!(@emit_struct [$req] $($tt)*);
3270 }
3271 };
3272
3273 (
3274 $(#[$a:meta])*
3275 pub enum $req:ident {
3276 $($tt:tt)*
3277 }
3278 ) => {
3279 $(#[$a])*
3280 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3281 pub enum $req {
3282 $($tt)*
3283 }
3284
3285 impl $crate::node::IntoRequest for $req {
3286 type Node = Node;
3287 fn into_request(self) -> $req {
3288 self
3289 }
3290 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3291 }
3292 };
3293
3294 (
3295 $(#[$a:meta])*
3296 pub struct $req:ident {
3297 $($tt:tt)*
3298 }
3299 ) => {
3300 $(#[$a])*
3301 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3302 pub struct $req {
3303 $($tt)*
3304 }
3305
3306 impl $crate::node::IntoRequest for $req {
3307 type Node = Node;
3308 fn into_request(self) -> $req {
3309 self
3310 }
3311 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3312 }
3313 };
3314
3315 (
3316 $(#[$a:meta])*
3317 pub struct $req:ident($($tt:tt)*);
3318 ) => {
3319 $(#[$a])*
3320 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3321 pub struct $req($($tt)*);
3322
3323 impl $crate::node::IntoRequest for $req {
3324 type Node = Node;
3325 fn into_request(self) -> $req {
3326 self
3327 }
3328 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3329 }
3330 };
3331}
3332
3333#[macro_export]
3371macro_rules! flowey_config {
3372 (
3373 $(#[$meta:meta])*
3374 pub struct $Config:ident {
3375 $(
3376 $(#[$field_meta:meta])*
3377 pub $field:ident : $ty:ty
3378 ),* $(,)?
3379 }
3380 ) => {
3381 $(#[$meta])*
3382 #[derive(
3383 $crate::reexports::Serialize,
3384 $crate::reexports::Deserialize,
3385 Default,
3386 )]
3387 pub struct $Config {
3388 $(
3389 $(#[$field_meta])*
3390 pub $field: $ty,
3391 )*
3392 }
3393
3394 impl $crate::node::ConfigMerge for $Config {
3395 fn merge(&mut self, other: Self) -> anyhow::Result<()> {
3396 $(
3397 $crate::node::ConfigField::merge_field(
3398 &mut self.$field,
3399 stringify!($field),
3400 other.$field,
3401 )?;
3402 )*
3403 Ok(())
3404 }
3405 }
3406
3407 impl $crate::node::IntoConfig for $Config {
3408 type Node = Node;
3409
3410 fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self) {}
3411 }
3412 };
3413}
3414
3415#[macro_export]
3432macro_rules! shell_cmd {
3433 ($rt:expr, $cmd:literal) => {{
3434 let flowey_sh = &$rt.sh;
3435 #[expect(clippy::disallowed_macros)]
3436 flowey_sh.wrap($crate::reexports::xshell::cmd!(flowey_sh.xshell(), $cmd))
3437 }};
3438}