1use std::sync::Arc;
2
3use crate::context::GlobalContext;
4use crate::discovery::ComponentRegistry;
5use crate::module::{create_nested_instance, ModuleBuilder, ModuleDefinition, ModuleInstance};
6use crate::router::HypenRouter;
7use crate::state::State;
8
9pub struct HypenApp {
43 context: Arc<GlobalContext>,
44 router: Arc<HypenRouter>,
45 components: ComponentRegistry,
46 routes: Vec<RouteEntry>,
47}
48
49struct RouteEntry {
51 pattern: String,
52 module_name: String,
53}
54
55impl HypenApp {
56 pub fn builder() -> HypenAppBuilder {
57 HypenAppBuilder {
58 components: ComponentRegistry::new(),
59 routes: Vec::new(),
60 components_dirs: Vec::new(),
61 }
62 }
63
64 pub fn module<S: State>(name: impl Into<String>) -> ModuleBuilder<S> {
73 ModuleBuilder::new(name)
74 }
75
76 pub fn context(&self) -> &GlobalContext {
78 &self.context
79 }
80
81 pub fn router(&self) -> &HypenRouter {
83 &self.router
84 }
85
86 pub fn components(&self) -> &ComponentRegistry {
88 &self.components
89 }
90
91 pub fn instantiate<S: State>(
93 &self,
94 definition: Arc<ModuleDefinition<S>>,
95 ) -> crate::error::Result<ModuleInstance<S>> {
96 ModuleInstance::new(definition, Some(Arc::clone(&self.context)))
97 }
98
99 pub fn instantiate_nested<S: State>(
116 &self,
117 definition: Arc<ModuleDefinition<S>>,
118 ) -> crate::error::Result<ModuleInstance<S>> {
119 create_nested_instance(definition, Arc::clone(&self.context))
120 }
121
122 pub fn instantiate_with_components<S: State>(
127 &self,
128 definition: Arc<ModuleDefinition<S>>,
129 ) -> crate::error::Result<ModuleInstance<S>> {
130 ModuleInstance::new_with_components(
131 definition,
132 Some(Arc::clone(&self.context)),
133 &self.components,
134 )
135 }
136
137 pub fn navigate(&self, path: &str) {
139 self.router.push(path);
140 }
141
142 pub fn match_route(&self, path: &str) -> Option<(&str, &str)> {
144 for entry in &self.routes {
145 if self.router.match_path(&entry.pattern, path).is_some() {
146 return Some((&entry.pattern, &entry.module_name));
147 }
148 }
149 None
150 }
151}
152
153impl Default for HypenApp {
154 fn default() -> Self {
155 let context = Arc::new(GlobalContext::new());
156 let router = Arc::new(HypenRouter::new());
157 context.set_router(Arc::clone(&router));
158 Self {
159 context,
160 router,
161 components: ComponentRegistry::new(),
162 routes: Vec::new(),
163 }
164 }
165}
166
167pub struct HypenAppBuilder {
169 components: ComponentRegistry,
170 routes: Vec<RouteEntry>,
171 components_dirs: Vec<String>,
172}
173
174impl HypenAppBuilder {
175 pub fn route<S: State>(
180 mut self,
181 pattern: impl Into<String>,
182 definition: ModuleDefinition<S>,
183 ) -> Self {
184 let pattern = pattern.into();
185 let name = definition.name().to_string();
186 self.routes.push(RouteEntry {
187 pattern,
188 module_name: name,
189 });
190 if let Some(source) = definition.ui_source() {
192 self.components.register(definition.name(), source, None);
193 }
194 self
195 }
196
197 pub fn component(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
199 self.components.register(name, source, None);
200 self
201 }
202
203 pub fn components_dir(mut self, dir: impl Into<String>) -> Self {
205 self.components_dirs.push(dir.into());
206 self
207 }
208
209 pub fn build(mut self) -> HypenApp {
211 for dir in &self.components_dirs {
213 if let Err(e) = self.components.load_dir(dir) {
215 eprintln!("[hypen-server] Warning: failed to load components from {dir}: {e}");
216 }
217 }
218
219 let context = Arc::new(GlobalContext::new());
220 let router = Arc::new(HypenRouter::new());
221 context.set_router(Arc::clone(&router));
222
223 HypenApp {
224 context,
225 router,
226 components: self.components,
227 routes: self.routes,
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use serde::{Deserialize, Serialize};
236
237 #[derive(Clone, Default, Serialize, Deserialize)]
238 struct TestState {
239 value: i32,
240 }
241
242 #[test]
243 fn test_module_shorthand() {
244 let def = HypenApp::module::<TestState>("MyModule")
245 .state(TestState { value: 42 })
246 .on_action::<()>("inc", |state, _, _| {
247 state.value += 1;
248 })
249 .build();
250
251 assert_eq!(def.name(), "MyModule");
252 }
253
254 #[test]
255 fn test_app_builder() {
256 let app = HypenApp::builder()
257 .component("Button", r#"Button { Text("Click") }"#)
258 .build();
259
260 assert!(app.components().has("Button"));
261 }
262
263 #[test]
264 fn test_app_with_routes() {
265 let app = HypenApp::builder()
266 .route(
267 "/",
268 HypenApp::module::<TestState>("Home")
269 .state(TestState::default())
270 .build(),
271 )
272 .route(
273 "/about",
274 HypenApp::module::<TestState>("About")
275 .state(TestState::default())
276 .build(),
277 )
278 .build();
279
280 let (_, module) = app.match_route("/").unwrap();
281 assert_eq!(module, "Home");
282
283 let (_, module) = app.match_route("/about").unwrap();
284 assert_eq!(module, "About");
285
286 assert!(app.match_route("/nonexistent").is_none());
287 }
288
289 #[test]
290 fn test_app_navigate() {
291 let app = HypenApp::default();
292 app.navigate("/users/42");
293 assert_eq!(app.router().current_path(), "/users/42");
294 }
295
296 #[test]
297 fn test_components_dir() {
298 let dir = std::env::temp_dir().join("hypen_test_app_components");
299 let _ = std::fs::remove_dir_all(&dir);
300 std::fs::create_dir_all(&dir).unwrap();
301
302 std::fs::write(dir.join("my-widget.hypen"), r#"Column { Text("Widget") }"#).unwrap();
303
304 let app = HypenApp::builder()
305 .components_dir(dir.to_str().unwrap())
306 .build();
307
308 assert!(app.components().has("MyWidget"));
309
310 let _ = std::fs::remove_dir_all(&dir);
311 }
312
313 #[test]
314 fn test_instantiate_module() {
315 let app = HypenApp::default();
316
317 let def = HypenApp::module::<TestState>("Test")
318 .state(TestState { value: 10 })
319 .on_action::<()>("double", |state, _, _| {
320 state.value *= 2;
321 })
322 .build();
323
324 let instance = app.instantiate(Arc::new(def)).unwrap();
325 instance.mount();
326 assert_eq!(instance.get_state().value, 10);
327
328 instance.dispatch_action("double", None).unwrap();
329 assert_eq!(instance.get_state().value, 20);
330 }
331}