1use crate::{
2 action::video::{
3 VideoPause, VideoPlay, VideoSeek, VideoSetMuted, VideoSetRate, VideoSetVolume, VideoStop,
4 },
5 async_runtime::{
6 JobRef, JobRequestPayload, JobSpec, ServiceBindings, ServiceSlot, ServiceSpec,
7 ServiceStartPayload,
8 },
9 context::{Effects, ReducerContext},
10 effect::{ActionInput, Effect, EffectEnvelope},
11 ui::Node,
12 Action, ActionEnvelope, ActionId, ActionScopeId, AppState, BoxedReducer,
13};
14use anyhow::{anyhow, Result};
15use fission_ir::{NodeId, WidgetNodeId};
16use serde::Serialize;
17use std::any::TypeId;
18use std::collections::{BTreeMap, HashMap};
19use std::sync::Arc;
20
21pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
29
30pub trait IntoHandler<S: AppState, A> {
34 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
36}
37
38impl<S: AppState, A> IntoHandler<S, A> for fn(&mut S, A) {
40 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, _ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
41 (self)(state, action);
42 }
43}
44
45impl<S: AppState, A> IntoHandler<S, A>
47 for for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>)
48{
49 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
50 (self)(state, action, ctx);
51 }
52}
53
54type TypedReducer<S> = Box<
56 dyn for<'a, 'b, 'c> Fn(
57 &mut S,
58 &ActionEnvelope,
59 NodeId,
60 &mut Effects<'a, S>,
61 &'b ActionInput,
62 ) -> Result<()>
63 + Send
64 + Sync,
65>;
66
67pub type RawActionHandler<S> = Box<
72 dyn for<'a, 'b> Fn(
73 &mut S,
74 &ActionEnvelope,
75 NodeId,
76 &mut Effects<'a, S>,
77 &'b ActionInput,
78 ) -> Result<()>
79 + Send
80 + Sync,
81>;
82
83pub struct ActionRegistry<S: AppState> {
89 handlers: BTreeMap<ActionId, Vec<TypedReducer<S>>>,
90}
91
92impl<S: AppState> Default for ActionRegistry<S> {
93 fn default() -> Self {
94 Self {
95 handlers: BTreeMap::new(),
96 }
97 }
98}
99
100impl<S: AppState> ActionRegistry<S> {
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn register<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
106 &mut self,
107 handler: H,
108 ) {
109 let action_id = A::static_id();
110
111 let typed_reducer: TypedReducer<S> = Box::new(
112 move |state: &mut S,
113 envelope: &ActionEnvelope,
114 _target,
115 effects,
116 input|
117 -> Result<()> {
118 let action: A = serde_json::from_slice(&envelope.payload)
119 .map_err(|e| anyhow!("Failed to deserialize action: {}", e))?;
120
121 let mut ctx = ReducerContext { effects, input };
122
123 handler.call(state, action, &mut ctx);
124 Ok(())
125 },
126 );
127
128 self.handlers
129 .entry(action_id)
130 .or_default()
131 .push(typed_reducer);
132 }
133
134 pub fn register_raw_action<F>(&mut self, action_id: ActionId, handler: F)
135 where
136 F: for<'a, 'b> Fn(
137 &mut S,
138 &ActionEnvelope,
139 NodeId,
140 &mut Effects<'a, S>,
141 &'b ActionInput,
142 ) -> Result<()>
143 + Send
144 + Sync
145 + 'static,
146 {
147 self.handlers
148 .entry(action_id)
149 .or_default()
150 .push(Box::new(handler));
151 }
152
153 pub fn register_scoped_raw_action<F>(
154 &mut self,
155 scope_id: ActionScopeId,
156 action_id: ActionId,
157 handler: F,
158 ) where
159 F: for<'a, 'b> Fn(
160 &mut S,
161 &ActionEnvelope,
162 NodeId,
163 &mut Effects<'a, S>,
164 &'b ActionInput,
165 ) -> Result<()>
166 + Send
167 + Sync
168 + 'static,
169 {
170 let expected_scope = scope_id.as_u128();
171 self.register_raw_action(action_id, move |state, envelope, target, effects, input| {
172 if input.action_scope_id() == Some(expected_scope) {
173 handler(state, envelope, target, effects, input)
174 } else {
175 Ok(())
176 }
177 });
178 }
179
180 pub fn into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
181 let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
182 let state_type_id = TypeId::of::<S>();
183
184 for (action_id, typed_reducers) in self.handlers {
185 for typed_reducer in typed_reducers {
186 let boxed_reducer: BoxedReducer = Box::new(
187 move |app_states: &mut HashMap<TypeId, Box<dyn AppState>>,
188 action: &ActionEnvelope,
189 target: NodeId,
190 out_effects: &mut Vec<EffectEnvelope>,
191 input: &ActionInput|
192 -> Result<()> {
193 if let Some(state_box) = app_states.get_mut(&state_type_id) {
194 let concrete_state =
195 state_box.downcast_mut::<S>().ok_or_else(|| {
196 anyhow!("Failed to downcast AppState to concrete type")
197 })?;
198
199 let mut effects_builder = Effects::new_headless(0);
200
201 typed_reducer(
202 concrete_state,
203 action,
204 target,
205 &mut effects_builder,
206 input,
207 )?;
208
209 out_effects.extend(effects_builder.out);
210
211 Ok(())
212 } else {
213 anyhow::bail!("Target AppState for reducer not found in runtime.");
214 }
215 },
216 );
217
218 runtime_reducers
219 .entry(action_id)
220 .or_default()
221 .push(boxed_reducer);
222 }
223 }
224 runtime_reducers
225 }
226}
227
228#[derive(Clone, Debug, PartialEq, Eq, Hash)]
246pub enum AnimationPropertyId {
247 Opacity,
248 TranslateX,
249 TranslateY,
250 Scale,
251 Rotation,
252 Custom(Arc<str>),
253}
254
255impl AnimationPropertyId {
256 pub fn opacity() -> Self {
257 Self::Opacity
258 }
259 pub fn translate_x() -> Self {
260 Self::TranslateX
261 }
262 pub fn translate_y() -> Self {
263 Self::TranslateY
264 }
265 pub fn scale() -> Self {
266 Self::Scale
267 }
268 pub fn rotation() -> Self {
269 Self::Rotation
270 }
271 pub fn custom(name: impl Into<String>) -> Self {
272 Self::Custom(Arc::from(name.into()))
273 }
274 pub fn default_value(&self) -> f32 {
275 match self {
276 Self::Opacity => 1.0,
277 Self::Scale => 1.0,
278 Self::TranslateX | Self::TranslateY | Self::Rotation | Self::Custom(_) => 0.0,
279 }
280 }
281}
282
283#[derive(Clone, Debug)]
285pub enum AnimationStartValue {
286 Explicit(f32),
288 Current,
290}
291
292#[derive(Clone, Debug, PartialEq)]
298pub enum EasingFunction {
299 Linear,
300 EaseIn,
301 EaseOut,
302 EaseInOut,
303 CubicBezier(f32, f32, f32, f32),
304}
305
306impl Default for EasingFunction {
307 fn default() -> Self {
308 Self::EaseInOut
309 }
310}
311
312impl EasingFunction {
313 pub fn apply(&self, t: f32) -> f32 {
315 match self {
316 Self::Linear => t,
317 Self::EaseIn => t * t,
318 Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
319 Self::EaseInOut => {
320 if t < 0.5 {
321 2.0 * t * t
322 } else {
323 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
324 }
325 }
326 Self::CubicBezier(_x1, y1, _x2, y2) => {
327 let t2 = t * t;
329 let t3 = t2 * t;
330 3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
331 }
332 }
333 }
334}
335#[derive(Clone, Debug)]
336pub struct AnimationRequest {
337 pub property: AnimationPropertyId,
339 pub from: AnimationStartValue,
341 pub to: f32,
343 pub duration_ms: u64,
345 pub repeat: bool,
347 pub delay_ms: u64,
349 pub frame_interval_ms: Option<u64>,
354 pub easing: EasingFunction,
355}
356
357#[derive(Clone, Debug)]
360pub struct VideoRegistration {
361 pub node_id: WidgetNodeId,
363 pub source: String,
365 pub autoplay: bool,
367 pub loop_playback: bool,
369}
370
371#[derive(Clone, Debug)]
373pub struct WebRegistration {
374 pub node_id: WidgetNodeId,
376 pub url: String,
378 pub user_agent: Option<String>,
380}
381
382pub struct BuildCtx<S: AppState> {
406 pub registry: ActionRegistry<S>,
408 pub resources: ResourceRegistry,
410 pub animation_requests: Vec<(WidgetNodeId, AnimationRequest)>,
412 pub video_nodes: Vec<VideoRegistration>,
414 pub web_nodes: Vec<WebRegistration>,
416 pub portals: Vec<PortalEntry>,
418 portal_seq: u64,
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
426pub enum PortalLayer {
427 Default = 0,
429 Modal = 100,
431 Flyout = 200,
433 Toast = 300,
435}
436
437#[derive(Clone, Debug)]
441pub struct PortalEntry {
442 pub layer: PortalLayer,
444 pub seq: u64,
446 pub id: Option<WidgetNodeId>,
448 pub node: Node,
450}
451
452impl<S: AppState> BuildCtx<S> {
453 pub fn new() -> Self {
454 Self {
455 registry: ActionRegistry::new(),
456 resources: ResourceRegistry::new(),
457 animation_requests: Vec::new(),
458 video_nodes: Vec::new(),
459 web_nodes: Vec::new(),
460 portals: Vec::new(),
461 portal_seq: 0,
462 }
463 }
464
465 pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope
466 where
467 H: IntoHandler<S, A> + Send + Sync + 'static,
468 {
469 self.registry.register(handler);
470
471 ActionEnvelope {
472 id: A::static_id(),
473 payload: action.encode(),
474 }
475 }
476
477 pub fn register<A: Action, H>(&mut self, handler: H)
478 where
479 H: IntoHandler<S, A> + Send + Sync + 'static,
480 {
481 self.registry.register::<A, H>(handler);
482 }
483
484 pub fn register_scoped_raw_action<F>(
485 &mut self,
486 scope_id: ActionScopeId,
487 action_id: ActionId,
488 handler: F,
489 ) where
490 F: for<'a, 'b> Fn(
491 &mut S,
492 &ActionEnvelope,
493 NodeId,
494 &mut Effects<'a, S>,
495 &'b ActionInput,
496 ) -> Result<()>
497 + Send
498 + Sync
499 + 'static,
500 {
501 self.registry
502 .register_scoped_raw_action(scope_id, action_id, handler);
503 }
504
505 pub fn request_animation_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
506 self.animation_requests.push((target, request));
507 }
508
509 pub fn register_video(&mut self, registration: VideoRegistration) {
510 self.video_nodes.push(registration);
511 }
512
513 pub fn register_web_view(&mut self, registration: WebRegistration) {
514 self.web_nodes.push(registration);
515 }
516
517 pub fn take_animation_requests(&mut self) -> Vec<(WidgetNodeId, AnimationRequest)> {
518 std::mem::take(&mut self.animation_requests)
519 }
520
521 pub fn take_video_registrations(&mut self) -> Vec<VideoRegistration> {
522 std::mem::take(&mut self.video_nodes)
523 }
524
525 pub fn take_web_registrations(&mut self) -> Vec<WebRegistration> {
526 std::mem::take(&mut self.web_nodes)
527 }
528
529 pub fn take_resources(&mut self) -> Vec<RuntimeResourceDeclaration> {
530 self.resources.take()
531 }
532
533 pub fn register_portal(&mut self, node: Node) {
534 self.register_portal_with_layer(PortalLayer::Default, None, node);
535 }
536
537 pub fn register_portal_with_id(&mut self, id: WidgetNodeId, node: Node) {
538 self.register_portal_with_layer(PortalLayer::Default, Some(id), node);
539 }
540
541 pub fn register_portal_with_layer(
542 &mut self,
543 layer: PortalLayer,
544 id: Option<WidgetNodeId>,
545 node: Node,
546 ) {
547 let seq = self.portal_seq;
548 self.portal_seq = self.portal_seq.wrapping_add(1);
549 self.portals.push(PortalEntry {
550 layer,
551 seq,
552 id,
553 node,
554 });
555 }
556
557 pub fn take_portals(&mut self) -> Vec<(Option<WidgetNodeId>, Node)> {
558 let mut entries = std::mem::take(&mut self.portals);
559 entries.sort_by(|a, b| (a.layer, a.seq).cmp(&(b.layer, b.seq)));
560 entries.into_iter().map(|e| (e.id, e.node)).collect()
561 }
562
563 pub fn anim_for(&mut self, target: WidgetNodeId) -> AnimCtx<'_, S> {
564 AnimCtx { target, ctx: self }
565 }
566
567 pub fn video_controls(&self, target: WidgetNodeId) -> VideoControlCtx {
568 VideoControlCtx { target }
569 }
570}
571
572pub struct AnimCtx<'a, S: AppState> {
573 target: WidgetNodeId,
574 ctx: &'a mut BuildCtx<S>,
575}
576
577impl<'a, S: AppState> AnimCtx<'a, S> {
578 pub fn request(&mut self, request: AnimationRequest) {
579 self.ctx.request_animation_for(self.target, request);
580 }
581
582 pub fn request_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
583 self.ctx.request_animation_for(target, request);
584 }
585}
586
587#[derive(Clone, Copy)]
588pub struct VideoControlCtx {
589 target: WidgetNodeId,
590}
591
592impl VideoControlCtx {
593 pub fn play(&self) -> ActionEnvelope {
594 let action = VideoPlay {
595 target: self.target,
596 };
597 ActionEnvelope {
598 id: VideoPlay::static_id(),
599 payload: action.encode(),
600 }
601 }
602
603 pub fn pause(&self) -> ActionEnvelope {
604 let action = VideoPause {
605 target: self.target,
606 };
607 ActionEnvelope {
608 id: VideoPause::static_id(),
609 payload: action.encode(),
610 }
611 }
612
613 pub fn stop(&self) -> ActionEnvelope {
614 let action = VideoStop {
615 target: self.target,
616 };
617 ActionEnvelope {
618 id: VideoStop::static_id(),
619 payload: action.encode(),
620 }
621 }
622
623 pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
624 let action = VideoSeek {
625 target: self.target,
626 position_ms,
627 };
628 ActionEnvelope {
629 id: VideoSeek::static_id(),
630 payload: action.encode(),
631 }
632 }
633
634 pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
635 let action = VideoSetRate {
636 target: self.target,
637 rate,
638 };
639 ActionEnvelope {
640 id: VideoSetRate::static_id(),
641 payload: action.encode(),
642 }
643 }
644
645 pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
646 let action = VideoSetVolume {
647 target: self.target,
648 volume,
649 };
650 ActionEnvelope {
651 id: VideoSetVolume::static_id(),
652 payload: action.encode(),
653 }
654 }
655
656 pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
657 let action = VideoSetMuted {
658 target: self.target,
659 muted,
660 };
661 ActionEnvelope {
662 id: VideoSetMuted::static_id(),
663 payload: action.encode(),
664 }
665 }
666}
667
668#[derive(Clone, Debug, PartialEq, Eq, Hash)]
669pub struct ResourceKey(String);
670
671impl ResourceKey {
672 pub fn new(name: impl Into<String>) -> Self {
673 Self(name.into())
674 }
675
676 pub fn widget(name: impl AsRef<str>, id: WidgetNodeId) -> Self {
677 Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
678 }
679
680 pub fn as_str(&self) -> &str {
681 &self.0
682 }
683}
684
685#[derive(Clone, Copy, Debug, PartialEq, Eq)]
686pub enum ResourcePolicy {
687 PreserveOnChange,
688 RestartOnChange,
689}
690
691impl Default for ResourcePolicy {
692 fn default() -> Self {
693 Self::RestartOnChange
694 }
695}
696
697#[derive(Clone, Debug, PartialEq)]
698pub struct RuntimeResourceDeclaration {
699 pub key: String,
700 pub deps: Option<Vec<u8>>,
701 pub policy: ResourcePolicy,
702 pub kind: RuntimeResourceKind,
703}
704
705#[derive(Clone, Debug, PartialEq)]
706pub enum RuntimeResourceKind {
707 Job(JobResource),
708 Service(ServiceResource),
709 Timer(TimerResource),
710}
711
712#[derive(Clone, Debug, PartialEq)]
713pub struct JobResource {
714 pub key: ResourceKey,
715 pub effect: EffectEnvelope,
716 pub deps: Option<Vec<u8>>,
717 pub policy: ResourcePolicy,
718}
719
720impl JobResource {
721 pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
722 let payload =
723 serde_json::to_vec(&request).expect("job resource request serialization must succeed");
724 Self {
725 key,
726 effect: EffectEnvelope {
727 req_id: 0,
728 effect: Effect::Job(JobRequestPayload {
729 job_name: job.name.to_string(),
730 payload,
731 }),
732 on_ok: None,
733 on_err: None,
734 service_bindings: None,
735 resource: None,
736 },
737 deps: None,
738 policy: ResourcePolicy::RestartOnChange,
739 }
740 }
741
742 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
743 self.deps =
744 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
745 self
746 }
747
748 pub fn preserve_on_change(mut self) -> Self {
749 self.policy = ResourcePolicy::PreserveOnChange;
750 self
751 }
752
753 pub fn restart_on_change(mut self) -> Self {
754 self.policy = ResourcePolicy::RestartOnChange;
755 self
756 }
757
758 pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
759 self.effect.on_ok = Some(action);
760 self
761 }
762
763 pub fn on_err(mut self, action: ActionEnvelope) -> Self {
764 self.effect.on_err = Some(action);
765 self
766 }
767}
768
769#[derive(Clone, Debug, PartialEq)]
770pub struct ServiceResource {
771 pub key: ResourceKey,
772 pub effect: EffectEnvelope,
773 pub deps: Option<Vec<u8>>,
774 pub policy: ResourcePolicy,
775}
776
777impl ServiceResource {
778 pub fn new<Svc: ServiceSpec>(
779 key: ResourceKey,
780 slot: ServiceSlot<Svc>,
781 config: Svc::Config,
782 ) -> Self {
783 let config = serde_json::to_vec(&config)
784 .expect("service resource config serialization must succeed");
785 Self {
786 key,
787 effect: EffectEnvelope {
788 req_id: 0,
789 effect: Effect::StartService(ServiceStartPayload {
790 service_name: slot.ty.name.to_string(),
791 slot_key: slot.slot_key().to_string(),
792 config,
793 }),
794 on_ok: None,
795 on_err: None,
796 service_bindings: Some(ServiceBindings::default()),
797 resource: None,
798 },
799 deps: None,
800 policy: ResourcePolicy::RestartOnChange,
801 }
802 }
803
804 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
805 self.deps =
806 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
807 self
808 }
809
810 pub fn preserve_on_change(mut self) -> Self {
811 self.policy = ResourcePolicy::PreserveOnChange;
812 self
813 }
814
815 pub fn restart_on_change(mut self) -> Self {
816 self.policy = ResourcePolicy::RestartOnChange;
817 self
818 }
819
820 pub fn on_started(mut self, action: ActionEnvelope) -> Self {
821 if let Some(bindings) = self.effect.service_bindings.as_mut() {
822 bindings.on_started = Some(action);
823 }
824 self
825 }
826
827 pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
828 if let Some(bindings) = self.effect.service_bindings.as_mut() {
829 bindings.on_start_failed = Some(action);
830 }
831 self
832 }
833
834 pub fn on_event(mut self, action: ActionEnvelope) -> Self {
835 if let Some(bindings) = self.effect.service_bindings.as_mut() {
836 bindings.on_event = Some(action);
837 }
838 self
839 }
840
841 pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
842 if let Some(bindings) = self.effect.service_bindings.as_mut() {
843 bindings.on_stopped = Some(action);
844 }
845 self
846 }
847
848 pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
849 if let Some(bindings) = self.effect.service_bindings.as_mut() {
850 bindings.on_command_ok = Some(action);
851 }
852 self
853 }
854
855 pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
856 if let Some(bindings) = self.effect.service_bindings.as_mut() {
857 bindings.on_command_err = Some(action);
858 }
859 self
860 }
861}
862
863#[derive(Clone, Debug, PartialEq, Eq)]
864pub struct TimerResource {
865 pub key: ResourceKey,
866 pub interval_ms: u64,
867 pub payload: Vec<u8>,
868 pub on_tick: Option<ActionEnvelope>,
869 pub deps: Option<Vec<u8>>,
870 pub immediate: bool,
871 pub policy: ResourcePolicy,
872}
873
874impl TimerResource {
875 pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
876 Self {
877 key,
878 interval_ms: interval.as_millis() as u64,
879 payload: serde_json::to_vec(&payload)
880 .expect("timer resource payload serialization must succeed"),
881 on_tick: None,
882 deps: None,
883 immediate: false,
884 policy: ResourcePolicy::RestartOnChange,
885 }
886 }
887
888 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
889 self.deps =
890 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
891 self
892 }
893
894 pub fn preserve_on_change(mut self) -> Self {
895 self.policy = ResourcePolicy::PreserveOnChange;
896 self
897 }
898
899 pub fn restart_on_change(mut self) -> Self {
900 self.policy = ResourcePolicy::RestartOnChange;
901 self
902 }
903
904 pub fn immediate(mut self) -> Self {
905 self.immediate = true;
906 self
907 }
908
909 pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
910 self.on_tick = Some(action);
911 self
912 }
913}
914
915#[derive(Default)]
916pub struct ResourceRegistry {
917 declarations: Vec<RuntimeResourceDeclaration>,
918 seen_keys: HashMap<String, usize>,
919}
920
921impl ResourceRegistry {
922 pub fn new() -> Self {
923 Self::default()
924 }
925
926 pub fn job(&mut self, resource: JobResource) {
927 self.push(RuntimeResourceDeclaration {
928 key: resource.key.as_str().to_string(),
929 deps: resource.deps.clone(),
930 policy: resource.policy,
931 kind: RuntimeResourceKind::Job(resource),
932 });
933 }
934
935 pub fn service(&mut self, resource: ServiceResource) {
936 self.push(RuntimeResourceDeclaration {
937 key: resource.key.as_str().to_string(),
938 deps: resource.deps.clone(),
939 policy: resource.policy,
940 kind: RuntimeResourceKind::Service(resource),
941 });
942 }
943
944 pub fn timer(&mut self, resource: TimerResource) {
945 self.push(RuntimeResourceDeclaration {
946 key: resource.key.as_str().to_string(),
947 deps: resource.deps.clone(),
948 policy: resource.policy,
949 kind: RuntimeResourceKind::Timer(resource),
950 });
951 }
952
953 pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
954 self.seen_keys.clear();
955 std::mem::take(&mut self.declarations)
956 }
957
958 fn push(&mut self, declaration: RuntimeResourceDeclaration) {
959 if let Some(index) = self.seen_keys.get(&declaration.key) {
960 panic!(
961 "duplicate runtime resource declaration for key '{}' at index {}",
962 declaration.key, index
963 );
964 }
965 let index = self.declarations.len();
966 self.seen_keys.insert(declaration.key.clone(), index);
967 self.declarations.push(declaration);
968 }
969}