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
938pub trait NodeCtxBackend {
939 fn current_node(&self) -> NodeHandle;
941
942 fn on_new_var(&mut self) -> String;
947
948 fn on_claimed_runtime_var(&mut self, var: &str, is_read: bool);
950
951 fn on_unused_read_var(&mut self, var: &str);
953
954 fn on_request(&mut self, node_handle: NodeHandle, req: anyhow::Result<Box<[u8]>>);
962
963 fn on_config(&mut self, node_handle: NodeHandle, config: anyhow::Result<Box<[u8]>>);
967
968 fn on_emit_rust_step(
969 &mut self,
970 label: &str,
971 can_merge: bool,
972 code: Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
973 );
974
975 fn on_emit_ado_step(
976 &mut self,
977 label: &str,
978 yaml_snippet: Box<dyn for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String>,
979 inline_script: Option<
980 Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
981 >,
982 condvar: Option<String>,
983 );
984
985 fn on_emit_gh_step(
986 &mut self,
987 label: &str,
988 uses: &str,
989 with: BTreeMap<String, ClaimedGhParam>,
990 condvar: Option<String>,
991 outputs: BTreeMap<String, Vec<GhOutput>>,
992 permissions: BTreeMap<GhPermission, GhPermissionValue>,
993 gh_to_rust: Vec<GhToRust>,
994 rust_to_gh: Vec<RustToGh>,
995 );
996
997 fn on_emit_side_effect_step(&mut self);
998
999 fn backend(&mut self) -> FlowBackend;
1000 fn platform(&mut self) -> FlowPlatform;
1001 fn arch(&mut self) -> FlowArch;
1002
1003 fn persistent_dir_path_var(&mut self) -> Option<String>;
1007}
1008
1009pub fn new_node_ctx(backend: &mut dyn NodeCtxBackend) -> NodeCtx<'_> {
1010 NodeCtx {
1011 backend: Rc::new(RefCell::new(backend)),
1012 }
1013}
1014
1015#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1017pub enum FlowBackend {
1018 Local,
1020 Ado,
1022 Github,
1024}
1025
1026#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1028pub enum FlowPlatformKind {
1029 Windows,
1030 Unix,
1031}
1032
1033#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1035pub enum FlowPlatformLinuxDistro {
1036 Fedora,
1038 Ubuntu,
1040 AzureLinux,
1042 Arch,
1044 Nix,
1046 Unknown,
1048}
1049
1050#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1052#[non_exhaustive]
1053pub enum FlowPlatform {
1054 Windows,
1056 Linux(FlowPlatformLinuxDistro),
1058 MacOs,
1060}
1061
1062impl FlowPlatform {
1063 pub fn kind(&self) -> FlowPlatformKind {
1064 match self {
1065 Self::Windows => FlowPlatformKind::Windows,
1066 Self::Linux(_) | Self::MacOs => FlowPlatformKind::Unix,
1067 }
1068 }
1069
1070 fn as_str(&self) -> &'static str {
1071 match self {
1072 Self::Windows => "windows",
1073 Self::Linux(_) => "linux",
1074 Self::MacOs => "macos",
1075 }
1076 }
1077
1078 pub fn exe_suffix(&self) -> &'static str {
1080 if self == &Self::Windows { ".exe" } else { "" }
1081 }
1082
1083 pub fn binary(&self, name: &str) -> String {
1085 format!("{}{}", name, self.exe_suffix())
1086 }
1087}
1088
1089impl std::fmt::Display for FlowPlatform {
1090 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1091 f.pad(self.as_str())
1092 }
1093}
1094
1095#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1097#[non_exhaustive]
1098pub enum FlowArch {
1099 X86_64,
1100 Aarch64,
1101}
1102
1103impl FlowArch {
1104 fn as_str(&self) -> &'static str {
1105 match self {
1106 Self::X86_64 => "x86_64",
1107 Self::Aarch64 => "aarch64",
1108 }
1109 }
1110}
1111
1112impl std::fmt::Display for FlowArch {
1113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1114 f.pad(self.as_str())
1115 }
1116}
1117
1118pub struct StepCtx<'a> {
1120 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1121}
1122
1123impl StepCtx<'_> {
1124 pub fn backend(&self) -> FlowBackend {
1127 self.backend.borrow_mut().backend()
1128 }
1129
1130 pub fn platform(&self) -> FlowPlatform {
1133 self.backend.borrow_mut().platform()
1134 }
1135}
1136
1137const NO_ADO_INLINE_SCRIPT: Option<
1138 for<'a> fn(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>,
1139> = None;
1140
1141pub struct NodeCtx<'a> {
1143 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1144}
1145
1146impl<'ctx> NodeCtx<'ctx> {
1147 pub fn emit_rust_step<F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<SideEffect>
1153 where
1154 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1155 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1156 {
1157 self.emit_rust_step_inner(label.as_ref(), false, code)
1158 }
1159
1160 pub fn emit_minor_rust_step<F, G>(
1166 &mut self,
1167 label: impl AsRef<str>,
1168 code: F,
1169 ) -> ReadVar<SideEffect>
1170 where
1171 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1172 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) + 'static,
1173 {
1174 self.emit_rust_step_inner(label.as_ref(), true, |ctx| {
1175 let f = code(ctx);
1176 |rt| {
1177 f(rt);
1178 Ok(())
1179 }
1180 })
1181 }
1182
1183 #[must_use]
1204 #[track_caller]
1205 pub fn emit_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1206 where
1207 T: Serialize + DeserializeOwned + 'static,
1208 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1209 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1210 {
1211 self.emit_rust_stepv_inner(label.as_ref(), false, code)
1212 }
1213
1214 #[must_use]
1238 #[track_caller]
1239 pub fn emit_minor_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1240 where
1241 T: Serialize + DeserializeOwned + 'static,
1242 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1243 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> T + 'static,
1244 {
1245 self.emit_rust_stepv_inner(label.as_ref(), true, |ctx| {
1246 let f = code(ctx);
1247 |rt| Ok(f(rt))
1248 })
1249 }
1250
1251 fn emit_rust_step_inner<F, G>(
1252 &mut self,
1253 label: &str,
1254 can_merge: bool,
1255 code: F,
1256 ) -> ReadVar<SideEffect>
1257 where
1258 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1259 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1260 {
1261 let (read, write) = self.new_prefixed_var("auto_se");
1262
1263 let ctx = &mut StepCtx {
1264 backend: self.backend.clone(),
1265 };
1266 write.claim(ctx);
1267
1268 let code = code(ctx);
1269 self.backend
1270 .borrow_mut()
1271 .on_emit_rust_step(label.as_ref(), can_merge, Box::new(code));
1272 read
1273 }
1274
1275 #[must_use]
1276 #[track_caller]
1277 fn emit_rust_stepv_inner<T, F, G>(
1278 &mut self,
1279 label: impl AsRef<str>,
1280 can_merge: bool,
1281 code: F,
1282 ) -> ReadVar<T>
1283 where
1284 T: Serialize + DeserializeOwned + 'static,
1285 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1286 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1287 {
1288 let (read, write) = self.new_var();
1289
1290 let ctx = &mut StepCtx {
1291 backend: self.backend.clone(),
1292 };
1293 let write = write.claim(ctx);
1294
1295 let code = code(ctx);
1296 self.backend.borrow_mut().on_emit_rust_step(
1297 label.as_ref(),
1298 can_merge,
1299 Box::new(|rt| {
1300 let val = code(rt)?;
1301 rt.write(write, &val);
1302 Ok(())
1303 }),
1304 );
1305 read
1306 }
1307
1308 #[track_caller]
1310 #[must_use]
1311 pub fn get_ado_variable(&mut self, ado_var: AdoRuntimeVar) -> ReadVar<String> {
1312 let (var, write_var) = self.new_var();
1313 self.emit_ado_step(format!("🌼 read {}", ado_var.as_raw_var_name()), |ctx| {
1314 let write_var = write_var.claim(ctx);
1315 |rt| {
1316 rt.set_var(write_var, ado_var);
1317 "".into()
1318 }
1319 });
1320 var
1321 }
1322
1323 pub fn emit_ado_step<F, G>(&mut self, display_name: impl AsRef<str>, yaml_snippet: F)
1325 where
1326 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1327 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1328 {
1329 self.emit_ado_step_inner(display_name, None, |ctx| {
1330 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1331 })
1332 }
1333
1334 pub fn emit_ado_step_with_condition<F, G>(
1337 &mut self,
1338 display_name: impl AsRef<str>,
1339 cond: ReadVar<bool>,
1340 yaml_snippet: F,
1341 ) where
1342 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1343 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1344 {
1345 self.emit_ado_step_inner(display_name, Some(cond), |ctx| {
1346 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1347 })
1348 }
1349
1350 pub fn emit_ado_step_with_condition_optional<F, G>(
1353 &mut self,
1354 display_name: impl AsRef<str>,
1355 cond: Option<ReadVar<bool>>,
1356 yaml_snippet: F,
1357 ) where
1358 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1359 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1360 {
1361 self.emit_ado_step_inner(display_name, cond, |ctx| {
1362 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1363 })
1364 }
1365
1366 pub fn emit_ado_step_with_inline_script<F, G, H>(
1395 &mut self,
1396 display_name: impl AsRef<str>,
1397 yaml_snippet: F,
1398 ) where
1399 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, H),
1400 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1401 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1402 {
1403 self.emit_ado_step_inner(display_name, None, |ctx| {
1404 let (f, g) = yaml_snippet(ctx);
1405 (f, Some(g))
1406 })
1407 }
1408
1409 fn emit_ado_step_inner<F, G, H>(
1410 &mut self,
1411 display_name: impl AsRef<str>,
1412 cond: Option<ReadVar<bool>>,
1413 yaml_snippet: F,
1414 ) where
1415 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, Option<H>),
1416 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1417 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1418 {
1419 let condvar = match cond.map(|c| c.backing_var) {
1420 Some(ReadVarBacking::Inline(cond)) => {
1422 if !cond {
1423 return;
1424 } else {
1425 None
1426 }
1427 }
1428 Some(ReadVarBacking::RuntimeVar {
1429 var,
1430 is_side_effect,
1431 }) => {
1432 assert!(!is_side_effect);
1433 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1434 Some(var)
1435 }
1436 None => None,
1437 };
1438
1439 let (yaml_snippet, inline_script) = yaml_snippet(&mut StepCtx {
1440 backend: self.backend.clone(),
1441 });
1442 self.backend.borrow_mut().on_emit_ado_step(
1443 display_name.as_ref(),
1444 Box::new(yaml_snippet),
1445 if let Some(inline_script) = inline_script {
1446 Some(Box::new(inline_script))
1447 } else {
1448 None
1449 },
1450 condvar,
1451 );
1452 }
1453
1454 #[track_caller]
1456 #[must_use]
1457 pub fn get_gh_context_var(&mut self) -> GhContextVarReader<'ctx, Root> {
1458 GhContextVarReader {
1459 ctx: NodeCtx {
1460 backend: self.backend.clone(),
1461 },
1462 _state: std::marker::PhantomData,
1463 }
1464 }
1465
1466 pub fn emit_gh_step(
1468 &mut self,
1469 display_name: impl AsRef<str>,
1470 uses: impl AsRef<str>,
1471 ) -> GhStepBuilder {
1472 GhStepBuilder::new(display_name, uses)
1473 }
1474
1475 fn emit_gh_step_inner(
1476 &mut self,
1477 display_name: impl AsRef<str>,
1478 cond: Option<ReadVar<bool>>,
1479 uses: impl AsRef<str>,
1480 with: Option<BTreeMap<String, GhParam>>,
1481 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
1482 run_after: Vec<ReadVar<SideEffect>>,
1483 permissions: BTreeMap<GhPermission, GhPermissionValue>,
1484 ) {
1485 let condvar = match cond.map(|c| c.backing_var) {
1486 Some(ReadVarBacking::Inline(cond)) => {
1488 if !cond {
1489 return;
1490 } else {
1491 None
1492 }
1493 }
1494 Some(ReadVarBacking::RuntimeVar {
1495 var,
1496 is_side_effect,
1497 }) => {
1498 assert!(!is_side_effect);
1499 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1500 Some(var)
1501 }
1502 None => None,
1503 };
1504
1505 let with = with
1506 .unwrap_or_default()
1507 .into_iter()
1508 .map(|(k, v)| {
1509 (
1510 k.clone(),
1511 v.claim(&mut StepCtx {
1512 backend: self.backend.clone(),
1513 }),
1514 )
1515 })
1516 .collect();
1517
1518 for var in run_after {
1519 var.claim(&mut StepCtx {
1520 backend: self.backend.clone(),
1521 });
1522 }
1523
1524 let outputvars = outputs
1525 .into_iter()
1526 .map(|(name, vars)| {
1527 (
1528 name,
1529 vars.into_iter()
1530 .map(|var| {
1531 let var = var.claim(&mut StepCtx {
1532 backend: self.backend.clone(),
1533 });
1534 GhOutput {
1535 backing_var: var.backing_var,
1536 is_secret: false,
1537 is_object: false,
1538 }
1539 })
1540 .collect(),
1541 )
1542 })
1543 .collect();
1544
1545 self.backend.borrow_mut().on_emit_gh_step(
1546 display_name.as_ref(),
1547 uses.as_ref(),
1548 with,
1549 condvar,
1550 outputvars,
1551 permissions,
1552 Vec::new(),
1553 Vec::new(),
1554 );
1555 }
1556
1557 pub fn emit_side_effect_step(
1565 &mut self,
1566 use_side_effects: impl IntoIterator<Item = ReadVar<SideEffect>>,
1567 resolve_side_effects: impl IntoIterator<Item = WriteVar<SideEffect>>,
1568 ) {
1569 let mut backend = self.backend.borrow_mut();
1570 for var in use_side_effects.into_iter() {
1571 if let ReadVarBacking::RuntimeVar {
1572 var,
1573 is_side_effect: _,
1574 } = &var.backing_var
1575 {
1576 backend.on_claimed_runtime_var(var, true);
1577 }
1578 }
1579
1580 for var in resolve_side_effects.into_iter() {
1581 backend.on_claimed_runtime_var(&var.backing_var, false);
1582 }
1583
1584 backend.on_emit_side_effect_step();
1585 }
1586
1587 pub fn backend(&self) -> FlowBackend {
1590 self.backend.borrow_mut().backend()
1591 }
1592
1593 pub fn platform(&self) -> FlowPlatform {
1596 self.backend.borrow_mut().platform()
1597 }
1598
1599 pub fn arch(&self) -> FlowArch {
1601 self.backend.borrow_mut().arch()
1602 }
1603
1604 pub fn req<R>(&mut self, req: R)
1606 where
1607 R: IntoRequest + 'static,
1608 {
1609 let mut backend = self.backend.borrow_mut();
1610 backend.on_request(
1611 NodeHandle::from_type::<R::Node>(),
1612 serde_json::to_vec(&req.into_request())
1613 .map(Into::into)
1614 .map_err(Into::into),
1615 );
1616 }
1617
1618 pub fn config<C>(&mut self, config: C)
1623 where
1624 C: IntoConfig + 'static,
1625 {
1626 let mut backend = self.backend.borrow_mut();
1627 backend.on_config(
1628 NodeHandle::from_type::<C::Node>(),
1629 serde_json::to_vec(&config)
1630 .map(Into::into)
1631 .map_err(Into::into),
1632 );
1633 }
1634
1635 #[track_caller]
1638 #[must_use]
1639 pub fn reqv<T, R>(&mut self, f: impl FnOnce(WriteVar<T>) -> R) -> ReadVar<T>
1640 where
1641 T: Serialize + DeserializeOwned,
1642 R: IntoRequest + 'static,
1643 {
1644 let (read, write) = self.new_var();
1645 self.req::<R>(f(write));
1646 read
1647 }
1648
1649 pub fn requests<N>(&mut self, reqs: impl IntoIterator<Item = N::Request>)
1651 where
1652 N: FlowNodeBase + 'static,
1653 {
1654 let mut backend = self.backend.borrow_mut();
1655 for req in reqs.into_iter() {
1656 backend.on_request(
1657 NodeHandle::from_type::<N>(),
1658 serde_json::to_vec(&req).map(Into::into).map_err(Into::into),
1659 );
1660 }
1661 }
1662
1663 #[track_caller]
1666 #[must_use]
1667 pub fn new_var<T>(&self) -> (ReadVar<T>, WriteVar<T>)
1668 where
1669 T: Serialize + DeserializeOwned,
1670 {
1671 self.new_prefixed_var("")
1672 }
1673
1674 #[track_caller]
1675 #[must_use]
1676 fn new_prefixed_var<T>(&self, prefix: &'static str) -> (ReadVar<T>, WriteVar<T>)
1677 where
1678 T: Serialize + DeserializeOwned,
1679 {
1680 let caller = std::panic::Location::caller().file().replace('\\', "/");
1687
1688 let caller = caller
1704 .split_once("flowey/")
1705 .expect("due to a known limitation with flowey, all flowey code must have an ancestor dir called 'flowey/' somewhere in its full path")
1706 .1;
1707
1708 let colon = if prefix.is_empty() { "" } else { ":" };
1709 let ordinal = self.backend.borrow_mut().on_new_var();
1710 let backing_var = format!("{prefix}{colon}{ordinal}:{caller}");
1711
1712 (
1713 ReadVar {
1714 backing_var: ReadVarBacking::RuntimeVar {
1715 var: backing_var.clone(),
1716 is_side_effect: false,
1717 },
1718 _kind: std::marker::PhantomData,
1719 },
1720 WriteVar {
1721 backing_var,
1722 is_side_effect: false,
1723 _kind: std::marker::PhantomData,
1724 },
1725 )
1726 }
1727
1728 #[track_caller]
1739 #[must_use]
1740 pub fn new_post_job_side_effect(&self) -> (ReadVar<SideEffect>, WriteVar<SideEffect>) {
1741 self.new_prefixed_var("post_job")
1742 }
1743
1744 #[track_caller]
1757 #[must_use]
1758 pub fn persistent_dir(&mut self) -> Option<ReadVar<PathBuf>> {
1759 let path: ReadVar<PathBuf> = ReadVar {
1760 backing_var: ReadVarBacking::RuntimeVar {
1761 var: self.backend.borrow_mut().persistent_dir_path_var()?,
1762 is_side_effect: false,
1763 },
1764 _kind: std::marker::PhantomData,
1765 };
1766
1767 let folder_name = self
1768 .backend
1769 .borrow_mut()
1770 .current_node()
1771 .modpath()
1772 .replace("::", "__");
1773
1774 Some(
1775 self.emit_rust_stepv("🌼 Create persistent store dir", |ctx| {
1776 let path = path.claim(ctx);
1777 |rt| {
1778 let dir = rt.read(path).join(folder_name);
1779 fs_err::create_dir_all(&dir)?;
1780 Ok(dir)
1781 }
1782 }),
1783 )
1784 }
1785
1786 pub fn supports_persistent_dir(&mut self) -> bool {
1788 self.backend
1789 .borrow_mut()
1790 .persistent_dir_path_var()
1791 .is_some()
1792 }
1793}
1794
1795pub trait RuntimeVarDb {
1798 fn get_var(&mut self, var_name: &str) -> (Vec<u8>, bool) {
1799 self.try_get_var(var_name)
1800 .unwrap_or_else(|| panic!("db is missing var {}", var_name))
1801 }
1802
1803 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)>;
1804 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>);
1805}
1806
1807impl RuntimeVarDb for Box<dyn RuntimeVarDb> {
1808 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)> {
1809 (**self).try_get_var(var_name)
1810 }
1811
1812 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>) {
1813 (**self).set_var(var_name, is_secret, value)
1814 }
1815}
1816
1817pub mod steps {
1818 pub mod ado {
1819 use crate::node::ClaimedReadVar;
1820 use crate::node::ClaimedWriteVar;
1821 use crate::node::ReadVarBacking;
1822 use serde::Deserialize;
1823 use serde::Serialize;
1824 use std::borrow::Cow;
1825
1826 #[derive(Debug, Clone, Serialize, Deserialize)]
1832 pub struct AdoResourcesRepositoryId {
1833 pub(crate) repo_id: String,
1834 }
1835
1836 impl AdoResourcesRepositoryId {
1837 pub fn new_self() -> Self {
1843 Self {
1844 repo_id: "self".into(),
1845 }
1846 }
1847
1848 pub fn dangerous_get_raw_id(&self) -> &str {
1854 &self.repo_id
1855 }
1856
1857 pub fn dangerous_new(repo_id: &str) -> Self {
1863 Self {
1864 repo_id: repo_id.into(),
1865 }
1866 }
1867 }
1868
1869 #[derive(Clone, Debug, Serialize, Deserialize)]
1874 pub struct AdoRuntimeVar {
1875 is_secret: bool,
1876 ado_var: Cow<'static, str>,
1877 }
1878
1879 impl AdoRuntimeVar {
1880 pub const BUILD_SOURCE_BRANCH: AdoRuntimeVar = AdoRuntimeVar::new("build.SourceBranch");
1886
1887 pub const BUILD_BUILD_NUMBER: AdoRuntimeVar = AdoRuntimeVar::new("build.BuildNumber");
1889
1890 pub const SYSTEM_ACCESS_TOKEN: AdoRuntimeVar =
1892 AdoRuntimeVar::new_secret("System.AccessToken");
1893
1894 pub const SYSTEM_JOB_ATTEMPT: AdoRuntimeVar =
1896 AdoRuntimeVar::new_secret("System.JobAttempt");
1897
1898 pub const PIPELINE_WORKSPACE: AdoRuntimeVar = AdoRuntimeVar::new("Pipeline.Workspace");
1900 }
1901
1902 impl AdoRuntimeVar {
1903 const fn new(s: &'static str) -> Self {
1904 Self {
1905 is_secret: false,
1906 ado_var: Cow::Borrowed(s),
1907 }
1908 }
1909
1910 const fn new_secret(s: &'static str) -> Self {
1911 Self {
1912 is_secret: true,
1913 ado_var: Cow::Borrowed(s),
1914 }
1915 }
1916
1917 pub fn is_secret(&self) -> bool {
1919 self.is_secret
1920 }
1921
1922 pub fn as_raw_var_name(&self) -> String {
1924 self.ado_var.as_ref().into()
1925 }
1926
1927 pub fn dangerous_from_global(ado_var_name: impl AsRef<str>, is_secret: bool) -> Self {
1935 Self {
1936 is_secret,
1937 ado_var: ado_var_name.as_ref().to_owned().into(),
1938 }
1939 }
1940 }
1941
1942 pub fn new_ado_step_services(
1943 fresh_ado_var: &mut dyn FnMut() -> String,
1944 ) -> AdoStepServices<'_> {
1945 AdoStepServices {
1946 fresh_ado_var,
1947 ado_to_rust: Vec::new(),
1948 rust_to_ado: Vec::new(),
1949 }
1950 }
1951
1952 pub struct CompletedAdoStepServices {
1953 pub ado_to_rust: Vec<(String, String, bool)>,
1954 pub rust_to_ado: Vec<(String, String)>,
1955 }
1956
1957 impl CompletedAdoStepServices {
1958 pub fn from_ado_step_services(access: AdoStepServices<'_>) -> Self {
1959 let AdoStepServices {
1960 fresh_ado_var: _,
1961 ado_to_rust,
1962 rust_to_ado,
1963 } = access;
1964
1965 Self {
1966 ado_to_rust,
1967 rust_to_ado,
1968 }
1969 }
1970 }
1971
1972 pub struct AdoStepServices<'a> {
1973 fresh_ado_var: &'a mut dyn FnMut() -> String,
1974 ado_to_rust: Vec<(String, String, bool)>,
1975 rust_to_ado: Vec<(String, String)>,
1976 }
1977
1978 impl AdoStepServices<'_> {
1979 pub fn resolve_repository_id(&self, repo_id: AdoResourcesRepositoryId) -> String {
1982 repo_id.repo_id
1983 }
1984
1985 pub fn set_var(&mut self, var: ClaimedWriteVar<String>, from_ado_var: AdoRuntimeVar) {
1991 self.ado_to_rust.push((
1992 from_ado_var.ado_var.into(),
1993 var.backing_var,
1994 from_ado_var.is_secret,
1995 ))
1996 }
1997
1998 pub fn get_var(&mut self, var: ClaimedReadVar<String>) -> AdoRuntimeVar {
2000 let backing_var = if let ReadVarBacking::RuntimeVar {
2001 var,
2002 is_side_effect,
2003 } = &var.backing_var
2004 {
2005 assert!(!is_side_effect);
2006 var
2007 } else {
2008 todo!("support inline ado read vars")
2009 };
2010
2011 let new_ado_var_name = (self.fresh_ado_var)();
2012
2013 self.rust_to_ado
2014 .push((backing_var.clone(), new_ado_var_name.clone()));
2015 AdoRuntimeVar::dangerous_from_global(new_ado_var_name, false)
2016 }
2017 }
2018 }
2019
2020 pub mod github {
2021 use crate::node::ClaimVar;
2022 use crate::node::NodeCtx;
2023 use crate::node::ReadVar;
2024 use crate::node::ReadVarBacking;
2025 use crate::node::SideEffect;
2026 use crate::node::StepCtx;
2027 use crate::node::VarClaimed;
2028 use crate::node::VarNotClaimed;
2029 use crate::node::WriteVar;
2030 use std::collections::BTreeMap;
2031
2032 pub struct GhStepBuilder {
2033 display_name: String,
2034 cond: Option<ReadVar<bool>>,
2035 uses: String,
2036 with: Option<BTreeMap<String, GhParam>>,
2037 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
2038 run_after: Vec<ReadVar<SideEffect>>,
2039 permissions: BTreeMap<GhPermission, GhPermissionValue>,
2040 }
2041
2042 impl GhStepBuilder {
2043 pub fn new(display_name: impl AsRef<str>, uses: impl AsRef<str>) -> Self {
2058 Self {
2059 display_name: display_name.as_ref().into(),
2060 cond: None,
2061 uses: uses.as_ref().into(),
2062 with: None,
2063 outputs: BTreeMap::new(),
2064 run_after: Vec::new(),
2065 permissions: BTreeMap::new(),
2066 }
2067 }
2068
2069 pub fn condition(mut self, cond: ReadVar<bool>) -> Self {
2076 self.cond = Some(cond);
2077 self
2078 }
2079
2080 pub fn with(mut self, k: impl AsRef<str>, v: impl Into<GhParam>) -> Self {
2106 self.with.get_or_insert_with(BTreeMap::new);
2107 if let Some(with) = &mut self.with {
2108 with.insert(k.as_ref().to_string(), v.into());
2109 }
2110 self
2111 }
2112
2113 pub fn output(mut self, k: impl AsRef<str>, v: WriteVar<String>) -> Self {
2122 self.outputs
2123 .entry(k.as_ref().to_string())
2124 .or_default()
2125 .push(v);
2126 self
2127 }
2128
2129 pub fn run_after(mut self, side_effect: ReadVar<SideEffect>) -> Self {
2131 self.run_after.push(side_effect);
2132 self
2133 }
2134
2135 pub fn requires_permission(
2140 mut self,
2141 perm: GhPermission,
2142 value: GhPermissionValue,
2143 ) -> Self {
2144 self.permissions.insert(perm, value);
2145 self
2146 }
2147
2148 #[track_caller]
2150 pub fn finish(self, ctx: &mut NodeCtx<'_>) -> ReadVar<SideEffect> {
2151 let (side_effect, claim_side_effect) = ctx.new_prefixed_var("auto_se");
2152 ctx.backend
2153 .borrow_mut()
2154 .on_claimed_runtime_var(&claim_side_effect.backing_var, false);
2155
2156 ctx.emit_gh_step_inner(
2157 self.display_name,
2158 self.cond,
2159 self.uses,
2160 self.with,
2161 self.outputs,
2162 self.run_after,
2163 self.permissions,
2164 );
2165
2166 side_effect
2167 }
2168 }
2169
2170 #[derive(Clone, Debug)]
2171 pub enum GhParam<C = VarNotClaimed> {
2172 Static(String),
2173 FloweyVar(ReadVar<String, C>),
2174 }
2175
2176 impl From<String> for GhParam {
2177 fn from(param: String) -> GhParam {
2178 GhParam::Static(param)
2179 }
2180 }
2181
2182 impl From<&str> for GhParam {
2183 fn from(param: &str) -> GhParam {
2184 GhParam::Static(param.to_string())
2185 }
2186 }
2187
2188 impl From<ReadVar<String>> for GhParam {
2189 fn from(param: ReadVar<String>) -> GhParam {
2190 GhParam::FloweyVar(param)
2191 }
2192 }
2193
2194 pub type ClaimedGhParam = GhParam<VarClaimed>;
2195
2196 impl ClaimVar for GhParam {
2197 type Claimed = ClaimedGhParam;
2198
2199 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedGhParam {
2200 match self {
2201 GhParam::Static(s) => ClaimedGhParam::Static(s),
2202 GhParam::FloweyVar(var) => match &var.backing_var {
2203 ReadVarBacking::RuntimeVar { is_side_effect, .. } => {
2204 assert!(!is_side_effect);
2205 ClaimedGhParam::FloweyVar(var.claim(ctx))
2206 }
2207 ReadVarBacking::Inline(var) => ClaimedGhParam::Static(var.clone()),
2208 },
2209 }
2210 }
2211 }
2212
2213 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
2218 pub enum GhPermissionValue {
2219 None = 0,
2220 Read = 1,
2221 Write = 2,
2222 }
2223
2224 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2230 pub enum GhPermission {
2231 Actions,
2232 ArtifactMetadata,
2233 Attestations,
2234 Checks,
2235 Contents,
2236 Deployments,
2237 Discussions,
2238 IdToken,
2239 Issues,
2240 Packages,
2241 Pages,
2242 PullRequests,
2243 RepositoryProjects,
2244 SecurityEvents,
2245 Statuses,
2246 }
2247 }
2248
2249 pub mod rust {
2250 use crate::node::ClaimedWriteVar;
2251 use crate::node::FlowArch;
2252 use crate::node::FlowBackend;
2253 use crate::node::FlowPlatform;
2254 use crate::node::ReadVarValue;
2255 use crate::node::RuntimeVarDb;
2256 use crate::shell::FloweyShell;
2257 use serde::Serialize;
2258 use serde::de::DeserializeOwned;
2259
2260 pub fn new_rust_runtime_services(
2261 runtime_var_db: &mut dyn RuntimeVarDb,
2262 backend: FlowBackend,
2263 platform: FlowPlatform,
2264 arch: FlowArch,
2265 ) -> anyhow::Result<RustRuntimeServices<'_>> {
2266 Ok(RustRuntimeServices {
2267 runtime_var_db,
2268 backend,
2269 platform,
2270 arch,
2271 has_read_secret: false,
2272 sh: FloweyShell::new()?,
2273 })
2274 }
2275
2276 pub struct RustRuntimeServices<'a> {
2277 runtime_var_db: &'a mut dyn RuntimeVarDb,
2278 backend: FlowBackend,
2279 platform: FlowPlatform,
2280 arch: FlowArch,
2281 has_read_secret: bool,
2282 pub sh: FloweyShell,
2288 }
2289
2290 impl RustRuntimeServices<'_> {
2291 pub fn backend(&self) -> FlowBackend {
2294 self.backend
2295 }
2296
2297 pub fn platform(&self) -> FlowPlatform {
2300 self.platform
2301 }
2302
2303 pub fn arch(&self) -> FlowArch {
2305 self.arch
2306 }
2307
2308 pub fn write<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2316 where
2317 T: Serialize + DeserializeOwned,
2318 {
2319 self.write_maybe_secret(var, val, self.has_read_secret)
2320 }
2321
2322 pub fn write_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2328 where
2329 T: Serialize + DeserializeOwned,
2330 {
2331 self.write_maybe_secret(var, val, true)
2332 }
2333
2334 pub fn write_not_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2341 where
2342 T: Serialize + DeserializeOwned,
2343 {
2344 self.write_maybe_secret(var, val, false)
2345 }
2346
2347 fn write_maybe_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T, is_secret: bool)
2348 where
2349 T: Serialize + DeserializeOwned,
2350 {
2351 let val = if var.is_side_effect {
2352 b"null".to_vec()
2353 } else {
2354 serde_json::to_vec(val).expect("improve this error path")
2355 };
2356 self.runtime_var_db
2357 .set_var(&var.backing_var, is_secret, val);
2358 }
2359
2360 pub fn write_all<T>(
2361 &mut self,
2362 vars: impl IntoIterator<Item = ClaimedWriteVar<T>>,
2363 val: &T,
2364 ) where
2365 T: Serialize + DeserializeOwned,
2366 {
2367 for var in vars {
2368 self.write(var, val)
2369 }
2370 }
2371
2372 pub fn read<T: ReadVarValue>(&mut self, var: T) -> T::Value {
2373 var.read_value(self)
2374 }
2375
2376 pub(crate) fn get_var(&mut self, var: &str, is_side_effect: bool) -> Vec<u8> {
2377 let (v, is_secret) = self.runtime_var_db.get_var(var);
2378 self.has_read_secret |= is_secret && !is_side_effect;
2379 v
2380 }
2381
2382 pub fn dangerous_gh_set_global_env_var(
2389 &mut self,
2390 var: String,
2391 gh_env_var: String,
2392 ) -> anyhow::Result<()> {
2393 if !matches!(self.backend, FlowBackend::Github) {
2394 return Err(anyhow::anyhow!(
2395 "dangerous_set_gh_env_var can only be used on GitHub Actions"
2396 ));
2397 }
2398
2399 let gh_env_file_path = std::env::var("GITHUB_ENV")?;
2400 let mut gh_env_file = fs_err::OpenOptions::new()
2401 .append(true)
2402 .open(gh_env_file_path)?;
2403 let gh_env_var_assignment = format!(
2404 r#"{}<<EOF
2405{}
2406EOF
2407"#,
2408 gh_env_var, var
2409 );
2410 std::io::Write::write_all(&mut gh_env_file, gh_env_var_assignment.as_bytes())?;
2411
2412 Ok(())
2413 }
2414 }
2415 }
2416}
2417
2418pub trait FlowNodeBase {
2423 type Request: Serialize + DeserializeOwned;
2424
2425 fn imports(&mut self, ctx: &mut ImportCtx<'_>);
2426 fn emit(
2427 &mut self,
2428 config_bytes: Vec<Box<[u8]>>,
2429 requests: Vec<Self::Request>,
2430 ctx: &mut NodeCtx<'_>,
2431 ) -> anyhow::Result<()>;
2432
2433 fn i_know_what_im_doing_with_this_manual_impl(&mut self);
2439}
2440
2441pub mod erased {
2442 use crate::node::FlowNodeBase;
2443 use crate::node::NodeCtx;
2444 use crate::node::user_facing::*;
2445
2446 pub struct ErasedNode<N: FlowNodeBase>(pub N);
2447
2448 impl<N: FlowNodeBase> ErasedNode<N> {
2449 pub fn from_node(node: N) -> Self {
2450 Self(node)
2451 }
2452 }
2453
2454 impl<N> FlowNodeBase for ErasedNode<N>
2455 where
2456 N: FlowNodeBase,
2457 {
2458 type Request = Box<[u8]>;
2460
2461 fn imports(&mut self, ctx: &mut ImportCtx<'_>) {
2462 self.0.imports(ctx)
2463 }
2464
2465 fn emit(
2466 &mut self,
2467 config_bytes: Vec<Box<[u8]>>,
2468 requests: Vec<Box<[u8]>>,
2469 ctx: &mut NodeCtx<'_>,
2470 ) -> anyhow::Result<()> {
2471 let mut converted_requests = Vec::new();
2472 for req in requests {
2473 converted_requests.push(serde_json::from_slice(&req)?)
2474 }
2475
2476 self.0.emit(config_bytes, converted_requests, ctx)
2477 }
2478
2479 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2480 }
2481}
2482
2483#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2485pub struct NodeHandle(std::any::TypeId);
2486
2487impl Ord for NodeHandle {
2488 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2489 self.modpath().cmp(other.modpath())
2490 }
2491}
2492
2493impl PartialOrd for NodeHandle {
2494 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2495 Some(self.cmp(other))
2496 }
2497}
2498
2499impl std::fmt::Debug for NodeHandle {
2500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2501 std::fmt::Debug::fmt(&self.try_modpath(), f)
2502 }
2503}
2504
2505impl NodeHandle {
2506 pub fn from_type<N: FlowNodeBase + 'static>() -> NodeHandle {
2507 NodeHandle(std::any::TypeId::of::<N>())
2508 }
2509
2510 pub fn from_modpath(modpath: &str) -> NodeHandle {
2511 node_luts::erased_node_by_modpath().get(modpath).unwrap().0
2512 }
2513
2514 pub fn try_from_modpath(modpath: &str) -> Option<NodeHandle> {
2515 node_luts::erased_node_by_modpath()
2516 .get(modpath)
2517 .map(|(s, _)| *s)
2518 }
2519
2520 pub fn new_erased_node(&self) -> Box<dyn FlowNodeBase<Request = Box<[u8]>>> {
2521 let ctor = node_luts::erased_node_by_typeid().get(self).unwrap();
2522 ctor()
2523 }
2524
2525 pub fn modpath(&self) -> &'static str {
2526 node_luts::modpath_by_node_typeid().get(self).unwrap()
2527 }
2528
2529 pub fn try_modpath(&self) -> Option<&'static str> {
2530 node_luts::modpath_by_node_typeid().get(self).cloned()
2531 }
2532
2533 pub fn dummy() -> NodeHandle {
2536 NodeHandle(std::any::TypeId::of::<()>())
2537 }
2538}
2539
2540pub fn list_all_registered_nodes() -> impl Iterator<Item = NodeHandle> {
2541 node_luts::modpath_by_node_typeid().keys().cloned()
2542}
2543
2544mod node_luts {
2559 use super::FlowNodeBase;
2560 use super::NodeHandle;
2561 use std::collections::HashMap;
2562 use std::sync::OnceLock;
2563
2564 pub(super) fn modpath_by_node_typeid() -> &'static HashMap<NodeHandle, &'static str> {
2565 static TYPEID_TO_MODPATH: OnceLock<HashMap<NodeHandle, &'static str>> = OnceLock::new();
2566
2567 TYPEID_TO_MODPATH.get_or_init(|| {
2568 let mut lookup = HashMap::new();
2569 for crate::node::private::FlowNodeMeta {
2570 module_path,
2571 ctor: _,
2572 typeid,
2573 } in crate::node::private::FLOW_NODES
2574 {
2575 let existing = lookup.insert(
2576 NodeHandle(*typeid),
2577 module_path
2578 .strip_suffix("::_only_one_call_to_flowey_node_per_module")
2579 .unwrap(),
2580 );
2581 assert!(existing.is_none())
2584 }
2585
2586 lookup
2587 })
2588 }
2589
2590 pub(super) fn erased_node_by_typeid()
2591 -> &'static HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>> {
2592 static LOOKUP: OnceLock<
2593 HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>>,
2594 > = OnceLock::new();
2595
2596 LOOKUP.get_or_init(|| {
2597 let mut lookup = HashMap::new();
2598 for crate::node::private::FlowNodeMeta {
2599 module_path: _,
2600 ctor,
2601 typeid,
2602 } in crate::node::private::FLOW_NODES
2603 {
2604 let existing = lookup.insert(NodeHandle(*typeid), *ctor);
2605 assert!(existing.is_none())
2608 }
2609
2610 lookup
2611 })
2612 }
2613
2614 pub(super) fn erased_node_by_modpath() -> &'static HashMap<
2615 &'static str,
2616 (
2617 NodeHandle,
2618 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2619 ),
2620 > {
2621 static MODPATH_LOOKUP: OnceLock<
2622 HashMap<
2623 &'static str,
2624 (
2625 NodeHandle,
2626 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2627 ),
2628 >,
2629 > = OnceLock::new();
2630
2631 MODPATH_LOOKUP.get_or_init(|| {
2632 let mut lookup = HashMap::new();
2633 for crate::node::private::FlowNodeMeta { module_path, ctor, typeid } in crate::node::private::FLOW_NODES {
2634 let existing = lookup.insert(module_path.strip_suffix("::_only_one_call_to_flowey_node_per_module").unwrap(), (NodeHandle(*typeid), *ctor));
2635 if existing.is_some() {
2636 panic!("conflicting node registrations at {module_path}! please ensure there is a single node per module!")
2637 }
2638 }
2639 lookup
2640 })
2641 }
2642}
2643
2644#[doc(hidden)]
2645pub mod private {
2646 pub use linkme;
2647
2648 pub struct FlowNodeMeta {
2649 pub module_path: &'static str,
2650 pub ctor: fn() -> Box<dyn super::FlowNodeBase<Request = Box<[u8]>>>,
2651 pub typeid: std::any::TypeId,
2652 }
2653
2654 #[linkme::distributed_slice]
2655 pub static FLOW_NODES: [FlowNodeMeta] = [..];
2656
2657 #[expect(unsafe_code)]
2659 #[linkme::distributed_slice(FLOW_NODES)]
2660 static DUMMY_FLOW_NODE: FlowNodeMeta = FlowNodeMeta {
2661 module_path: "<dummy>::_only_one_call_to_flowey_node_per_module",
2662 ctor: || unreachable!(),
2663 typeid: std::any::TypeId::of::<()>(),
2664 };
2665}
2666
2667#[doc(hidden)]
2668#[macro_export]
2669macro_rules! new_flow_node_base {
2670 (struct Node) => {
2671 #[non_exhaustive]
2673 pub struct Node;
2674
2675 mod _only_one_call_to_flowey_node_per_module {
2676 const _: () = {
2677 use $crate::node::private::linkme;
2678
2679 fn new_erased() -> Box<dyn $crate::node::FlowNodeBase<Request = Box<[u8]>>> {
2680 Box::new($crate::node::erased::ErasedNode(super::Node))
2681 }
2682
2683 #[linkme::distributed_slice($crate::node::private::FLOW_NODES)]
2684 #[linkme(crate = linkme)]
2685 static FLOW_NODE: $crate::node::private::FlowNodeMeta =
2686 $crate::node::private::FlowNodeMeta {
2687 module_path: module_path!(),
2688 ctor: new_erased,
2689 typeid: std::any::TypeId::of::<super::Node>(),
2690 };
2691 };
2692 }
2693 };
2694}
2695
2696pub trait FlowNode {
2783 type Request: Serialize + DeserializeOwned;
2787
2788 fn imports(ctx: &mut ImportCtx<'_>);
2800
2801 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2804}
2805
2806#[macro_export]
2807macro_rules! new_flow_node {
2808 (struct Node) => {
2809 $crate::new_flow_node_base!(struct Node);
2810
2811 impl $crate::node::FlowNodeBase for Node
2812 where
2813 Node: FlowNode,
2814 {
2815 type Request = <Node as FlowNode>::Request;
2816
2817 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2818 <Node as FlowNode>::imports(dep)
2819 }
2820
2821 fn emit(
2822 &mut self,
2823 _config_bytes: Vec<Box<[u8]>>,
2824 requests: Vec<Self::Request>,
2825 ctx: &mut $crate::node::NodeCtx<'_>,
2826 ) -> anyhow::Result<()> {
2827 <Node as FlowNode>::emit(requests, ctx)
2828 }
2829
2830 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2831 }
2832 };
2833}
2834
2835pub trait SimpleFlowNode {
2856 type Request: Serialize + DeserializeOwned;
2857
2858 fn imports(ctx: &mut ImportCtx<'_>);
2870
2871 fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2873}
2874
2875#[macro_export]
2876macro_rules! new_simple_flow_node {
2877 (struct Node) => {
2878 $crate::new_flow_node_base!(struct Node);
2879
2880 impl $crate::node::FlowNodeBase for Node
2881 where
2882 Node: $crate::node::SimpleFlowNode,
2883 {
2884 type Request = <Node as $crate::node::SimpleFlowNode>::Request;
2885
2886 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2887 <Node as $crate::node::SimpleFlowNode>::imports(dep)
2888 }
2889
2890 fn emit(
2891 &mut self,
2892 _config_bytes: Vec<Box<[u8]>>,
2893 requests: Vec<Self::Request>,
2894 ctx: &mut $crate::node::NodeCtx<'_>,
2895 ) -> anyhow::Result<()> {
2896 for req in requests {
2897 <Node as $crate::node::SimpleFlowNode>::process_request(req, ctx)?
2898 }
2899
2900 Ok(())
2901 }
2902
2903 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2904 }
2905 };
2906}
2907
2908pub trait FlowNodeWithConfig {
2955 type Request: Serialize + DeserializeOwned;
2957
2958 type Config: ConfigMerge;
2965
2966 fn imports(ctx: &mut ImportCtx<'_>);
2968
2969 fn emit(
2971 config: Self::Config,
2972 requests: Vec<Self::Request>,
2973 ctx: &mut NodeCtx<'_>,
2974 ) -> anyhow::Result<()>;
2975}
2976
2977#[macro_export]
2978macro_rules! new_flow_node_with_config {
2979 (struct Node) => {
2980 $crate::new_flow_node_base!(struct Node);
2981
2982 impl $crate::node::FlowNodeBase for Node
2983 where
2984 Node: $crate::node::FlowNodeWithConfig,
2985 {
2986 type Request = <Node as $crate::node::FlowNodeWithConfig>::Request;
2987
2988 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2989 <Node as $crate::node::FlowNodeWithConfig>::imports(dep)
2990 }
2991
2992 fn emit(
2993 &mut self,
2994 config_bytes: Vec<Box<[u8]>>,
2995 requests: Vec<Self::Request>,
2996 ctx: &mut $crate::node::NodeCtx<'_>,
2997 ) -> anyhow::Result<()> {
2998 use $crate::node::ConfigMerge;
2999
3000 type C = <Node as $crate::node::FlowNodeWithConfig>::Config;
3001
3002 let mut merged = <C as Default>::default();
3003 for bytes in config_bytes {
3004 let partial: C = serde_json::from_slice(&bytes)?;
3005 merged.merge(partial)?;
3006 }
3007
3008 <Node as $crate::node::FlowNodeWithConfig>::emit(merged, requests, ctx)
3009 }
3010
3011 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
3012 }
3013 };
3014}
3015
3016pub trait IntoRequest {
3024 type Node: FlowNodeBase;
3025 fn into_request(self) -> <Self::Node as FlowNodeBase>::Request;
3026
3027 #[doc(hidden)]
3030 #[expect(nonstandard_style)]
3031 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self);
3032}
3033
3034pub trait IntoConfig: Serialize {
3040 type Node: FlowNodeBase;
3041
3042 #[doc(hidden)]
3045 #[expect(nonstandard_style)]
3046 fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self);
3047}
3048
3049pub trait ConfigMerge: Serialize + DeserializeOwned + Default {
3052 fn merge(&mut self, other: Self) -> anyhow::Result<()>;
3055}
3056
3057pub trait ConfigField {
3064 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()>;
3065}
3066
3067impl<T: PartialEq> ConfigField for Option<T> {
3068 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3069 if let Some(new) = other {
3070 match self {
3071 None => *self = Some(new),
3072 Some(old) if *old == new => {}
3073 Some(_) => {
3074 anyhow::bail!("config field `{field_name}` mismatch");
3075 }
3076 }
3077 }
3078 Ok(())
3079 }
3080}
3081
3082impl<K: Ord + std::fmt::Debug, V: PartialEq> ConfigField for BTreeMap<K, V> {
3083 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3084 for (k, v) in other {
3085 use std::collections::btree_map::Entry;
3086 match self.entry(k) {
3087 Entry::Vacant(e) => {
3088 e.insert(v);
3089 }
3090 Entry::Occupied(e) if *e.get() == v => {}
3091 Entry::Occupied(e) => {
3092 anyhow::bail!("config field `{field_name}` mismatch for key {:?}", e.key(),);
3093 }
3094 }
3095 }
3096 Ok(())
3097 }
3098}
3099
3100#[doc(hidden)]
3101#[macro_export]
3102macro_rules! __flowey_request_inner {
3103 (@emit_struct [$req:ident]
3107 $(#[$a:meta])*
3108 $variant:ident($($tt:tt)*),
3109 $($rest:tt)*
3110 ) => {
3111 $(#[$a])*
3112 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3113 pub struct $variant($($tt)*);
3114
3115 impl IntoRequest for $variant {
3116 type Node = Node;
3117 fn into_request(self) -> $req {
3118 $req::$variant(self)
3119 }
3120 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3121 }
3122
3123 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3124 };
3125 (@emit_struct [$req:ident]
3126 $(#[$a:meta])*
3127 $variant:ident { $($tt:tt)* },
3128 $($rest:tt)*
3129 ) => {
3130 $(#[$a])*
3131 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3132 pub struct $variant {
3133 $($tt)*
3134 }
3135
3136 impl IntoRequest for $variant {
3137 type Node = Node;
3138 fn into_request(self) -> $req {
3139 $req::$variant(self)
3140 }
3141 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3142 }
3143
3144 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3145 };
3146 (@emit_struct [$req:ident]
3147 $(#[$a:meta])*
3148 $variant:ident,
3149 $($rest:tt)*
3150 ) => {
3151 $(#[$a])*
3152 #[derive(Serialize, Deserialize)]
3153 pub struct $variant;
3154
3155 impl IntoRequest for $variant {
3156 type Node = Node;
3157 fn into_request(self) -> $req {
3158 $req::$variant(self)
3159 }
3160 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3161 }
3162
3163 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3164 };
3165 (@emit_struct [$req:ident]
3166 ) => {};
3167
3168 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3172 $(#[$a:meta])*
3173 $variant:ident($($tt:tt)*),
3174 $($rest:tt)*
3175 ) => {
3176 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3177 };
3178 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3179 $(#[$a:meta])*
3180 $variant:ident { $($tt:tt)* },
3181 $($rest:tt)*
3182 ) => {
3183 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3184 };
3185 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3186 $(#[$a:meta])*
3187 $variant:ident,
3188 $($rest:tt)*
3189 ) => {
3190 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3191 };
3192 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3193 ) => {
3194 #[derive(Serialize, Deserialize)]
3195 pub enum $req {$(
3196 $(#[$prev_a])*
3197 $prev(self::req::$prev),
3198 )*}
3199
3200 impl IntoRequest for $req {
3201 type Node = Node;
3202 fn into_request(self) -> $req {
3203 self
3204 }
3205 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3206 }
3207 };
3208}
3209
3210#[macro_export]
3258macro_rules! flowey_request {
3259 (
3260 $(#[$root_a:meta])*
3261 pub enum_struct $req:ident {
3262 $($tt:tt)*
3263 }
3264 ) => {
3265 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*),] $($tt)*);
3266 pub mod req {
3267 use super::*;
3268 $crate::__flowey_request_inner!(@emit_struct [$req] $($tt)*);
3269 }
3270 };
3271
3272 (
3273 $(#[$a:meta])*
3274 pub enum $req:ident {
3275 $($tt:tt)*
3276 }
3277 ) => {
3278 $(#[$a])*
3279 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3280 pub enum $req {
3281 $($tt)*
3282 }
3283
3284 impl $crate::node::IntoRequest for $req {
3285 type Node = Node;
3286 fn into_request(self) -> $req {
3287 self
3288 }
3289 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3290 }
3291 };
3292
3293 (
3294 $(#[$a:meta])*
3295 pub struct $req:ident {
3296 $($tt:tt)*
3297 }
3298 ) => {
3299 $(#[$a])*
3300 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3301 pub struct $req {
3302 $($tt)*
3303 }
3304
3305 impl $crate::node::IntoRequest for $req {
3306 type Node = Node;
3307 fn into_request(self) -> $req {
3308 self
3309 }
3310 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3311 }
3312 };
3313
3314 (
3315 $(#[$a:meta])*
3316 pub struct $req:ident($($tt:tt)*);
3317 ) => {
3318 $(#[$a])*
3319 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3320 pub struct $req($($tt)*);
3321
3322 impl $crate::node::IntoRequest for $req {
3323 type Node = Node;
3324 fn into_request(self) -> $req {
3325 self
3326 }
3327 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3328 }
3329 };
3330}
3331
3332#[macro_export]
3370macro_rules! flowey_config {
3371 (
3372 $(#[$meta:meta])*
3373 pub struct $Config:ident {
3374 $(
3375 $(#[$field_meta:meta])*
3376 pub $field:ident : $ty:ty
3377 ),* $(,)?
3378 }
3379 ) => {
3380 $(#[$meta])*
3381 #[derive(
3382 $crate::reexports::Serialize,
3383 $crate::reexports::Deserialize,
3384 Default,
3385 )]
3386 pub struct $Config {
3387 $(
3388 $(#[$field_meta])*
3389 pub $field: $ty,
3390 )*
3391 }
3392
3393 impl $crate::node::ConfigMerge for $Config {
3394 fn merge(&mut self, other: Self) -> anyhow::Result<()> {
3395 $(
3396 $crate::node::ConfigField::merge_field(
3397 &mut self.$field,
3398 stringify!($field),
3399 other.$field,
3400 )?;
3401 )*
3402 Ok(())
3403 }
3404 }
3405
3406 impl $crate::node::IntoConfig for $Config {
3407 type Node = Node;
3408
3409 fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self) {}
3410 }
3411 };
3412}
3413
3414#[macro_export]
3431macro_rules! shell_cmd {
3432 ($rt:expr, $cmd:literal) => {{
3433 let flowey_sh = &$rt.sh;
3434 #[expect(clippy::disallowed_macros)]
3435 flowey_sh.wrap($crate::reexports::xshell::cmd!(flowey_sh.xshell(), $cmd))
3436 }};
3437}