Skip to main content

ranvier_core/
static_gen.rs

1//! Static State Generation Support
2//!
3//! This module provides traits and types for building static state at build time.
4//! Static axons are executed without external input to generate pre-computed state
5//! for frontend SSG (Static Site Generation).
6
7use crate::bus::Bus;
8use crate::outcome::Outcome;
9use crate::schematic::NodeKind;
10use anyhow::Result;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::path::Path;
14
15/// Legacy trait for static graph nodes (kept for backward compatibility)
16#[deprecated(
17    since = "0.9.0",
18    note = "Legacy trait kept for backward compatibility. Use StaticAxon instead."
19)]
20pub trait StaticNode {
21    /// Unique identifier for the node
22    fn id(&self) -> &'static str;
23
24    /// The kind of node (Start, Process, etc.)
25    fn kind(&self) -> NodeKind;
26
27    /// List of IDs of nodes this node connects to
28    fn next_nodes(&self) -> Vec<&'static str>;
29}
30
31/// Marker trait for axons that can be executed statically at build time.
32///
33/// # Static Safety Contract
34///
35/// Static axons MUST:
36/// - Have NO external input (request-free execution)
37/// - Have limited side-effects (read-only operations preferred)
38/// - Be deterministic (same input → same output)
39/// - NOT use WebSocket, DB writes, or random sources
40///
41/// # Example
42///
43/// ```rust
44/// use ranvier_core::static_gen::StaticAxon;
45/// use ranvier_core::Bus;
46/// use ranvier_core::Outcome;
47/// use anyhow::Result;
48///
49/// # #[derive(serde::Serialize)]
50/// # struct LandingState { title: String }
51/// #
52/// # #[derive(Debug, Clone)]
53/// # struct AppError;
54/// #
55/// # impl std::fmt::Display for AppError {
56/// #     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57/// #         write!(f, "AppError")
58/// #     }
59/// # }
60/// #
61/// # impl std::error::Error for AppError {}
62/// #
63/// # impl From<anyhow::Error> for AppError {
64/// #     fn from(_: anyhow::Error) -> Self { AppError }
65/// # }
66/// #
67/// struct LandingPageAxon;
68///
69/// impl StaticAxon for LandingPageAxon {
70///     type Output = LandingState;
71///     type Error = AppError;
72///
73///     fn name(&self) -> &'static str {
74///         "landing_page"
75///     }
76///
77///     fn generate(&self, _bus: &mut Bus) -> Result<Outcome<LandingState, AppError>> {
78///         // Load features, pricing from read-only sources
79///         Ok(Outcome::Next(LandingState { title: "Welcome".to_string() }))
80///     }
81/// }
82/// ```
83pub trait StaticAxon: Send + Sync {
84    /// The output state type (must be serializable)
85    type Output: Serialize;
86
87    /// Error type for static generation failures
88    type Error: Into<anyhow::Error> + std::fmt::Debug;
89
90    /// Unique identifier for this static state
91    fn name(&self) -> &'static str;
92
93    /// Execute the static axon to generate state.
94    ///
95    /// This is called at build time with an empty or pre-configured Bus.
96    fn generate(&self, bus: &mut Bus) -> Result<Outcome<Self::Output, Self::Error>>;
97}
98
99/// Manifest for static build output.
100///
101/// The manifest lists all generated static states and metadata about the build.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct StaticManifest {
104    /// Schema version
105    pub version: String,
106
107    /// When this build was generated
108    pub generated_at: DateTime<Utc>,
109
110    /// List of generated state entries
111    pub states: Vec<StaticStateEntry>,
112}
113
114impl StaticManifest {
115    /// Create a new manifest with the current timestamp
116    pub fn new() -> Self {
117        Self {
118            version: env!("CARGO_PKG_VERSION").to_string(),
119            generated_at: Utc::now(),
120            states: Vec::new(),
121        }
122    }
123
124    /// Add a state entry to the manifest
125    pub fn add_state(&mut self, name: impl Into<String>, file: impl Into<String>) {
126        self.states.push(StaticStateEntry {
127            name: name.into(),
128            file: file.into(),
129            content_type: "application/json".to_string(),
130        });
131    }
132}
133
134impl Default for StaticManifest {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140/// Entry in the static build manifest.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct StaticStateEntry {
143    /// Logical name of the state (e.g., "landing_page")
144    pub name: String,
145
146    /// Relative path to the generated JSON file
147    pub file: String,
148
149    /// MIME type of the content
150    pub content_type: String,
151}
152
153/// Configuration for static builds.
154#[derive(Debug, Clone)]
155pub struct StaticBuildConfig {
156    /// Output directory for generated files
157    pub output_dir: Option<String>,
158
159    /// Optional filter to build only specific axons
160    pub only: Option<String>,
161
162    /// Whether to include schematic.json in output
163    pub include_schematic: bool,
164
165    /// Whether to pretty-print JSON output
166    pub pretty: bool,
167}
168
169impl StaticBuildConfig {
170    /// Create a new default build config
171    pub fn new() -> Self {
172        Self {
173            output_dir: None,
174            only: None,
175            include_schematic: true,
176            pretty: true,
177        }
178    }
179
180    /// Set the output directory
181    pub fn with_output_dir(mut self, dir: impl Into<String>) -> Self {
182        self.output_dir = Some(dir.into());
183        self
184    }
185
186    /// Filter to build only the specified axon
187    pub fn with_only(mut self, name: impl Into<String>) -> Self {
188        self.only = Some(name.into());
189        self
190    }
191
192    /// Enable or disable schematic output
193    pub fn with_schematic(mut self, include: bool) -> Self {
194        self.include_schematic = include;
195        self
196    }
197
198    /// Enable or disable pretty JSON output
199    pub fn with_pretty(mut self, pretty: bool) -> Self {
200        self.pretty = pretty;
201        self
202    }
203
204    /// Get the default output directory
205    pub fn get_output_dir(&self) -> &str {
206        self.output_dir.as_deref().unwrap_or("./dist/static")
207    }
208}
209
210impl Default for StaticBuildConfig {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216/// Result of a single static build execution.
217#[derive(Debug)]
218pub struct StaticBuildResult {
219    /// Name of the axon that was built
220    pub name: String,
221
222    /// Path to the generated JSON file
223    pub file_path: String,
224
225    /// Whether the build was successful
226    pub success: bool,
227}
228
229/// Write a serializable value to a JSON file.
230#[deprecated(since = "0.9.0", note = "Internal API")]
231pub fn write_json_file<T: Serialize>(path: &Path, value: &T, pretty: bool) -> anyhow::Result<()> {
232    let json = if pretty {
233        serde_json::to_string_pretty(value)?
234    } else {
235        serde_json::to_string(value)?
236    };
237
238    // Ensure parent directory exists
239    if let Some(parent) = path.parent() {
240        std::fs::create_dir_all(parent)?;
241    }
242
243    std::fs::write(path, json)?;
244    Ok(())
245}
246
247/// Read a JSON file and deserialize it.
248#[deprecated(since = "0.9.0", note = "Internal API")]
249pub fn read_json_file<T: for<'de> Deserialize<'de>>(path: &Path) -> anyhow::Result<T> {
250    let content = std::fs::read_to_string(path)?;
251    let value = serde_json::from_str(&content)?;
252    Ok(value)
253}