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 pub use crate::pipeline::resolve as resolve_artifact;
76
77 pub fn same_across_all_reqs<T: PartialEq>(
109 req_name: &str,
110 var: &mut Option<T>,
111 new: T,
112 ) -> anyhow::Result<()> {
113 match (var.as_ref(), new) {
114 (None, v) => *var = Some(v),
115 (Some(old), new) => {
116 if *old != new {
117 anyhow::bail!("`{}` must be consistent across requests", req_name);
118 }
119 }
120 }
121
122 Ok(())
123 }
124
125 pub fn same_across_all_reqs_backing_var<V: VarEqBacking>(
129 req_name: &str,
130 var: &mut Option<V>,
131 new: V,
132 ) -> anyhow::Result<()> {
133 match (var.as_ref(), new) {
134 (None, v) => *var = Some(v),
135 (Some(old), new) => {
136 if !old.eq(&new) {
137 anyhow::bail!("`{}` must be consistent across requests", req_name);
138 }
139 }
140 }
141
142 Ok(())
143 }
144
145 #[macro_export]
149 macro_rules! match_arch {
150 ($host_arch:expr, $match_arch:pat, $expr:expr) => {
151 if matches!($host_arch, $match_arch) {
152 $expr
153 } else {
154 anyhow::bail!("Linux distro not supported on host arch {}", $host_arch);
155 }
156 };
157 }
158
159 #[macro_export]
161 macro_rules! claim_vars {
162 ($ctx:ident, ($($var:ident),* $(,)?)) => {
163 $(let $var = $var.claim($ctx);)*
164 };
165 }
166
167 #[macro_export]
169 macro_rules! read_vars {
170 ($rt:ident, ($($var:ident),* $(,)?)) => {
171 $(let $var = $rt.read($var);)*
172 };
173 }
174}
175
176pub trait VarEqBacking {
200 fn eq(&self, other: &Self) -> bool;
202}
203
204impl<T> VarEqBacking for WriteVar<T>
205where
206 T: Serialize + DeserializeOwned,
207{
208 fn eq(&self, other: &Self) -> bool {
209 self.backing_var == other.backing_var
210 }
211}
212
213impl<T> VarEqBacking for ReadVar<T>
214where
215 T: Serialize + DeserializeOwned + PartialEq + Eq + Clone,
216{
217 fn eq(&self, other: &Self) -> bool {
218 self.backing_var == other.backing_var
219 }
220}
221
222impl<T, U> VarEqBacking for (T, U)
224where
225 T: VarEqBacking,
226 U: VarEqBacking,
227{
228 fn eq(&self, other: &Self) -> bool {
229 (self.0.eq(&other.0)) && (self.1.eq(&other.1))
230 }
231}
232
233#[derive(Serialize, Deserialize)]
251#[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))]
252pub struct ConfigVar<T>(pub ReadVar<T>);
253
254impl<T: Serialize + DeserializeOwned> Clone for ConfigVar<T> {
255 fn clone(&self) -> Self {
256 ConfigVar(self.0.clone())
257 }
258}
259
260impl<T> std::fmt::Debug for ConfigVar<T> {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 f.debug_tuple("ConfigVar").finish()
263 }
264}
265
266impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> PartialEq for ConfigVar<T> {
267 fn eq(&self, other: &Self) -> bool {
268 VarEqBacking::eq(&self.0, &other.0)
269 }
270}
271
272impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> ClaimVar for ConfigVar<T> {
273 type Claimed = ClaimedReadVar<T>;
274
275 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
276 self.0.claim(ctx)
277 }
278}
279
280impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> From<ReadVar<T>> for ConfigVar<T> {
281 fn from(v: ReadVar<T>) -> Self {
282 ConfigVar(v)
283 }
284}
285
286pub type SideEffect = ();
293
294#[derive(Clone, Debug, Serialize, Deserialize)]
297pub enum VarNotClaimed {}
298
299#[derive(Clone, Debug, Serialize, Deserialize)]
302pub enum VarClaimed {}
303
304#[derive(Debug, Serialize, Deserialize)]
324pub struct WriteVar<T: Serialize + DeserializeOwned, C = VarNotClaimed> {
325 backing_var: String,
326 is_side_effect: bool,
329
330 #[serde(skip)]
331 _kind: core::marker::PhantomData<(T, C)>,
332}
333
334pub type ClaimedWriteVar<T> = WriteVar<T, VarClaimed>;
337
338impl<T: Serialize + DeserializeOwned> WriteVar<T, VarNotClaimed> {
339 fn into_claimed(self) -> WriteVar<T, VarClaimed> {
341 let Self {
342 backing_var,
343 is_side_effect,
344 _kind,
345 } = self;
346
347 WriteVar {
348 backing_var,
349 is_side_effect,
350 _kind: std::marker::PhantomData,
351 }
352 }
353
354 #[track_caller]
356 pub fn write_static(self, ctx: &mut NodeCtx<'_>, val: T)
357 where
358 T: 'static,
359 {
360 let val = ReadVar::from_static(val);
361 val.write_into(ctx, self);
362 }
363
364 pub(crate) fn into_json(self) -> WriteVar<serde_json::Value> {
365 WriteVar {
366 backing_var: self.backing_var,
367 is_side_effect: self.is_side_effect,
368 _kind: std::marker::PhantomData,
369 }
370 }
371}
372
373impl WriteVar<SideEffect, VarNotClaimed> {
374 pub fn discard_result<T: Serialize + DeserializeOwned>(self) -> WriteVar<T> {
379 WriteVar {
380 backing_var: self.backing_var,
381 is_side_effect: true,
382 _kind: std::marker::PhantomData,
383 }
384 }
385}
386
387pub trait ClaimVar {
395 type Claimed;
397 fn claim(self, ctx: &mut StepCtx<'_>) -> Self::Claimed;
399}
400
401pub trait ReadVarValue {
407 type Value;
409 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value;
411}
412
413impl<T: Serialize + DeserializeOwned> ClaimVar for ReadVar<T> {
414 type Claimed = ClaimedReadVar<T>;
415
416 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedReadVar<T> {
417 if let ReadVarBacking::RuntimeVar {
418 var,
419 is_side_effect: _,
420 } = &self.backing_var
421 {
422 ctx.backend.borrow_mut().on_claimed_runtime_var(var, true);
423 }
424 self.into_claimed()
425 }
426}
427
428impl<T: Serialize + DeserializeOwned> ClaimVar for WriteVar<T> {
429 type Claimed = ClaimedWriteVar<T>;
430
431 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedWriteVar<T> {
432 ctx.backend
433 .borrow_mut()
434 .on_claimed_runtime_var(&self.backing_var, false);
435 self.into_claimed()
436 }
437}
438
439impl<T: Serialize + DeserializeOwned> ReadVarValue for ClaimedReadVar<T> {
440 type Value = T;
441
442 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
443 match self.backing_var {
444 ReadVarBacking::RuntimeVar {
445 var,
446 is_side_effect,
447 } => {
448 let data = rt.get_var(&var, is_side_effect);
450 if is_side_effect {
451 serde_json::from_slice(b"null").expect("should be deserializing into ()")
455 } else {
456 serde_json::from_slice(&data).expect("improve this error path")
458 }
459 }
460 ReadVarBacking::Inline(val) => val,
461 }
462 }
463}
464
465impl<T: ClaimVar> ClaimVar for Vec<T> {
466 type Claimed = Vec<T::Claimed>;
467
468 fn claim(self, ctx: &mut StepCtx<'_>) -> Vec<T::Claimed> {
469 self.into_iter().map(|v| v.claim(ctx)).collect()
470 }
471}
472
473impl<T: ReadVarValue> ReadVarValue for Vec<T> {
474 type Value = Vec<T::Value>;
475
476 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
477 self.into_iter().map(|v| v.read_value(rt)).collect()
478 }
479}
480
481impl<T: ClaimVar> ClaimVar for Option<T> {
482 type Claimed = Option<T::Claimed>;
483
484 fn claim(self, ctx: &mut StepCtx<'_>) -> Option<T::Claimed> {
485 self.map(|x| x.claim(ctx))
486 }
487}
488
489impl<T: ReadVarValue> ReadVarValue for Option<T> {
490 type Value = Option<T::Value>;
491
492 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
493 self.map(|x| x.read_value(rt))
494 }
495}
496
497impl<U: Ord, T: ClaimVar> ClaimVar for BTreeMap<U, T> {
498 type Claimed = BTreeMap<U, T::Claimed>;
499
500 fn claim(self, ctx: &mut StepCtx<'_>) -> BTreeMap<U, T::Claimed> {
501 self.into_iter().map(|(k, v)| (k, v.claim(ctx))).collect()
502 }
503}
504
505impl<U: Ord, T: ReadVarValue> ReadVarValue for BTreeMap<U, T> {
506 type Value = BTreeMap<U, T::Value>;
507
508 fn read_value(self, rt: &mut RustRuntimeServices<'_>) -> Self::Value {
509 self.into_iter()
510 .map(|(k, v)| (k, v.read_value(rt)))
511 .collect()
512 }
513}
514
515macro_rules! impl_tuple_claim {
516 ($($T:tt)*) => {
517 impl<$($T,)*> $crate::node::ClaimVar for ($($T,)*)
518 where
519 $($T: $crate::node::ClaimVar,)*
520 {
521 type Claimed = ($($T::Claimed,)*);
522
523 #[expect(non_snake_case)]
524 fn claim(self, ctx: &mut $crate::node::StepCtx<'_>) -> Self::Claimed {
525 let ($($T,)*) = self;
526 ($($T.claim(ctx),)*)
527 }
528 }
529
530 impl<$($T,)*> $crate::node::ReadVarValue for ($($T,)*)
531 where
532 $($T: $crate::node::ReadVarValue,)*
533 {
534 type Value = ($($T::Value,)*);
535
536 #[expect(non_snake_case)]
537 fn read_value(self, rt: &mut $crate::node::RustRuntimeServices<'_>) -> Self::Value {
538 let ($($T,)*) = self;
539 ($($T.read_value(rt),)*)
540 }
541 }
542 };
543}
544
545impl_tuple_claim!(A B C D E F G H I J);
546impl_tuple_claim!(A B C D E F G H I);
547impl_tuple_claim!(A B C D E F G H);
548impl_tuple_claim!(A B C D E F G);
549impl_tuple_claim!(A B C D E F);
550impl_tuple_claim!(A B C D E);
551impl_tuple_claim!(A B C D);
552impl_tuple_claim!(A B C);
553impl_tuple_claim!(A B);
554impl_tuple_claim!(A);
555
556impl ClaimVar for () {
557 type Claimed = ();
558
559 fn claim(self, _ctx: &mut StepCtx<'_>) -> Self::Claimed {}
560}
561
562impl ReadVarValue for () {
563 type Value = ();
564
565 fn read_value(self, _rt: &mut RustRuntimeServices<'_>) -> Self::Value {}
566}
567
568#[derive(Serialize, Deserialize, Clone)]
573pub struct GhUserSecretVar(pub(crate) String);
574
575#[derive(Debug, Serialize, Deserialize)]
594pub struct ReadVar<T, C = VarNotClaimed> {
595 backing_var: ReadVarBacking<T>,
596 #[serde(skip)]
597 _kind: std::marker::PhantomData<C>,
598}
599
600pub type ClaimedReadVar<T> = ReadVar<T, VarClaimed>;
603
604impl<T: Serialize + DeserializeOwned, C> Clone for ReadVar<T, C> {
606 fn clone(&self) -> Self {
607 ReadVar {
608 backing_var: self.backing_var.clone(),
609 _kind: std::marker::PhantomData,
610 }
611 }
612}
613
614#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
615enum ReadVarBacking<T> {
616 RuntimeVar {
617 var: String,
618 is_side_effect: bool,
625 },
626 Inline(T),
627}
628
629impl<T: Serialize + DeserializeOwned> Clone for ReadVarBacking<T> {
631 fn clone(&self) -> Self {
632 match self {
633 Self::RuntimeVar {
634 var,
635 is_side_effect,
636 } => Self::RuntimeVar {
637 var: var.clone(),
638 is_side_effect: *is_side_effect,
639 },
640 Self::Inline(v) => {
641 Self::Inline(serde_json::from_value(serde_json::to_value(v).unwrap()).unwrap())
642 }
643 }
644 }
645}
646
647impl<T: Serialize + DeserializeOwned> ReadVar<T> {
648 fn into_claimed(self) -> ReadVar<T, VarClaimed> {
650 let Self { backing_var, _kind } = self;
651
652 ReadVar {
653 backing_var,
654 _kind: std::marker::PhantomData,
655 }
656 }
657
658 #[must_use]
667 pub fn into_side_effect(self) -> ReadVar<SideEffect> {
668 ReadVar {
669 backing_var: match self.backing_var {
670 ReadVarBacking::RuntimeVar {
671 var,
672 is_side_effect: _,
673 } => ReadVarBacking::RuntimeVar {
674 var,
675 is_side_effect: true,
676 },
677 ReadVarBacking::Inline(_) => ReadVarBacking::Inline(()),
678 },
679 _kind: std::marker::PhantomData,
680 }
681 }
682
683 #[track_caller]
686 #[must_use]
687 pub fn map<F, U>(&self, ctx: &mut NodeCtx<'_>, f: F) -> ReadVar<U>
688 where
689 T: 'static,
690 U: Serialize + DeserializeOwned + 'static,
691 F: FnOnce(T) -> U + 'static,
692 {
693 let (read_from, write_into) = ctx.new_var();
694 self.write_into_with(ctx, write_into, f);
695 read_from
696 }
697
698 #[track_caller]
701 pub fn write_into_with<F, U>(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<U>, f: F)
702 where
703 T: 'static,
704 U: Serialize + DeserializeOwned + 'static,
705 F: FnOnce(T) -> U + 'static,
706 {
707 let this = self.clone();
708 ctx.emit_minor_rust_step("🌼 write_into Var", move |ctx| {
709 let this = this.claim(ctx);
710 let write_into = write_into.claim(ctx);
711 move |rt| {
712 let this = rt.read(this);
713 rt.write(write_into, &f(this));
714 }
715 });
716 }
717
718 #[track_caller]
720 pub fn write_into(&self, ctx: &mut NodeCtx<'_>, write_into: WriteVar<T>)
721 where
722 T: 'static,
723 {
724 self.write_into_with(ctx, write_into, |x| x);
725 }
726
727 #[track_caller]
730 #[must_use]
731 pub fn zip<U>(&self, ctx: &mut NodeCtx<'_>, other: ReadVar<U>) -> ReadVar<(T, U)>
732 where
733 T: 'static,
734 U: Serialize + DeserializeOwned + 'static,
735 {
736 let (read_from, write_into) = ctx.new_var();
737 let this = self.clone();
738 ctx.emit_minor_rust_step("🌼 Zip Vars", move |ctx| {
739 let this = this.claim(ctx);
740 let other = other.claim(ctx);
741 let write_into = write_into.claim(ctx);
742 move |rt| {
743 let this = rt.read(this);
744 let other = rt.read(other);
745 rt.write(write_into, &(this, other));
746 }
747 });
748 read_from
749 }
750
751 #[track_caller]
756 #[must_use]
757 pub fn from_static(val: T) -> ReadVar<T>
758 where
759 T: 'static,
760 {
761 ReadVar {
762 backing_var: ReadVarBacking::Inline(val),
763 _kind: std::marker::PhantomData,
764 }
765 }
766
767 pub fn get_static(&self) -> Option<T> {
776 match self.clone().backing_var {
777 ReadVarBacking::Inline(v) => Some(v),
778 _ => None,
779 }
780 }
781
782 #[track_caller]
784 #[must_use]
785 pub fn transpose_vec(ctx: &mut NodeCtx<'_>, vec: Vec<ReadVar<T>>) -> ReadVar<Vec<T>>
786 where
787 T: 'static,
788 {
789 let (read_from, write_into) = ctx.new_var();
790 ctx.emit_minor_rust_step("🌼 Transpose Vec<ReadVar<T>>", move |ctx| {
791 let vec = vec.claim(ctx);
792 let write_into = write_into.claim(ctx);
793 move |rt| {
794 let mut v = Vec::new();
795 for var in vec {
796 v.push(rt.read(var));
797 }
798 rt.write(write_into, &v);
799 }
800 });
801 read_from
802 }
803
804 #[must_use]
820 pub fn depending_on<U>(&self, ctx: &mut NodeCtx<'_>, other: &ReadVar<U>) -> Self
821 where
822 T: 'static,
823 U: Serialize + DeserializeOwned + 'static,
824 {
825 ctx.emit_minor_rust_stepv("🌼 Add dependency", |ctx| {
828 let this = self.clone().claim(ctx);
829 other.clone().claim(ctx);
830 move |rt| rt.read(this)
831 })
832 }
833
834 pub fn claim_unused(self, ctx: &mut NodeCtx<'_>) {
837 match self.backing_var {
838 ReadVarBacking::RuntimeVar {
839 var,
840 is_side_effect: _,
841 } => ctx.backend.borrow_mut().on_unused_read_var(&var),
842 ReadVarBacking::Inline(_) => {}
843 }
844 }
845
846 pub(crate) fn into_json(self) -> ReadVar<serde_json::Value> {
847 match self.backing_var {
848 ReadVarBacking::RuntimeVar {
849 var,
850 is_side_effect,
851 } => ReadVar {
852 backing_var: ReadVarBacking::RuntimeVar {
853 var,
854 is_side_effect,
855 },
856 _kind: std::marker::PhantomData,
857 },
858 ReadVarBacking::Inline(v) => ReadVar {
859 backing_var: ReadVarBacking::Inline(serde_json::to_value(v).unwrap()),
860 _kind: std::marker::PhantomData,
861 },
862 }
863 }
864}
865
866#[must_use]
872pub fn thin_air_read_runtime_var<T>(backing_var: String) -> ReadVar<T>
873where
874 T: Serialize + DeserializeOwned,
875{
876 ReadVar {
877 backing_var: ReadVarBacking::RuntimeVar {
878 var: backing_var,
879 is_side_effect: false,
880 },
881 _kind: std::marker::PhantomData,
882 }
883}
884
885#[must_use]
891pub fn thin_air_write_runtime_var<T>(backing_var: String) -> WriteVar<T>
892where
893 T: Serialize + DeserializeOwned,
894{
895 WriteVar {
896 backing_var,
897 is_side_effect: false,
898 _kind: std::marker::PhantomData,
899 }
900}
901
902pub fn read_var_internals<T: Serialize + DeserializeOwned, C>(
908 var: &ReadVar<T, C>,
909) -> (Option<String>, bool) {
910 match var.backing_var {
911 ReadVarBacking::RuntimeVar {
912 var: ref s,
913 is_side_effect,
914 } => (Some(s.clone()), is_side_effect),
915 ReadVarBacking::Inline(_) => (None, false),
916 }
917}
918
919pub trait ImportCtxBackend {
920 fn on_possible_dep(&mut self, node_handle: NodeHandle);
921}
922
923pub struct ImportCtx<'a> {
925 backend: &'a mut dyn ImportCtxBackend,
926}
927
928impl ImportCtx<'_> {
929 pub fn import<N: FlowNodeBase + 'static>(&mut self) {
931 self.backend.on_possible_dep(NodeHandle::from_type::<N>())
932 }
933}
934
935pub fn new_import_ctx(backend: &mut dyn ImportCtxBackend) -> ImportCtx<'_> {
936 ImportCtx { backend }
937}
938
939pub trait NodeCtxBackend {
940 fn current_node(&self) -> NodeHandle;
942
943 fn on_new_var(&mut self) -> String;
948
949 fn on_claimed_runtime_var(&mut self, var: &str, is_read: bool);
951
952 fn on_unused_read_var(&mut self, var: &str);
954
955 fn on_request(&mut self, node_handle: NodeHandle, req: anyhow::Result<Box<[u8]>>);
963
964 fn on_config(&mut self, node_handle: NodeHandle, config: anyhow::Result<Box<[u8]>>);
968
969 fn on_emit_rust_step(
970 &mut self,
971 label: &str,
972 can_merge: bool,
973 code: Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
974 );
975
976 fn on_emit_ado_step(
977 &mut self,
978 label: &str,
979 yaml_snippet: Box<dyn for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String>,
980 inline_script: Option<
981 Box<dyn for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>>,
982 >,
983 condvar: Option<String>,
984 );
985
986 fn on_emit_gh_step(
987 &mut self,
988 label: &str,
989 uses: &str,
990 with: BTreeMap<String, ClaimedGhParam>,
991 condvar: Option<String>,
992 outputs: BTreeMap<String, Vec<GhOutput>>,
993 permissions: BTreeMap<GhPermission, GhPermissionValue>,
994 gh_to_rust: Vec<GhToRust>,
995 rust_to_gh: Vec<RustToGh>,
996 );
997
998 fn on_emit_side_effect_step(&mut self);
999
1000 fn backend(&mut self) -> FlowBackend;
1001 fn platform(&mut self) -> FlowPlatform;
1002 fn arch(&mut self) -> FlowArch;
1003
1004 fn persistent_dir_path_var(&mut self) -> Option<String>;
1008}
1009
1010pub fn new_node_ctx(backend: &mut dyn NodeCtxBackend) -> NodeCtx<'_> {
1011 NodeCtx {
1012 backend: Rc::new(RefCell::new(backend)),
1013 }
1014}
1015
1016#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1018pub enum FlowBackend {
1019 Local,
1021 Ado,
1023 Github,
1025}
1026
1027#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1029pub enum FlowPlatformKind {
1030 Windows,
1031 Unix,
1032}
1033
1034#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1036pub enum FlowPlatformLinuxDistro {
1037 Fedora,
1039 Ubuntu,
1041 AzureLinux,
1043 Arch,
1045 OpenSuse,
1047 Nix,
1049 Unknown,
1051}
1052
1053#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1055#[non_exhaustive]
1056pub enum FlowPlatform {
1057 Windows,
1059 Linux(FlowPlatformLinuxDistro),
1061 MacOs,
1063}
1064
1065impl FlowPlatform {
1066 pub fn kind(&self) -> FlowPlatformKind {
1067 match self {
1068 Self::Windows => FlowPlatformKind::Windows,
1069 Self::Linux(_) | Self::MacOs => FlowPlatformKind::Unix,
1070 }
1071 }
1072
1073 fn as_str(&self) -> &'static str {
1074 match self {
1075 Self::Windows => "windows",
1076 Self::Linux(_) => "linux",
1077 Self::MacOs => "macos",
1078 }
1079 }
1080
1081 pub fn exe_suffix(&self) -> &'static str {
1083 if self == &Self::Windows { ".exe" } else { "" }
1084 }
1085
1086 pub fn binary(&self, name: &str) -> String {
1088 format!("{}{}", name, self.exe_suffix())
1089 }
1090}
1091
1092impl std::fmt::Display for FlowPlatform {
1093 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1094 f.pad(self.as_str())
1095 }
1096}
1097
1098#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1100#[non_exhaustive]
1101pub enum FlowArch {
1102 X86_64,
1103 Aarch64,
1104}
1105
1106impl FlowArch {
1107 fn as_str(&self) -> &'static str {
1108 match self {
1109 Self::X86_64 => "x86_64",
1110 Self::Aarch64 => "aarch64",
1111 }
1112 }
1113}
1114
1115impl std::fmt::Display for FlowArch {
1116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1117 f.pad(self.as_str())
1118 }
1119}
1120
1121pub struct StepCtx<'a> {
1123 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1124}
1125
1126impl StepCtx<'_> {
1127 pub fn backend(&self) -> FlowBackend {
1130 self.backend.borrow_mut().backend()
1131 }
1132
1133 pub fn platform(&self) -> FlowPlatform {
1136 self.backend.borrow_mut().platform()
1137 }
1138}
1139
1140const NO_ADO_INLINE_SCRIPT: Option<
1141 for<'a> fn(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()>,
1142> = None;
1143
1144pub struct NodeCtx<'a> {
1146 backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
1147}
1148
1149impl<'ctx> NodeCtx<'ctx> {
1150 pub fn emit_rust_step<F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<SideEffect>
1156 where
1157 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1158 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1159 {
1160 self.emit_rust_step_inner(label.as_ref(), false, code)
1161 }
1162
1163 pub fn emit_minor_rust_step<F, G>(
1169 &mut self,
1170 label: impl AsRef<str>,
1171 code: F,
1172 ) -> ReadVar<SideEffect>
1173 where
1174 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1175 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) + 'static,
1176 {
1177 self.emit_rust_step_inner(label.as_ref(), true, |ctx| {
1178 let f = code(ctx);
1179 |rt| {
1180 f(rt);
1181 Ok(())
1182 }
1183 })
1184 }
1185
1186 #[must_use]
1207 #[track_caller]
1208 pub fn emit_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1209 where
1210 T: Serialize + DeserializeOwned + 'static,
1211 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1212 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1213 {
1214 self.emit_rust_stepv_inner(label.as_ref(), false, code)
1215 }
1216
1217 #[must_use]
1241 #[track_caller]
1242 pub fn emit_minor_rust_stepv<T, F, G>(&mut self, label: impl AsRef<str>, code: F) -> ReadVar<T>
1243 where
1244 T: Serialize + DeserializeOwned + 'static,
1245 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1246 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> T + 'static,
1247 {
1248 self.emit_rust_stepv_inner(label.as_ref(), true, |ctx| {
1249 let f = code(ctx);
1250 |rt| Ok(f(rt))
1251 })
1252 }
1253
1254 fn emit_rust_step_inner<F, G>(
1255 &mut self,
1256 label: &str,
1257 can_merge: bool,
1258 code: F,
1259 ) -> ReadVar<SideEffect>
1260 where
1261 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1262 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1263 {
1264 let (read, write) = self.new_prefixed_var("auto_se");
1265
1266 let ctx = &mut StepCtx {
1267 backend: self.backend.clone(),
1268 };
1269 write.claim(ctx);
1270
1271 let code = code(ctx);
1272 self.backend
1273 .borrow_mut()
1274 .on_emit_rust_step(label.as_ref(), can_merge, Box::new(code));
1275 read
1276 }
1277
1278 #[must_use]
1279 #[track_caller]
1280 fn emit_rust_stepv_inner<T, F, G>(
1281 &mut self,
1282 label: impl AsRef<str>,
1283 can_merge: bool,
1284 code: F,
1285 ) -> ReadVar<T>
1286 where
1287 T: Serialize + DeserializeOwned + 'static,
1288 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1289 G: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<T> + 'static,
1290 {
1291 let (read, write) = self.new_var();
1292
1293 let ctx = &mut StepCtx {
1294 backend: self.backend.clone(),
1295 };
1296 let write = write.claim(ctx);
1297
1298 let code = code(ctx);
1299 self.backend.borrow_mut().on_emit_rust_step(
1300 label.as_ref(),
1301 can_merge,
1302 Box::new(|rt| {
1303 let val = code(rt)?;
1304 rt.write(write, &val);
1305 Ok(())
1306 }),
1307 );
1308 read
1309 }
1310
1311 #[track_caller]
1313 #[must_use]
1314 pub fn get_ado_variable(&mut self, ado_var: AdoRuntimeVar) -> ReadVar<String> {
1315 let (var, write_var) = self.new_var();
1316 self.emit_ado_step(format!("🌼 read {}", ado_var.as_raw_var_name()), |ctx| {
1317 let write_var = write_var.claim(ctx);
1318 |rt| {
1319 rt.set_var(write_var, ado_var);
1320 "".into()
1321 }
1322 });
1323 var
1324 }
1325
1326 pub fn emit_ado_step<F, G>(&mut self, display_name: impl AsRef<str>, yaml_snippet: F)
1328 where
1329 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1330 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1331 {
1332 self.emit_ado_step_inner(display_name, None, |ctx| {
1333 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1334 })
1335 }
1336
1337 pub fn emit_ado_step_with_condition<F, G>(
1340 &mut self,
1341 display_name: impl AsRef<str>,
1342 cond: ReadVar<bool>,
1343 yaml_snippet: F,
1344 ) where
1345 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1346 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1347 {
1348 self.emit_ado_step_inner(display_name, Some(cond), |ctx| {
1349 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1350 })
1351 }
1352
1353 pub fn emit_ado_step_with_condition_optional<F, G>(
1356 &mut self,
1357 display_name: impl AsRef<str>,
1358 cond: Option<ReadVar<bool>>,
1359 yaml_snippet: F,
1360 ) where
1361 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> G,
1362 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1363 {
1364 self.emit_ado_step_inner(display_name, cond, |ctx| {
1365 (yaml_snippet(ctx), NO_ADO_INLINE_SCRIPT)
1366 })
1367 }
1368
1369 pub fn emit_ado_step_with_inline_script<F, G, H>(
1398 &mut self,
1399 display_name: impl AsRef<str>,
1400 yaml_snippet: F,
1401 ) where
1402 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, H),
1403 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1404 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1405 {
1406 self.emit_ado_step_inner(display_name, None, |ctx| {
1407 let (f, g) = yaml_snippet(ctx);
1408 (f, Some(g))
1409 })
1410 }
1411
1412 fn emit_ado_step_inner<F, G, H>(
1413 &mut self,
1414 display_name: impl AsRef<str>,
1415 cond: Option<ReadVar<bool>>,
1416 yaml_snippet: F,
1417 ) where
1418 F: for<'a> FnOnce(&'a mut StepCtx<'_>) -> (G, Option<H>),
1419 G: for<'a> FnOnce(&'a mut AdoStepServices<'_>) -> String + 'static,
1420 H: for<'a> FnOnce(&'a mut RustRuntimeServices<'_>) -> anyhow::Result<()> + 'static,
1421 {
1422 let condvar = match cond.map(|c| c.backing_var) {
1423 Some(ReadVarBacking::Inline(cond)) => {
1425 if !cond {
1426 return;
1427 } else {
1428 None
1429 }
1430 }
1431 Some(ReadVarBacking::RuntimeVar {
1432 var,
1433 is_side_effect,
1434 }) => {
1435 assert!(!is_side_effect);
1436 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1437 Some(var)
1438 }
1439 None => None,
1440 };
1441
1442 let (yaml_snippet, inline_script) = yaml_snippet(&mut StepCtx {
1443 backend: self.backend.clone(),
1444 });
1445 self.backend.borrow_mut().on_emit_ado_step(
1446 display_name.as_ref(),
1447 Box::new(yaml_snippet),
1448 if let Some(inline_script) = inline_script {
1449 Some(Box::new(inline_script))
1450 } else {
1451 None
1452 },
1453 condvar,
1454 );
1455 }
1456
1457 #[track_caller]
1459 #[must_use]
1460 pub fn get_gh_context_var(&mut self) -> GhContextVarReader<'ctx, Root> {
1461 GhContextVarReader {
1462 ctx: NodeCtx {
1463 backend: self.backend.clone(),
1464 },
1465 _state: std::marker::PhantomData,
1466 }
1467 }
1468
1469 pub fn emit_gh_step(
1471 &mut self,
1472 display_name: impl AsRef<str>,
1473 uses: impl AsRef<str>,
1474 ) -> GhStepBuilder {
1475 GhStepBuilder::new(display_name, uses)
1476 }
1477
1478 fn emit_gh_step_inner(
1479 &mut self,
1480 display_name: impl AsRef<str>,
1481 cond: Option<ReadVar<bool>>,
1482 uses: impl AsRef<str>,
1483 with: Option<BTreeMap<String, GhParam>>,
1484 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
1485 run_after: Vec<ReadVar<SideEffect>>,
1486 permissions: BTreeMap<GhPermission, GhPermissionValue>,
1487 ) {
1488 let condvar = match cond.map(|c| c.backing_var) {
1489 Some(ReadVarBacking::Inline(cond)) => {
1491 if !cond {
1492 return;
1493 } else {
1494 None
1495 }
1496 }
1497 Some(ReadVarBacking::RuntimeVar {
1498 var,
1499 is_side_effect,
1500 }) => {
1501 assert!(!is_side_effect);
1502 self.backend.borrow_mut().on_claimed_runtime_var(&var, true);
1503 Some(var)
1504 }
1505 None => None,
1506 };
1507
1508 let with = with
1509 .unwrap_or_default()
1510 .into_iter()
1511 .map(|(k, v)| {
1512 (
1513 k.clone(),
1514 v.claim(&mut StepCtx {
1515 backend: self.backend.clone(),
1516 }),
1517 )
1518 })
1519 .collect();
1520
1521 for var in run_after {
1522 var.claim(&mut StepCtx {
1523 backend: self.backend.clone(),
1524 });
1525 }
1526
1527 let outputvars = outputs
1528 .into_iter()
1529 .map(|(name, vars)| {
1530 (
1531 name,
1532 vars.into_iter()
1533 .map(|var| {
1534 let var = var.claim(&mut StepCtx {
1535 backend: self.backend.clone(),
1536 });
1537 GhOutput {
1538 backing_var: var.backing_var,
1539 is_secret: false,
1540 is_object: false,
1541 }
1542 })
1543 .collect(),
1544 )
1545 })
1546 .collect();
1547
1548 self.backend.borrow_mut().on_emit_gh_step(
1549 display_name.as_ref(),
1550 uses.as_ref(),
1551 with,
1552 condvar,
1553 outputvars,
1554 permissions,
1555 Vec::new(),
1556 Vec::new(),
1557 );
1558 }
1559
1560 pub fn emit_side_effect_step(
1568 &mut self,
1569 use_side_effects: impl IntoIterator<Item = ReadVar<SideEffect>>,
1570 resolve_side_effects: impl IntoIterator<Item = WriteVar<SideEffect>>,
1571 ) {
1572 let mut backend = self.backend.borrow_mut();
1573 for var in use_side_effects.into_iter() {
1574 if let ReadVarBacking::RuntimeVar {
1575 var,
1576 is_side_effect: _,
1577 } = &var.backing_var
1578 {
1579 backend.on_claimed_runtime_var(var, true);
1580 }
1581 }
1582
1583 for var in resolve_side_effects.into_iter() {
1584 backend.on_claimed_runtime_var(&var.backing_var, false);
1585 }
1586
1587 backend.on_emit_side_effect_step();
1588 }
1589
1590 pub fn backend(&self) -> FlowBackend {
1593 self.backend.borrow_mut().backend()
1594 }
1595
1596 pub fn platform(&self) -> FlowPlatform {
1599 self.backend.borrow_mut().platform()
1600 }
1601
1602 pub fn arch(&self) -> FlowArch {
1604 self.backend.borrow_mut().arch()
1605 }
1606
1607 pub fn req<R>(&mut self, req: R)
1609 where
1610 R: IntoRequest + 'static,
1611 {
1612 let mut backend = self.backend.borrow_mut();
1613 backend.on_request(
1614 NodeHandle::from_type::<R::Node>(),
1615 serde_json::to_vec(&req.into_request())
1616 .map(Into::into)
1617 .map_err(Into::into),
1618 );
1619 }
1620
1621 pub fn config<C>(&mut self, config: C)
1626 where
1627 C: IntoConfig + 'static,
1628 {
1629 let mut backend = self.backend.borrow_mut();
1630 backend.on_config(
1631 NodeHandle::from_type::<C::Node>(),
1632 serde_json::to_vec(&config)
1633 .map(Into::into)
1634 .map_err(Into::into),
1635 );
1636 }
1637
1638 #[track_caller]
1641 #[must_use]
1642 pub fn reqv<T, R>(&mut self, f: impl FnOnce(WriteVar<T>) -> R) -> ReadVar<T>
1643 where
1644 T: Serialize + DeserializeOwned,
1645 R: IntoRequest + 'static,
1646 {
1647 let (read, write) = self.new_var();
1648 self.req::<R>(f(write));
1649 read
1650 }
1651
1652 pub fn requests<N>(&mut self, reqs: impl IntoIterator<Item = N::Request>)
1654 where
1655 N: FlowNodeBase + 'static,
1656 {
1657 let mut backend = self.backend.borrow_mut();
1658 for req in reqs.into_iter() {
1659 backend.on_request(
1660 NodeHandle::from_type::<N>(),
1661 serde_json::to_vec(&req).map(Into::into).map_err(Into::into),
1662 );
1663 }
1664 }
1665
1666 #[track_caller]
1669 #[must_use]
1670 pub fn new_var<T>(&self) -> (ReadVar<T>, WriteVar<T>)
1671 where
1672 T: Serialize + DeserializeOwned,
1673 {
1674 self.new_prefixed_var("")
1675 }
1676
1677 #[track_caller]
1678 #[must_use]
1679 fn new_prefixed_var<T>(&self, prefix: &'static str) -> (ReadVar<T>, WriteVar<T>)
1680 where
1681 T: Serialize + DeserializeOwned,
1682 {
1683 let caller = std::panic::Location::caller().file().replace('\\', "/");
1690
1691 let caller = caller
1707 .split_once("flowey/")
1708 .expect("due to a known limitation with flowey, all flowey code must have an ancestor dir called 'flowey/' somewhere in its full path")
1709 .1;
1710
1711 let colon = if prefix.is_empty() { "" } else { ":" };
1712 let ordinal = self.backend.borrow_mut().on_new_var();
1713 let backing_var = format!("{prefix}{colon}{ordinal}:{caller}");
1714
1715 (
1716 ReadVar {
1717 backing_var: ReadVarBacking::RuntimeVar {
1718 var: backing_var.clone(),
1719 is_side_effect: false,
1720 },
1721 _kind: std::marker::PhantomData,
1722 },
1723 WriteVar {
1724 backing_var,
1725 is_side_effect: false,
1726 _kind: std::marker::PhantomData,
1727 },
1728 )
1729 }
1730
1731 #[track_caller]
1742 #[must_use]
1743 pub fn new_post_job_side_effect(&self) -> (ReadVar<SideEffect>, WriteVar<SideEffect>) {
1744 self.new_prefixed_var("post_job")
1745 }
1746
1747 #[track_caller]
1760 #[must_use]
1761 pub fn persistent_dir(&mut self) -> Option<ReadVar<PathBuf>> {
1762 let path: ReadVar<PathBuf> = ReadVar {
1763 backing_var: ReadVarBacking::RuntimeVar {
1764 var: self.backend.borrow_mut().persistent_dir_path_var()?,
1765 is_side_effect: false,
1766 },
1767 _kind: std::marker::PhantomData,
1768 };
1769
1770 let folder_name = self
1771 .backend
1772 .borrow_mut()
1773 .current_node()
1774 .modpath()
1775 .replace("::", "__");
1776
1777 Some(
1778 self.emit_rust_stepv("🌼 Create persistent store dir", |ctx| {
1779 let path = path.claim(ctx);
1780 |rt| {
1781 let dir = rt.read(path).join(folder_name);
1782 fs_err::create_dir_all(&dir)?;
1783 Ok(dir)
1784 }
1785 }),
1786 )
1787 }
1788
1789 pub fn supports_persistent_dir(&mut self) -> bool {
1791 self.backend
1792 .borrow_mut()
1793 .persistent_dir_path_var()
1794 .is_some()
1795 }
1796}
1797
1798pub trait RuntimeVarDb {
1801 fn get_var(&mut self, var_name: &str) -> (Vec<u8>, bool) {
1802 self.try_get_var(var_name)
1803 .unwrap_or_else(|| panic!("db is missing var {}", var_name))
1804 }
1805
1806 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)>;
1807 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>);
1808}
1809
1810impl RuntimeVarDb for Box<dyn RuntimeVarDb> {
1811 fn try_get_var(&mut self, var_name: &str) -> Option<(Vec<u8>, bool)> {
1812 (**self).try_get_var(var_name)
1813 }
1814
1815 fn set_var(&mut self, var_name: &str, is_secret: bool, value: Vec<u8>) {
1816 (**self).set_var(var_name, is_secret, value)
1817 }
1818}
1819
1820pub mod steps {
1821 pub mod ado {
1822 use crate::node::ClaimedReadVar;
1823 use crate::node::ClaimedWriteVar;
1824 use crate::node::ReadVarBacking;
1825 use serde::Deserialize;
1826 use serde::Serialize;
1827 use std::borrow::Cow;
1828
1829 #[derive(Debug, Clone, Serialize, Deserialize)]
1835 pub struct AdoResourcesRepositoryId {
1836 pub(crate) repo_id: String,
1837 }
1838
1839 impl AdoResourcesRepositoryId {
1840 pub fn new_self() -> Self {
1846 Self {
1847 repo_id: "self".into(),
1848 }
1849 }
1850
1851 pub fn dangerous_get_raw_id(&self) -> &str {
1857 &self.repo_id
1858 }
1859
1860 pub fn dangerous_new(repo_id: &str) -> Self {
1866 Self {
1867 repo_id: repo_id.into(),
1868 }
1869 }
1870 }
1871
1872 #[derive(Clone, Debug, Serialize, Deserialize)]
1877 pub struct AdoRuntimeVar {
1878 is_secret: bool,
1879 ado_var: Cow<'static, str>,
1880 }
1881
1882 impl AdoRuntimeVar {
1883 pub const BUILD_SOURCE_BRANCH: AdoRuntimeVar = AdoRuntimeVar::new("build.SourceBranch");
1889
1890 pub const BUILD_BUILD_NUMBER: AdoRuntimeVar = AdoRuntimeVar::new("build.BuildNumber");
1892
1893 pub const BUILD_BUILD_ID: AdoRuntimeVar = AdoRuntimeVar::new("Build.BuildId");
1895
1896 pub const SYSTEM_COLLECTION_URI: AdoRuntimeVar =
1898 AdoRuntimeVar::new("System.CollectionUri");
1899
1900 pub const SYSTEM_TEAM_PROJECT_ID: AdoRuntimeVar =
1902 AdoRuntimeVar::new("System.TeamProjectId");
1903
1904 pub const SYSTEM_ACCESS_TOKEN: AdoRuntimeVar =
1906 AdoRuntimeVar::new_secret("System.AccessToken");
1907
1908 pub const SYSTEM_JOB_ATTEMPT: AdoRuntimeVar =
1910 AdoRuntimeVar::new_secret("System.JobAttempt");
1911
1912 pub const PIPELINE_WORKSPACE: AdoRuntimeVar = AdoRuntimeVar::new("Pipeline.Workspace");
1914 }
1915
1916 impl AdoRuntimeVar {
1917 const fn new(s: &'static str) -> Self {
1918 Self {
1919 is_secret: false,
1920 ado_var: Cow::Borrowed(s),
1921 }
1922 }
1923
1924 const fn new_secret(s: &'static str) -> Self {
1925 Self {
1926 is_secret: true,
1927 ado_var: Cow::Borrowed(s),
1928 }
1929 }
1930
1931 pub fn is_secret(&self) -> bool {
1933 self.is_secret
1934 }
1935
1936 pub fn as_raw_var_name(&self) -> String {
1938 self.ado_var.as_ref().into()
1939 }
1940
1941 pub fn dangerous_from_global(ado_var_name: impl AsRef<str>, is_secret: bool) -> Self {
1949 Self {
1950 is_secret,
1951 ado_var: ado_var_name.as_ref().to_owned().into(),
1952 }
1953 }
1954 }
1955
1956 pub fn new_ado_step_services(
1957 fresh_ado_var: &mut dyn FnMut() -> String,
1958 ) -> AdoStepServices<'_> {
1959 AdoStepServices {
1960 fresh_ado_var,
1961 ado_to_rust: Vec::new(),
1962 rust_to_ado: Vec::new(),
1963 }
1964 }
1965
1966 pub struct CompletedAdoStepServices {
1967 pub ado_to_rust: Vec<(String, String, bool)>,
1968 pub rust_to_ado: Vec<(String, String)>,
1969 }
1970
1971 impl CompletedAdoStepServices {
1972 pub fn from_ado_step_services(access: AdoStepServices<'_>) -> Self {
1973 let AdoStepServices {
1974 fresh_ado_var: _,
1975 ado_to_rust,
1976 rust_to_ado,
1977 } = access;
1978
1979 Self {
1980 ado_to_rust,
1981 rust_to_ado,
1982 }
1983 }
1984 }
1985
1986 pub struct AdoStepServices<'a> {
1987 fresh_ado_var: &'a mut dyn FnMut() -> String,
1988 ado_to_rust: Vec<(String, String, bool)>,
1989 rust_to_ado: Vec<(String, String)>,
1990 }
1991
1992 impl AdoStepServices<'_> {
1993 pub fn resolve_repository_id(&self, repo_id: AdoResourcesRepositoryId) -> String {
1996 repo_id.repo_id
1997 }
1998
1999 pub fn set_var(&mut self, var: ClaimedWriteVar<String>, from_ado_var: AdoRuntimeVar) {
2005 self.ado_to_rust.push((
2006 from_ado_var.ado_var.into(),
2007 var.backing_var,
2008 from_ado_var.is_secret,
2009 ))
2010 }
2011
2012 pub fn get_var(&mut self, var: ClaimedReadVar<String>) -> AdoRuntimeVar {
2014 let backing_var = if let ReadVarBacking::RuntimeVar {
2015 var,
2016 is_side_effect,
2017 } = &var.backing_var
2018 {
2019 assert!(!is_side_effect);
2020 var
2021 } else {
2022 todo!("support inline ado read vars")
2023 };
2024
2025 let new_ado_var_name = (self.fresh_ado_var)();
2026
2027 self.rust_to_ado
2028 .push((backing_var.clone(), new_ado_var_name.clone()));
2029 AdoRuntimeVar::dangerous_from_global(new_ado_var_name, false)
2030 }
2031 }
2032 }
2033
2034 pub mod github {
2035 use crate::node::ClaimVar;
2036 use crate::node::NodeCtx;
2037 use crate::node::ReadVar;
2038 use crate::node::ReadVarBacking;
2039 use crate::node::SideEffect;
2040 use crate::node::StepCtx;
2041 use crate::node::VarClaimed;
2042 use crate::node::VarNotClaimed;
2043 use crate::node::WriteVar;
2044 use std::collections::BTreeMap;
2045
2046 pub struct GhStepBuilder {
2047 display_name: String,
2048 cond: Option<ReadVar<bool>>,
2049 uses: String,
2050 with: Option<BTreeMap<String, GhParam>>,
2051 outputs: BTreeMap<String, Vec<WriteVar<String>>>,
2052 run_after: Vec<ReadVar<SideEffect>>,
2053 permissions: BTreeMap<GhPermission, GhPermissionValue>,
2054 }
2055
2056 impl GhStepBuilder {
2057 pub fn new(display_name: impl AsRef<str>, uses: impl AsRef<str>) -> Self {
2072 Self {
2073 display_name: display_name.as_ref().into(),
2074 cond: None,
2075 uses: uses.as_ref().into(),
2076 with: None,
2077 outputs: BTreeMap::new(),
2078 run_after: Vec::new(),
2079 permissions: BTreeMap::new(),
2080 }
2081 }
2082
2083 pub fn condition(mut self, cond: ReadVar<bool>) -> Self {
2090 self.cond = Some(cond);
2091 self
2092 }
2093
2094 pub fn with(mut self, k: impl AsRef<str>, v: impl Into<GhParam>) -> Self {
2120 self.with.get_or_insert_with(BTreeMap::new);
2121 if let Some(with) = &mut self.with {
2122 with.insert(k.as_ref().to_string(), v.into());
2123 }
2124 self
2125 }
2126
2127 pub fn output(mut self, k: impl AsRef<str>, v: WriteVar<String>) -> Self {
2136 self.outputs
2137 .entry(k.as_ref().to_string())
2138 .or_default()
2139 .push(v);
2140 self
2141 }
2142
2143 pub fn run_after(mut self, side_effect: ReadVar<SideEffect>) -> Self {
2145 self.run_after.push(side_effect);
2146 self
2147 }
2148
2149 pub fn requires_permission(
2154 mut self,
2155 perm: GhPermission,
2156 value: GhPermissionValue,
2157 ) -> Self {
2158 self.permissions.insert(perm, value);
2159 self
2160 }
2161
2162 #[track_caller]
2164 pub fn finish(self, ctx: &mut NodeCtx<'_>) -> ReadVar<SideEffect> {
2165 let (side_effect, claim_side_effect) = ctx.new_prefixed_var("auto_se");
2166 ctx.backend
2167 .borrow_mut()
2168 .on_claimed_runtime_var(&claim_side_effect.backing_var, false);
2169
2170 ctx.emit_gh_step_inner(
2171 self.display_name,
2172 self.cond,
2173 self.uses,
2174 self.with,
2175 self.outputs,
2176 self.run_after,
2177 self.permissions,
2178 );
2179
2180 side_effect
2181 }
2182 }
2183
2184 #[derive(Clone, Debug)]
2185 pub enum GhParam<C = VarNotClaimed> {
2186 Static(String),
2187 FloweyVar(ReadVar<String, C>),
2188 }
2189
2190 impl From<String> for GhParam {
2191 fn from(param: String) -> GhParam {
2192 GhParam::Static(param)
2193 }
2194 }
2195
2196 impl From<&str> for GhParam {
2197 fn from(param: &str) -> GhParam {
2198 GhParam::Static(param.to_string())
2199 }
2200 }
2201
2202 impl From<ReadVar<String>> for GhParam {
2203 fn from(param: ReadVar<String>) -> GhParam {
2204 GhParam::FloweyVar(param)
2205 }
2206 }
2207
2208 pub type ClaimedGhParam = GhParam<VarClaimed>;
2209
2210 impl ClaimVar for GhParam {
2211 type Claimed = ClaimedGhParam;
2212
2213 fn claim(self, ctx: &mut StepCtx<'_>) -> ClaimedGhParam {
2214 match self {
2215 GhParam::Static(s) => ClaimedGhParam::Static(s),
2216 GhParam::FloweyVar(var) => match &var.backing_var {
2217 ReadVarBacking::RuntimeVar { is_side_effect, .. } => {
2218 assert!(!is_side_effect);
2219 ClaimedGhParam::FloweyVar(var.claim(ctx))
2220 }
2221 ReadVarBacking::Inline(var) => ClaimedGhParam::Static(var.clone()),
2222 },
2223 }
2224 }
2225 }
2226
2227 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
2232 pub enum GhPermissionValue {
2233 None = 0,
2234 Read = 1,
2235 Write = 2,
2236 }
2237
2238 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2244 pub enum GhPermission {
2245 Actions,
2246 ArtifactMetadata,
2247 Attestations,
2248 Checks,
2249 Contents,
2250 Deployments,
2251 Discussions,
2252 IdToken,
2253 Issues,
2254 Packages,
2255 Pages,
2256 PullRequests,
2257 RepositoryProjects,
2258 SecurityEvents,
2259 Statuses,
2260 }
2261 }
2262
2263 pub mod rust {
2264 use crate::node::ClaimedWriteVar;
2265 use crate::node::FlowArch;
2266 use crate::node::FlowBackend;
2267 use crate::node::FlowPlatform;
2268 use crate::node::ReadVarValue;
2269 use crate::node::RuntimeVarDb;
2270 use crate::shell::FloweyShell;
2271 use serde::Serialize;
2272 use serde::de::DeserializeOwned;
2273
2274 pub fn new_rust_runtime_services(
2275 runtime_var_db: &mut dyn RuntimeVarDb,
2276 backend: FlowBackend,
2277 platform: FlowPlatform,
2278 arch: FlowArch,
2279 ) -> anyhow::Result<RustRuntimeServices<'_>> {
2280 Ok(RustRuntimeServices {
2281 runtime_var_db,
2282 backend,
2283 platform,
2284 arch,
2285 has_read_secret: false,
2286 sh: FloweyShell::new()?,
2287 })
2288 }
2289
2290 pub struct RustRuntimeServices<'a> {
2291 runtime_var_db: &'a mut dyn RuntimeVarDb,
2292 backend: FlowBackend,
2293 platform: FlowPlatform,
2294 arch: FlowArch,
2295 has_read_secret: bool,
2296 pub sh: FloweyShell,
2302 }
2303
2304 impl RustRuntimeServices<'_> {
2305 pub fn backend(&self) -> FlowBackend {
2308 self.backend
2309 }
2310
2311 pub fn platform(&self) -> FlowPlatform {
2314 self.platform
2315 }
2316
2317 pub fn arch(&self) -> FlowArch {
2319 self.arch
2320 }
2321
2322 pub fn write<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2330 where
2331 T: Serialize + DeserializeOwned,
2332 {
2333 self.write_maybe_secret(var, val, self.has_read_secret)
2334 }
2335
2336 pub fn write_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2342 where
2343 T: Serialize + DeserializeOwned,
2344 {
2345 self.write_maybe_secret(var, val, true)
2346 }
2347
2348 pub fn write_not_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T)
2355 where
2356 T: Serialize + DeserializeOwned,
2357 {
2358 self.write_maybe_secret(var, val, false)
2359 }
2360
2361 fn write_maybe_secret<T>(&mut self, var: ClaimedWriteVar<T>, val: &T, is_secret: bool)
2362 where
2363 T: Serialize + DeserializeOwned,
2364 {
2365 let val = if var.is_side_effect {
2366 b"null".to_vec()
2367 } else {
2368 serde_json::to_vec(val).expect("improve this error path")
2369 };
2370 self.runtime_var_db
2371 .set_var(&var.backing_var, is_secret, val);
2372 }
2373
2374 pub fn write_all<T>(
2375 &mut self,
2376 vars: impl IntoIterator<Item = ClaimedWriteVar<T>>,
2377 val: &T,
2378 ) where
2379 T: Serialize + DeserializeOwned,
2380 {
2381 for var in vars {
2382 self.write(var, val)
2383 }
2384 }
2385
2386 pub fn read<T: ReadVarValue>(&mut self, var: T) -> T::Value {
2387 var.read_value(self)
2388 }
2389
2390 pub(crate) fn get_var(&mut self, var: &str, is_side_effect: bool) -> Vec<u8> {
2391 let (v, is_secret) = self.runtime_var_db.get_var(var);
2392 self.has_read_secret |= is_secret && !is_side_effect;
2393 v
2394 }
2395
2396 pub fn dangerous_gh_set_global_env_var(
2403 &mut self,
2404 var: String,
2405 gh_env_var: String,
2406 ) -> anyhow::Result<()> {
2407 if !matches!(self.backend, FlowBackend::Github) {
2408 return Err(anyhow::anyhow!(
2409 "dangerous_set_gh_env_var can only be used on GitHub Actions"
2410 ));
2411 }
2412
2413 let gh_env_file_path = std::env::var("GITHUB_ENV")?;
2414 let mut gh_env_file = fs_err::OpenOptions::new()
2415 .append(true)
2416 .open(gh_env_file_path)?;
2417 let gh_env_var_assignment = format!(
2418 r#"{}<<EOF
2419{}
2420EOF
2421"#,
2422 gh_env_var, var
2423 );
2424 std::io::Write::write_all(&mut gh_env_file, gh_env_var_assignment.as_bytes())?;
2425
2426 Ok(())
2427 }
2428 }
2429 }
2430}
2431
2432pub trait FlowNodeBase {
2437 type Request: Serialize + DeserializeOwned;
2438
2439 fn imports(&mut self, ctx: &mut ImportCtx<'_>);
2440 fn emit(
2441 &mut self,
2442 config_bytes: Vec<Box<[u8]>>,
2443 requests: Vec<Self::Request>,
2444 ctx: &mut NodeCtx<'_>,
2445 ) -> anyhow::Result<()>;
2446
2447 fn i_know_what_im_doing_with_this_manual_impl(&mut self);
2453}
2454
2455pub mod erased {
2456 use crate::node::FlowNodeBase;
2457 use crate::node::NodeCtx;
2458 use crate::node::user_facing::*;
2459
2460 pub struct ErasedNode<N: FlowNodeBase>(pub N);
2461
2462 impl<N: FlowNodeBase> ErasedNode<N> {
2463 pub fn from_node(node: N) -> Self {
2464 Self(node)
2465 }
2466 }
2467
2468 impl<N> FlowNodeBase for ErasedNode<N>
2469 where
2470 N: FlowNodeBase,
2471 {
2472 type Request = Box<[u8]>;
2474
2475 fn imports(&mut self, ctx: &mut ImportCtx<'_>) {
2476 self.0.imports(ctx)
2477 }
2478
2479 fn emit(
2480 &mut self,
2481 config_bytes: Vec<Box<[u8]>>,
2482 requests: Vec<Box<[u8]>>,
2483 ctx: &mut NodeCtx<'_>,
2484 ) -> anyhow::Result<()> {
2485 let mut converted_requests = Vec::new();
2486 for req in requests {
2487 converted_requests.push(serde_json::from_slice(&req)?)
2488 }
2489
2490 self.0.emit(config_bytes, converted_requests, ctx)
2491 }
2492
2493 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2494 }
2495}
2496
2497#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2499pub struct NodeHandle(std::any::TypeId);
2500
2501impl Ord for NodeHandle {
2502 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2503 self.modpath().cmp(other.modpath())
2504 }
2505}
2506
2507impl PartialOrd for NodeHandle {
2508 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2509 Some(self.cmp(other))
2510 }
2511}
2512
2513impl std::fmt::Debug for NodeHandle {
2514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2515 std::fmt::Debug::fmt(&self.try_modpath(), f)
2516 }
2517}
2518
2519impl NodeHandle {
2520 pub const fn from_type<N: FlowNodeBase + 'static>() -> NodeHandle {
2521 NodeHandle(std::any::TypeId::of::<N>())
2522 }
2523
2524 pub fn from_modpath(modpath: &str) -> NodeHandle {
2525 node_luts::erased_node_by_modpath().get(modpath).unwrap().0
2526 }
2527
2528 pub fn try_from_modpath(modpath: &str) -> Option<NodeHandle> {
2529 node_luts::erased_node_by_modpath()
2530 .get(modpath)
2531 .map(|(s, _)| *s)
2532 }
2533
2534 pub fn new_erased_node(&self) -> Box<dyn FlowNodeBase<Request = Box<[u8]>>> {
2535 let ctor = node_luts::erased_node_by_typeid().get(self).unwrap();
2536 ctor()
2537 }
2538
2539 pub fn modpath(&self) -> &'static str {
2540 node_luts::modpath_by_node_typeid().get(self).unwrap()
2541 }
2542
2543 pub fn try_modpath(&self) -> Option<&'static str> {
2544 node_luts::modpath_by_node_typeid().get(self).cloned()
2545 }
2546
2547 pub const fn dummy() -> NodeHandle {
2550 NodeHandle(std::any::TypeId::of::<()>())
2551 }
2552}
2553
2554pub fn list_all_registered_nodes() -> impl Iterator<Item = NodeHandle> {
2555 node_luts::modpath_by_node_typeid().keys().cloned()
2556}
2557
2558mod node_luts {
2573 use super::FlowNodeBase;
2574 use super::NodeHandle;
2575 use std::collections::HashMap;
2576 use std::sync::OnceLock;
2577
2578 pub(super) fn modpath_by_node_typeid() -> &'static HashMap<NodeHandle, &'static str> {
2579 static TYPEID_TO_MODPATH: OnceLock<HashMap<NodeHandle, &'static str>> = OnceLock::new();
2580
2581 TYPEID_TO_MODPATH.get_or_init(|| {
2582 let mut lookup = HashMap::new();
2583 for crate::node::private::FlowNodeMeta {
2584 module_path,
2585 ctor: _,
2586 typeid,
2587 } in crate::node::private::FLOW_NODES
2588 {
2589 let existing = lookup.insert(
2590 NodeHandle(*typeid),
2591 module_path
2592 .strip_suffix("::_only_one_call_to_flowey_node_per_module")
2593 .unwrap(),
2594 );
2595 assert!(existing.is_none())
2598 }
2599
2600 lookup
2601 })
2602 }
2603
2604 pub(super) fn erased_node_by_typeid()
2605 -> &'static HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>> {
2606 static LOOKUP: OnceLock<
2607 HashMap<NodeHandle, fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>>,
2608 > = OnceLock::new();
2609
2610 LOOKUP.get_or_init(|| {
2611 let mut lookup = HashMap::new();
2612 for crate::node::private::FlowNodeMeta {
2613 module_path: _,
2614 ctor,
2615 typeid,
2616 } in crate::node::private::FLOW_NODES
2617 {
2618 let existing = lookup.insert(NodeHandle(*typeid), *ctor);
2619 assert!(existing.is_none())
2622 }
2623
2624 lookup
2625 })
2626 }
2627
2628 pub(super) fn erased_node_by_modpath() -> &'static HashMap<
2629 &'static str,
2630 (
2631 NodeHandle,
2632 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2633 ),
2634 > {
2635 static MODPATH_LOOKUP: OnceLock<
2636 HashMap<
2637 &'static str,
2638 (
2639 NodeHandle,
2640 fn() -> Box<dyn FlowNodeBase<Request = Box<[u8]>>>,
2641 ),
2642 >,
2643 > = OnceLock::new();
2644
2645 MODPATH_LOOKUP.get_or_init(|| {
2646 let mut lookup = HashMap::new();
2647 for crate::node::private::FlowNodeMeta { module_path, ctor, typeid } in crate::node::private::FLOW_NODES {
2648 let existing = lookup.insert(module_path.strip_suffix("::_only_one_call_to_flowey_node_per_module").unwrap(), (NodeHandle(*typeid), *ctor));
2649 if existing.is_some() {
2650 panic!("conflicting node registrations at {module_path}! please ensure there is a single node per module!")
2651 }
2652 }
2653 lookup
2654 })
2655 }
2656}
2657
2658#[doc(hidden)]
2659pub mod private {
2660 pub use linkme;
2661
2662 pub struct FlowNodeMeta {
2663 pub module_path: &'static str,
2664 pub ctor: fn() -> Box<dyn super::FlowNodeBase<Request = Box<[u8]>>>,
2665 pub typeid: std::any::TypeId,
2666 }
2667
2668 #[linkme::distributed_slice]
2669 pub static FLOW_NODES: [FlowNodeMeta] = [..];
2670
2671 #[expect(unsafe_code)]
2673 #[linkme::distributed_slice(FLOW_NODES)]
2674 static DUMMY_FLOW_NODE: FlowNodeMeta = FlowNodeMeta {
2675 module_path: "<dummy>::_only_one_call_to_flowey_node_per_module",
2676 ctor: || unreachable!(),
2677 typeid: std::any::TypeId::of::<()>(),
2678 };
2679}
2680
2681#[doc(hidden)]
2682#[macro_export]
2683macro_rules! new_flow_node_base {
2684 (struct Node) => {
2685 #[non_exhaustive]
2687 pub struct Node;
2688
2689 mod _only_one_call_to_flowey_node_per_module {
2690 const _: () = {
2691 use $crate::node::private::linkme;
2692
2693 fn new_erased() -> Box<dyn $crate::node::FlowNodeBase<Request = Box<[u8]>>> {
2694 Box::new($crate::node::erased::ErasedNode(super::Node))
2695 }
2696
2697 #[linkme::distributed_slice($crate::node::private::FLOW_NODES)]
2698 #[linkme(crate = linkme)]
2699 static FLOW_NODE: $crate::node::private::FlowNodeMeta =
2700 $crate::node::private::FlowNodeMeta {
2701 module_path: module_path!(),
2702 ctor: new_erased,
2703 typeid: std::any::TypeId::of::<super::Node>(),
2704 };
2705 };
2706 }
2707 };
2708}
2709
2710pub trait FlowNode {
2797 type Request: Serialize + DeserializeOwned;
2801
2802 fn imports(ctx: &mut ImportCtx<'_>);
2814
2815 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2818}
2819
2820#[macro_export]
2821macro_rules! new_flow_node {
2822 (struct Node) => {
2823 $crate::new_flow_node_base!(struct Node);
2824
2825 impl $crate::node::FlowNodeBase for Node
2826 where
2827 Node: FlowNode,
2828 {
2829 type Request = <Node as FlowNode>::Request;
2830
2831 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2832 <Node as FlowNode>::imports(dep)
2833 }
2834
2835 fn emit(
2836 &mut self,
2837 _config_bytes: Vec<Box<[u8]>>,
2838 requests: Vec<Self::Request>,
2839 ctx: &mut $crate::node::NodeCtx<'_>,
2840 ) -> anyhow::Result<()> {
2841 <Node as FlowNode>::emit(requests, ctx)
2842 }
2843
2844 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2845 }
2846 };
2847}
2848
2849pub trait SimpleFlowNode {
2870 type Request: Serialize + DeserializeOwned;
2871
2872 fn imports(ctx: &mut ImportCtx<'_>);
2884
2885 fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()>;
2887}
2888
2889#[macro_export]
2890macro_rules! new_simple_flow_node {
2891 (struct Node) => {
2892 $crate::new_flow_node_base!(struct Node);
2893
2894 impl $crate::node::FlowNodeBase for Node
2895 where
2896 Node: $crate::node::SimpleFlowNode,
2897 {
2898 type Request = <Node as $crate::node::SimpleFlowNode>::Request;
2899
2900 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
2901 <Node as $crate::node::SimpleFlowNode>::imports(dep)
2902 }
2903
2904 fn emit(
2905 &mut self,
2906 _config_bytes: Vec<Box<[u8]>>,
2907 requests: Vec<Self::Request>,
2908 ctx: &mut $crate::node::NodeCtx<'_>,
2909 ) -> anyhow::Result<()> {
2910 for req in requests {
2911 <Node as $crate::node::SimpleFlowNode>::process_request(req, ctx)?
2912 }
2913
2914 Ok(())
2915 }
2916
2917 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
2918 }
2919 };
2920}
2921
2922pub trait FlowNodeWithConfig {
2969 type Request: Serialize + DeserializeOwned;
2971
2972 type Config: ConfigMerge;
2979
2980 fn imports(ctx: &mut ImportCtx<'_>);
2982
2983 fn emit(
2985 config: Self::Config,
2986 requests: Vec<Self::Request>,
2987 ctx: &mut NodeCtx<'_>,
2988 ) -> anyhow::Result<()>;
2989}
2990
2991#[macro_export]
2992macro_rules! new_flow_node_with_config {
2993 (struct Node) => {
2994 $crate::new_flow_node_base!(struct Node);
2995
2996 impl $crate::node::FlowNodeBase for Node
2997 where
2998 Node: $crate::node::FlowNodeWithConfig,
2999 {
3000 type Request = <Node as $crate::node::FlowNodeWithConfig>::Request;
3001
3002 fn imports(&mut self, dep: &mut $crate::node::ImportCtx<'_>) {
3003 <Node as $crate::node::FlowNodeWithConfig>::imports(dep)
3004 }
3005
3006 fn emit(
3007 &mut self,
3008 config_bytes: Vec<Box<[u8]>>,
3009 requests: Vec<Self::Request>,
3010 ctx: &mut $crate::node::NodeCtx<'_>,
3011 ) -> anyhow::Result<()> {
3012 use $crate::node::ConfigMerge;
3013
3014 type C = <Node as $crate::node::FlowNodeWithConfig>::Config;
3015
3016 let mut merged = <C as Default>::default();
3017 for bytes in config_bytes {
3018 let partial: C = serde_json::from_slice(&bytes)?;
3019 merged.merge(partial)?;
3020 }
3021
3022 <Node as $crate::node::FlowNodeWithConfig>::emit(merged, requests, ctx)
3023 }
3024
3025 fn i_know_what_im_doing_with_this_manual_impl(&mut self) {}
3026 }
3027 };
3028}
3029
3030pub trait IntoRequest {
3038 type Node: FlowNodeBase;
3039 fn into_request(self) -> <Self::Node as FlowNodeBase>::Request;
3040
3041 #[doc(hidden)]
3044 #[expect(nonstandard_style)]
3045 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self);
3046}
3047
3048pub trait IntoConfig: Serialize {
3054 type Node: FlowNodeBase;
3055
3056 #[doc(hidden)]
3059 #[expect(nonstandard_style)]
3060 fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self);
3061}
3062
3063pub trait ConfigMerge: Serialize + DeserializeOwned + Default {
3066 fn merge(&mut self, other: Self) -> anyhow::Result<()>;
3069}
3070
3071pub trait ConfigField {
3078 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()>;
3079}
3080
3081impl<T: PartialEq> ConfigField for Option<T> {
3082 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3083 if let Some(new) = other {
3084 match self {
3085 None => *self = Some(new),
3086 Some(old) if *old == new => {}
3087 Some(_) => {
3088 anyhow::bail!("config field `{field_name}` mismatch");
3089 }
3090 }
3091 }
3092 Ok(())
3093 }
3094}
3095
3096impl<K: Ord + std::fmt::Debug, V: PartialEq> ConfigField for BTreeMap<K, V> {
3097 fn merge_field(&mut self, field_name: &str, other: Self) -> anyhow::Result<()> {
3098 for (k, v) in other {
3099 use std::collections::btree_map::Entry;
3100 match self.entry(k) {
3101 Entry::Vacant(e) => {
3102 e.insert(v);
3103 }
3104 Entry::Occupied(e) if *e.get() == v => {}
3105 Entry::Occupied(e) => {
3106 anyhow::bail!("config field `{field_name}` mismatch for key {:?}", e.key(),);
3107 }
3108 }
3109 }
3110 Ok(())
3111 }
3112}
3113
3114#[doc(hidden)]
3115#[macro_export]
3116macro_rules! __flowey_request_inner {
3117 (@emit_struct [$req:ident]
3121 $(#[$a:meta])*
3122 $variant:ident($($tt:tt)*),
3123 $($rest:tt)*
3124 ) => {
3125 $(#[$a])*
3126 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3127 pub struct $variant($($tt)*);
3128
3129 impl IntoRequest for $variant {
3130 type Node = Node;
3131 fn into_request(self) -> $req {
3132 $req::$variant(self)
3133 }
3134 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3135 }
3136
3137 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3138 };
3139 (@emit_struct [$req:ident]
3140 $(#[$a:meta])*
3141 $variant:ident { $($tt:tt)* },
3142 $($rest:tt)*
3143 ) => {
3144 $(#[$a])*
3145 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3146 pub struct $variant {
3147 $($tt)*
3148 }
3149
3150 impl IntoRequest for $variant {
3151 type Node = Node;
3152 fn into_request(self) -> $req {
3153 $req::$variant(self)
3154 }
3155 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3156 }
3157
3158 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3159 };
3160 (@emit_struct [$req:ident]
3161 $(#[$a:meta])*
3162 $variant:ident,
3163 $($rest:tt)*
3164 ) => {
3165 $(#[$a])*
3166 #[derive(Serialize, Deserialize)]
3167 pub struct $variant;
3168
3169 impl IntoRequest for $variant {
3170 type Node = Node;
3171 fn into_request(self) -> $req {
3172 $req::$variant(self)
3173 }
3174 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3175 }
3176
3177 $crate::__flowey_request_inner!(@emit_struct [$req] $($rest)*);
3178 };
3179 (@emit_struct [$req:ident]
3180 ) => {};
3181
3182 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3186 $(#[$a:meta])*
3187 $variant:ident($($tt:tt)*),
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 $(#[$a:meta])*
3194 $variant:ident { $($tt:tt)* },
3195 $($rest:tt)*
3196 ) => {
3197 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3198 };
3199 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3200 $(#[$a:meta])*
3201 $variant:ident,
3202 $($rest:tt)*
3203 ) => {
3204 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*), $($prev[$($prev_a,)*])* $variant[$($a,)*]] $($rest)*);
3205 };
3206 (@emit_req_enum [$req:ident($($root_a:meta,)*), $($prev:ident[$($prev_a:meta,)*])*]
3207 ) => {
3208 #[derive(Serialize, Deserialize)]
3209 pub enum $req {$(
3210 $(#[$prev_a])*
3211 $prev(self::req::$prev),
3212 )*}
3213
3214 impl IntoRequest for $req {
3215 type Node = Node;
3216 fn into_request(self) -> $req {
3217 self
3218 }
3219 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3220 }
3221 };
3222}
3223
3224#[macro_export]
3272macro_rules! flowey_request {
3273 (
3274 $(#[$root_a:meta])*
3275 pub enum_struct $req:ident {
3276 $($tt:tt)*
3277 }
3278 ) => {
3279 $crate::__flowey_request_inner!(@emit_req_enum [$req($($root_a,)*),] $($tt)*);
3280 pub mod req {
3281 use super::*;
3282 $crate::__flowey_request_inner!(@emit_struct [$req] $($tt)*);
3283 }
3284 };
3285
3286 (
3287 $(#[$a:meta])*
3288 pub enum $req:ident {
3289 $($tt:tt)*
3290 }
3291 ) => {
3292 $(#[$a])*
3293 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3294 pub enum $req {
3295 $($tt)*
3296 }
3297
3298 impl $crate::node::IntoRequest for $req {
3299 type Node = Node;
3300 fn into_request(self) -> $req {
3301 self
3302 }
3303 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3304 }
3305 };
3306
3307 (
3308 $(#[$a:meta])*
3309 pub struct $req:ident {
3310 $($tt:tt)*
3311 }
3312 ) => {
3313 $(#[$a])*
3314 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3315 pub struct $req {
3316 $($tt)*
3317 }
3318
3319 impl $crate::node::IntoRequest for $req {
3320 type Node = Node;
3321 fn into_request(self) -> $req {
3322 self
3323 }
3324 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3325 }
3326 };
3327
3328 (
3329 $(#[$a:meta])*
3330 pub struct $req:ident($($tt:tt)*);
3331 ) => {
3332 $(#[$a])*
3333 #[derive($crate::reexports::Serialize, $crate::reexports::Deserialize)]
3334 pub struct $req($($tt)*);
3335
3336 impl $crate::node::IntoRequest for $req {
3337 type Node = Node;
3338 fn into_request(self) -> $req {
3339 self
3340 }
3341 fn do_not_manually_impl_this_trait__use_the_flowey_request_macro_instead(&mut self) {}
3342 }
3343 };
3344}
3345
3346#[macro_export]
3384macro_rules! flowey_config {
3385 (
3386 $(#[$meta:meta])*
3387 pub struct $Config:ident {
3388 $(
3389 $(#[$field_meta:meta])*
3390 pub $field:ident : $ty:ty
3391 ),* $(,)?
3392 }
3393 ) => {
3394 $(#[$meta])*
3395 #[derive(
3396 $crate::reexports::Serialize,
3397 $crate::reexports::Deserialize,
3398 Default,
3399 )]
3400 pub struct $Config {
3401 $(
3402 $(#[$field_meta])*
3403 pub $field: $ty,
3404 )*
3405 }
3406
3407 impl $crate::node::ConfigMerge for $Config {
3408 fn merge(&mut self, other: Self) -> anyhow::Result<()> {
3409 $(
3410 $crate::node::ConfigField::merge_field(
3411 &mut self.$field,
3412 stringify!($field),
3413 other.$field,
3414 )?;
3415 )*
3416 Ok(())
3417 }
3418 }
3419
3420 impl $crate::node::IntoConfig for $Config {
3421 type Node = Node;
3422
3423 fn do_not_manually_impl_this_trait__use_the_flowey_config_macro_instead(&mut self) {}
3424 }
3425 };
3426}
3427
3428#[macro_export]
3445macro_rules! shell_cmd {
3446 ($rt:expr, $cmd:literal) => {{
3447 let flowey_sh = &$rt.sh;
3448 #[expect(clippy::disallowed_macros)]
3449 flowey_sh.wrap($crate::reexports::xshell::cmd!(flowey_sh.xshell(), $cmd))
3450 }};
3451}