pasaka/
lib.rs

1//! # Example
2//! ```
3//! # #[macro_use] extern crate pasaka;
4//! use pasaka::*;
5//!
6//! #[passage]
7//! fn Example(mut h: PassageHandle, state: i32) -> PassageResult {
8//!     h.text("Text can be outputted using .text.")
9//!         .text("It can also be chained.")
10//!         .text(format!("Count: {state}"));
11//!
12//!     h.choice()
13//!         .option("The expression is usually a choice", |h, state| {
14//!             h.passage(Example, state + 1)
15//!         })
16//!         .build(state)
17//! }
18//!
19//! # fn main() {}
20//! ```
21
22extern crate self as pasaka;
23
24use std::{
25    collections::HashMap,
26    sync::{LazyLock, Mutex},
27};
28
29mod choice;
30pub use choice::PassageHandle;
31pub use choice::PassageResult;
32
33mod engine;
34pub use engine::Engine;
35
36mod runner;
37pub use runner::*;
38
39pub trait PassageImpl: 'static
40where
41    Self::State: serde::Serialize + for<'a> serde::Deserialize<'a>,
42{
43    type State;
44
45    fn run(&self, h: PassageHandle, state: Self::State) -> PassageResult;
46
47    fn with_state(&self, state: Self::State) -> Passage {
48        Passage {
49            state: serde_json::to_value(state).unwrap(),
50            registry_key: format!("{}::{}", self.module_path(), self.name()),
51        }
52    }
53
54    fn name(&self) -> &'static str;
55
56    fn module_path(&self) -> &'static str;
57}
58
59/// Combination of a passage name together with its state.
60///
61/// [Passage] can be used to provide a "callback".
62#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
63pub struct Passage {
64    state: serde_json::Value,
65    registry_key: String,
66}
67
68impl Passage {
69    pub fn run(self, h: PassageHandle) -> PassageResult {
70        let guard = PASSAGE_REGISTRY
71            .lock()
72            .expect("accessing passage registry should not panic");
73        let f = guard.get(&self.registry_key).expect(&format!(
74            "passage {} should be registered using #[passage]",
75            self.registry_key
76        ));
77
78        f(h, self.state)
79    }
80
81    pub fn state<S: for<'a> serde::Deserialize<'a>>(&self) -> Option<S> {
82        serde_json::from_value(self.state.clone()).ok()
83    }
84}
85
86type BoxedPassage = Box<dyn Fn(PassageHandle, serde_json::Value) -> PassageResult + Send + Sync>;
87
88static PASSAGE_REGISTRY: LazyLock<Mutex<HashMap<String, BoxedPassage>>> =
89    LazyLock::new(|| Mutex::new(HashMap::new()));
90
91/// Register passage to the global registry.
92/// You should not call this function,
93/// as the [`#[passage]`](passage) macro calls it for you.
94pub fn register_passage<P: PassageImpl + Send + Sync>(passage: P) {
95    PASSAGE_REGISTRY
96        .lock()
97        .expect("registering passage registry should not panic")
98        .insert(
99            format!("{}::{}", passage.module_path(), passage.name()),
100            Box::new(move |h, value| {
101                let state: P::State = serde_json::from_value(value)
102                    .expect("deserialized value should match passage state");
103                passage.run(h, state)
104            }),
105        );
106}
107
108pub use pasaka_macro::passage;
109
110/// Re-exports for use inside of the [`#[passage]`](passage) macro.
111pub mod macro_support {
112    /// Re-export for use inside of the [`#[passage]`](crate::passage) macro.
113    pub mod ctor {
114        pub use ctor::*;
115    }
116
117    /// Re-export for use inside of the [`#[passage]`](crate::passage) macro.
118    pub mod serde {
119        pub use serde::*;
120    }
121
122    /// Re-export for use inside of the [`#[passage]`](crate::passage) macro.
123    #[cfg(feature = "web")]
124    pub mod yew {
125        pub use yew::*;
126    }
127}