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}