1use core::ffi::c_void;
10use nros_core::lifecycle::{
11 LifecycleState, LifecycleTransition, TransitionResult, apply_transition, can_transition,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LifecycleError {
17 InvalidTransition {
19 from: LifecycleState,
21 transition: LifecycleTransition,
23 },
24 CallbackFailed {
26 transition: LifecycleTransition,
28 result: TransitionResult,
30 },
31 NodeFinalized,
33}
34
35pub type LifecycleCallbackFn = fn() -> TransitionResult;
41
42pub struct LifecyclePollingNode {
60 state: LifecycleState,
61 on_configure: Option<LifecycleCallbackFn>,
62 on_activate: Option<LifecycleCallbackFn>,
63 on_deactivate: Option<LifecycleCallbackFn>,
64 on_cleanup: Option<LifecycleCallbackFn>,
65 on_shutdown: Option<LifecycleCallbackFn>,
66 on_error: Option<LifecycleCallbackFn>,
67}
68
69impl LifecyclePollingNode {
70 pub const fn new() -> Self {
74 Self {
75 state: LifecycleState::Unconfigured,
76 on_configure: None,
77 on_activate: None,
78 on_deactivate: None,
79 on_cleanup: None,
80 on_shutdown: None,
81 on_error: None,
82 }
83 }
84
85 pub const fn state(&self) -> LifecycleState {
87 self.state
88 }
89
90 pub fn trigger_transition(
92 &mut self,
93 transition: LifecycleTransition,
94 ) -> Result<LifecycleState, LifecycleError> {
95 if self.state.is_terminal() {
96 return Err(LifecycleError::NodeFinalized);
97 }
98
99 if !can_transition(self.state, transition) {
100 return Err(LifecycleError::InvalidTransition {
101 from: self.state,
102 transition,
103 });
104 }
105
106 let result = self.invoke_callback(transition);
107 self.state = apply_transition(self.state, transition, result);
108
109 if result == TransitionResult::Success {
110 Ok(self.state)
111 } else {
112 Err(LifecycleError::CallbackFailed { transition, result })
113 }
114 }
115
116 pub fn configure(&mut self) -> Result<LifecycleState, LifecycleError> {
118 self.trigger_transition(LifecycleTransition::Configure)
119 }
120
121 pub fn activate(&mut self) -> Result<LifecycleState, LifecycleError> {
123 self.trigger_transition(LifecycleTransition::Activate)
124 }
125
126 pub fn deactivate(&mut self) -> Result<LifecycleState, LifecycleError> {
128 self.trigger_transition(LifecycleTransition::Deactivate)
129 }
130
131 pub fn cleanup(&mut self) -> Result<LifecycleState, LifecycleError> {
133 self.trigger_transition(LifecycleTransition::Cleanup)
134 }
135
136 pub fn shutdown(&mut self) -> Result<LifecycleState, LifecycleError> {
138 let transition = match self.state {
139 LifecycleState::Unconfigured => LifecycleTransition::ShutdownUnconfigured,
140 LifecycleState::Inactive => LifecycleTransition::ShutdownInactive,
141 LifecycleState::Active => LifecycleTransition::ShutdownActive,
142 LifecycleState::Finalized => return Err(LifecycleError::NodeFinalized),
143 LifecycleState::ErrorProcessing => {
144 return Err(LifecycleError::InvalidTransition {
145 from: self.state,
146 transition: LifecycleTransition::ShutdownUnconfigured,
147 });
148 }
149 };
150 self.trigger_transition(transition)
151 }
152
153 pub fn bring_up(&mut self) -> Result<LifecycleState, LifecycleError> {
155 self.configure()?;
156 self.activate()
157 }
158
159 pub fn register_on_configure(&mut self, cb: LifecycleCallbackFn) {
161 self.on_configure = Some(cb);
162 }
163
164 pub fn register_on_activate(&mut self, cb: LifecycleCallbackFn) {
166 self.on_activate = Some(cb);
167 }
168
169 pub fn register_on_deactivate(&mut self, cb: LifecycleCallbackFn) {
171 self.on_deactivate = Some(cb);
172 }
173
174 pub fn register_on_cleanup(&mut self, cb: LifecycleCallbackFn) {
176 self.on_cleanup = Some(cb);
177 }
178
179 pub fn register_on_shutdown(&mut self, cb: LifecycleCallbackFn) {
181 self.on_shutdown = Some(cb);
182 }
183
184 pub fn register_on_error(&mut self, cb: LifecycleCallbackFn) {
186 self.on_error = Some(cb);
187 }
188
189 fn invoke_callback(&mut self, transition: LifecycleTransition) -> TransitionResult {
190 let cb = match transition {
191 LifecycleTransition::Configure => self.on_configure,
192 LifecycleTransition::Activate => self.on_activate,
193 LifecycleTransition::Deactivate => self.on_deactivate,
194 LifecycleTransition::Cleanup => self.on_cleanup,
195 LifecycleTransition::ShutdownUnconfigured
196 | LifecycleTransition::ShutdownInactive
197 | LifecycleTransition::ShutdownActive => self.on_shutdown,
198 LifecycleTransition::ErrorRecovery => self.on_error,
199 };
200
201 match cb {
202 Some(f) => f(),
203 None => TransitionResult::Success,
204 }
205 }
206}
207
208impl Default for LifecyclePollingNode {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214pub type LifecycleCallbackFnCtx = unsafe extern "C" fn(ctx: *mut c_void) -> u8;
225
226pub struct LifecyclePollingNodeCtx {
234 state: LifecycleState,
235 on_configure: Option<LifecycleCallbackFnCtx>,
236 on_activate: Option<LifecycleCallbackFnCtx>,
237 on_deactivate: Option<LifecycleCallbackFnCtx>,
238 on_cleanup: Option<LifecycleCallbackFnCtx>,
239 on_shutdown: Option<LifecycleCallbackFnCtx>,
240 on_error: Option<LifecycleCallbackFnCtx>,
241 context: *mut c_void,
242}
243
244impl LifecyclePollingNodeCtx {
248 pub const fn new() -> Self {
250 Self {
251 state: LifecycleState::Unconfigured,
252 on_configure: None,
253 on_activate: None,
254 on_deactivate: None,
255 on_cleanup: None,
256 on_shutdown: None,
257 on_error: None,
258 context: core::ptr::null_mut(),
259 }
260 }
261
262 pub const fn state(&self) -> LifecycleState {
264 self.state
265 }
266
267 pub fn set_context(&mut self, ctx: *mut c_void) {
269 self.context = ctx;
270 }
271
272 pub fn context(&self) -> *mut c_void {
274 self.context
275 }
276
277 pub fn register(&mut self, slot: LifecycleCallbackSlot, cb: Option<LifecycleCallbackFnCtx>) {
279 match slot {
280 LifecycleCallbackSlot::Configure => self.on_configure = cb,
281 LifecycleCallbackSlot::Activate => self.on_activate = cb,
282 LifecycleCallbackSlot::Deactivate => self.on_deactivate = cb,
283 LifecycleCallbackSlot::Cleanup => self.on_cleanup = cb,
284 LifecycleCallbackSlot::Shutdown => self.on_shutdown = cb,
285 LifecycleCallbackSlot::Error => self.on_error = cb,
286 }
287 }
288
289 pub fn clear_callbacks(&mut self) {
291 self.on_configure = None;
292 self.on_activate = None;
293 self.on_deactivate = None;
294 self.on_cleanup = None;
295 self.on_shutdown = None;
296 self.on_error = None;
297 self.context = core::ptr::null_mut();
298 }
299
300 pub fn finalize(&mut self) {
302 self.state = LifecycleState::Finalized;
303 }
304
305 pub unsafe fn trigger_transition(
312 &mut self,
313 transition: LifecycleTransition,
314 ) -> Result<LifecycleState, LifecycleError> {
315 if self.state.is_terminal() {
316 return Err(LifecycleError::NodeFinalized);
317 }
318
319 if !can_transition(self.state, transition) {
320 return Err(LifecycleError::InvalidTransition {
321 from: self.state,
322 transition,
323 });
324 }
325
326 let cb = match transition {
327 LifecycleTransition::Configure => self.on_configure,
328 LifecycleTransition::Activate => self.on_activate,
329 LifecycleTransition::Deactivate => self.on_deactivate,
330 LifecycleTransition::Cleanup => self.on_cleanup,
331 LifecycleTransition::ShutdownUnconfigured
332 | LifecycleTransition::ShutdownInactive
333 | LifecycleTransition::ShutdownActive => self.on_shutdown,
334 LifecycleTransition::ErrorRecovery => self.on_error,
335 };
336
337 let result = match cb {
338 Some(f) => {
339 let raw = unsafe { f(self.context) };
340 TransitionResult::from_u8(raw).unwrap_or(TransitionResult::Error)
341 }
342 None => TransitionResult::Success,
343 };
344
345 self.state = apply_transition(self.state, transition, result);
346
347 if result == TransitionResult::Success {
348 Ok(self.state)
349 } else {
350 Err(LifecycleError::CallbackFailed { transition, result })
351 }
352 }
353}
354
355impl Default for LifecyclePollingNodeCtx {
356 fn default() -> Self {
357 Self::new()
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub enum LifecycleCallbackSlot {
364 Configure,
366 Activate,
368 Deactivate,
370 Cleanup,
372 Shutdown,
374 Error,
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
387 fn test_polling_node_initial_state() {
388 let node = LifecyclePollingNode::new();
389 assert_eq!(node.state(), LifecycleState::Unconfigured);
390 }
391
392 #[test]
393 fn test_polling_node_default() {
394 let node = LifecyclePollingNode::default();
395 assert_eq!(node.state(), LifecycleState::Unconfigured);
396 }
397
398 #[test]
399 fn test_polling_node_happy_path() {
400 let mut node = LifecyclePollingNode::new();
401
402 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
403 assert_eq!(node.activate().unwrap(), LifecycleState::Active);
404 assert_eq!(node.deactivate().unwrap(), LifecycleState::Inactive);
405 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
406 }
407
408 #[test]
409 fn test_polling_node_cleanup_cycle() {
410 let mut node = LifecyclePollingNode::new();
411
412 node.configure().unwrap();
413 assert_eq!(node.cleanup().unwrap(), LifecycleState::Unconfigured);
414
415 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
417 }
418
419 #[test]
420 fn test_polling_node_invalid_transition() {
421 let mut node = LifecyclePollingNode::new();
422
423 let err = node.activate().unwrap_err();
424 assert_eq!(
425 err,
426 LifecycleError::InvalidTransition {
427 from: LifecycleState::Unconfigured,
428 transition: LifecycleTransition::Activate,
429 }
430 );
431 }
432
433 #[test]
434 fn test_polling_node_finalized_rejection() {
435 let mut node = LifecyclePollingNode::new();
436 node.shutdown().unwrap();
437
438 assert_eq!(node.configure().unwrap_err(), LifecycleError::NodeFinalized);
439 assert_eq!(node.shutdown().unwrap_err(), LifecycleError::NodeFinalized);
440 }
441
442 fn on_configure_success() -> TransitionResult {
443 TransitionResult::Success
444 }
445
446 fn on_configure_failure() -> TransitionResult {
447 TransitionResult::Failure
448 }
449
450 fn on_configure_error() -> TransitionResult {
451 TransitionResult::Error
452 }
453
454 #[test]
455 fn test_polling_node_callback_success() {
456 let mut node = LifecyclePollingNode::new();
457 node.register_on_configure(on_configure_success);
458
459 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
460 }
461
462 #[test]
463 fn test_polling_node_callback_failure_rollback() {
464 let mut node = LifecyclePollingNode::new();
465 node.register_on_configure(on_configure_failure);
466
467 let err = node.configure().unwrap_err();
468 assert_eq!(
469 err,
470 LifecycleError::CallbackFailed {
471 transition: LifecycleTransition::Configure,
472 result: TransitionResult::Failure,
473 }
474 );
475 assert_eq!(node.state(), LifecycleState::Unconfigured);
477 }
478
479 #[test]
480 fn test_polling_node_callback_error() {
481 let mut node = LifecyclePollingNode::new();
482 node.register_on_configure(on_configure_error);
483
484 let err = node.configure().unwrap_err();
485 assert_eq!(
486 err,
487 LifecycleError::CallbackFailed {
488 transition: LifecycleTransition::Configure,
489 result: TransitionResult::Error,
490 }
491 );
492 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
494 }
495
496 #[test]
497 fn test_polling_node_error_recovery() {
498 let mut node = LifecyclePollingNode::new();
499 node.register_on_configure(on_configure_error);
500
501 let _ = node.configure();
502 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
503
504 assert!(node.shutdown().is_err());
506
507 node.register_on_error(on_configure_success);
509 let result = node.trigger_transition(LifecycleTransition::ErrorRecovery);
510 assert_eq!(result.unwrap(), LifecycleState::Unconfigured);
511 }
512
513 #[test]
514 fn test_polling_node_bring_up() {
515 let mut node = LifecyclePollingNode::new();
516 assert_eq!(node.bring_up().unwrap(), LifecycleState::Active);
517 }
518
519 #[test]
520 fn test_polling_node_bring_up_stops_on_configure_failure() {
521 let mut node = LifecyclePollingNode::new();
522 node.register_on_configure(on_configure_failure);
523
524 let err = node.bring_up().unwrap_err();
525 assert_eq!(
526 err,
527 LifecycleError::CallbackFailed {
528 transition: LifecycleTransition::Configure,
529 result: TransitionResult::Failure,
530 }
531 );
532 assert_eq!(node.state(), LifecycleState::Unconfigured);
534 }
535
536 #[test]
537 fn test_polling_node_shutdown_from_each_state() {
538 let mut node = LifecyclePollingNode::new();
540 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
541
542 let mut node = LifecyclePollingNode::new();
544 node.configure().unwrap();
545 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
546
547 let mut node = LifecyclePollingNode::new();
549 node.bring_up().unwrap();
550 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
551 }
552
553 #[test]
554 fn test_polling_node_no_callback_defaults_success() {
555 let mut node = LifecyclePollingNode::new();
557 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
558 assert_eq!(node.activate().unwrap(), LifecycleState::Active);
559 assert_eq!(node.deactivate().unwrap(), LifecycleState::Inactive);
560 assert_eq!(node.cleanup().unwrap(), LifecycleState::Unconfigured);
561 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
562 }
563
564 fn on_shutdown_success() -> TransitionResult {
565 TransitionResult::Success
566 }
567
568 #[test]
569 fn test_polling_node_shutdown_callback_invoked() {
570 let mut node = LifecyclePollingNode::new();
571 node.register_on_shutdown(on_shutdown_success);
572
573 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
575 }
576
577 unsafe extern "C" fn ctx_cb_success(_: *mut c_void) -> u8 {
582 TransitionResult::Success as u8
583 }
584 unsafe extern "C" fn ctx_cb_failure(_: *mut c_void) -> u8 {
585 TransitionResult::Failure as u8
586 }
587 unsafe extern "C" fn ctx_cb_error(_: *mut c_void) -> u8 {
588 TransitionResult::Error as u8
589 }
590
591 #[test]
592 fn test_ctx_node_happy_path() {
593 unsafe {
594 let mut node = LifecyclePollingNodeCtx::new();
595 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_success));
596 node.register(LifecycleCallbackSlot::Activate, Some(ctx_cb_success));
597 node.register(LifecycleCallbackSlot::Deactivate, Some(ctx_cb_success));
598 node.register(LifecycleCallbackSlot::Shutdown, Some(ctx_cb_success));
599
600 assert_eq!(
601 node.trigger_transition(LifecycleTransition::Configure)
602 .unwrap(),
603 LifecycleState::Inactive
604 );
605 assert_eq!(
606 node.trigger_transition(LifecycleTransition::Activate)
607 .unwrap(),
608 LifecycleState::Active
609 );
610 assert_eq!(
611 node.trigger_transition(LifecycleTransition::Deactivate)
612 .unwrap(),
613 LifecycleState::Inactive
614 );
615 assert_eq!(
616 node.trigger_transition(LifecycleTransition::ShutdownInactive)
617 .unwrap(),
618 LifecycleState::Finalized
619 );
620 }
621 }
622
623 #[test]
624 fn test_ctx_node_invalid_transition() {
625 unsafe {
626 let mut node = LifecyclePollingNodeCtx::new();
627 let err = node
628 .trigger_transition(LifecycleTransition::Activate)
629 .unwrap_err();
630 assert_eq!(
631 err,
632 LifecycleError::InvalidTransition {
633 from: LifecycleState::Unconfigured,
634 transition: LifecycleTransition::Activate,
635 }
636 );
637 assert_eq!(node.state(), LifecycleState::Unconfigured);
638 }
639 }
640
641 #[test]
642 fn test_ctx_node_callback_failure_rolls_back() {
643 unsafe {
644 let mut node = LifecyclePollingNodeCtx::new();
645 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_failure));
646 assert!(
647 node.trigger_transition(LifecycleTransition::Configure)
648 .is_err()
649 );
650 assert_eq!(node.state(), LifecycleState::Unconfigured);
651 }
652 }
653
654 #[test]
655 fn test_ctx_node_callback_error_enters_error_processing() {
656 unsafe {
657 let mut node = LifecyclePollingNodeCtx::new();
658 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_error));
659 assert!(
660 node.trigger_transition(LifecycleTransition::Configure)
661 .is_err()
662 );
663 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
664 }
665 }
666
667 #[test]
668 fn test_ctx_node_finalized_rejects() {
669 unsafe {
670 let mut node = LifecyclePollingNodeCtx::new();
671 node.finalize();
672 let err = node
673 .trigger_transition(LifecycleTransition::Configure)
674 .unwrap_err();
675 assert_eq!(err, LifecycleError::NodeFinalized);
676 }
677 }
678
679 #[test]
680 fn test_ctx_node_context_passed() {
681 use core::sync::atomic::{AtomicU32, Ordering};
682 static SEEN: AtomicU32 = AtomicU32::new(0);
683 unsafe extern "C" fn cb_record(ctx: *mut c_void) -> u8 {
684 SEEN.store(ctx as usize as u32, Ordering::Relaxed);
685 TransitionResult::Success as u8
686 }
687
688 unsafe {
689 let mut node = LifecyclePollingNodeCtx::new();
690 node.set_context(0xBEEFu32 as usize as *mut c_void);
691 node.register(LifecycleCallbackSlot::Configure, Some(cb_record));
692 let _ = node.trigger_transition(LifecycleTransition::Configure);
693 assert_eq!(SEEN.load(Ordering::Relaxed), 0xBEEF);
694 }
695 }
696
697 #[test]
698 fn test_ctx_node_clear_callbacks_resets() {
699 unsafe {
700 let mut node = LifecyclePollingNodeCtx::new();
701 node.set_context(core::ptr::dangling_mut::<c_void>());
702 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_success));
703 node.clear_callbacks();
704 assert!(node.context().is_null());
705 assert_eq!(
707 node.trigger_transition(LifecycleTransition::Configure)
708 .unwrap(),
709 LifecycleState::Inactive
710 );
711 }
712 }
713}