Skip to main content

reinhardt_apps/
builder.rs

1//! Application builder module
2//!
3//! Provides a builder pattern for configuring and constructing Reinhardt applications.
4//! Inspired by Django's application configuration system.
5
6use crate::apps::{AppConfig, AppError, Apps};
7use std::collections::HashMap;
8use std::sync::Arc;
9use thiserror::Error;
10
11/// Errors that can occur when building an application
12#[derive(Debug, Error)]
13pub enum BuildError {
14	/// An error originating from the application registry.
15	#[error("Application error: {0}")]
16	App(#[from] AppError),
17
18	/// The provided configuration value is invalid.
19	#[error("Invalid configuration: {0}")]
20	InvalidConfig(String),
21
22	/// A required configuration value was not provided.
23	#[error("Missing required configuration: {0}")]
24	MissingConfig(String),
25
26	/// An error in route configuration.
27	#[error("Route configuration error: {0}")]
28	RouteError(String),
29
30	/// An error in database configuration.
31	#[error("Database configuration error: {0}")]
32	DatabaseError(String),
33}
34
35/// A specialized `Result` type for application build operations.
36pub type BuildResult<T> = Result<T, BuildError>;
37
38/// Route definition for the application
39/// Lightweight wrapper around path patterns
40#[derive(Clone)]
41pub struct RouteConfig {
42	/// URL path pattern for this route.
43	pub path: String,
44	/// Name of the handler associated with this route.
45	pub handler_name: String,
46	/// Optional name for reverse URL lookup.
47	pub name: Option<String>,
48	/// Optional namespace for grouping routes.
49	pub namespace: Option<String>,
50}
51
52impl RouteConfig {
53	/// Create a new route configuration
54	///
55	/// # Examples
56	///
57	/// ```
58	/// use reinhardt_apps::builder::RouteConfig;
59	///
60	/// let route = RouteConfig::new("/users/", "UserListHandler");
61	/// assert_eq!(route.path, "/users/");
62	/// assert_eq!(route.handler_name, "UserListHandler");
63	/// ```
64	pub fn new(path: impl Into<String>, handler_name: impl Into<String>) -> Self {
65		Self {
66			path: path.into(),
67			handler_name: handler_name.into(),
68			name: None,
69			namespace: None,
70		}
71	}
72
73	/// Set the route name
74	///
75	/// # Examples
76	///
77	/// ```
78	/// use reinhardt_apps::builder::RouteConfig;
79	///
80	/// let route = RouteConfig::new("/users/", "UserListHandler")
81	///     .with_name("user-list");
82	/// assert_eq!(route.name, Some("user-list".to_string()));
83	/// ```
84	pub fn with_name(mut self, name: impl Into<String>) -> Self {
85		self.name = Some(name.into());
86		self
87	}
88
89	/// Set the route namespace
90	///
91	/// # Examples
92	///
93	/// ```
94	/// use reinhardt_apps::builder::RouteConfig;
95	///
96	/// let route = RouteConfig::new("/users/", "UserListHandler")
97	///     .with_namespace("api");
98	/// assert_eq!(route.namespace, Some("api".to_string()));
99	/// ```
100	pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
101		self.namespace = Some(namespace.into());
102		self
103	}
104
105	/// Get the full name including namespace
106	///
107	/// # Examples
108	///
109	/// ```
110	/// use reinhardt_apps::builder::RouteConfig;
111	///
112	/// let route = RouteConfig::new("/users/", "UserListHandler")
113	///     .with_namespace("api")
114	///     .with_name("list");
115	/// assert_eq!(route.full_name(), Some("api:list".to_string()));
116	///
117	/// let route = RouteConfig::new("/users/", "UserListHandler")
118	///     .with_name("list");
119	/// assert_eq!(route.full_name(), Some("list".to_string()));
120	///
121	/// let route = RouteConfig::new("/users/", "UserListHandler");
122	/// assert_eq!(route.full_name(), None);
123	/// ```
124	pub fn full_name(&self) -> Option<String> {
125		match (&self.namespace, &self.name) {
126			(Some(ns), Some(name)) => Some(format!("{}:{}", ns, name)),
127			(None, Some(name)) => Some(name.clone()),
128			_ => None,
129		}
130	}
131}
132
133/// Database configuration for the application
134#[derive(Clone, Debug)]
135pub struct ApplicationDatabaseConfig {
136	/// Database connection URL.
137	pub url: String,
138	/// Maximum number of connections in the pool.
139	pub pool_size: Option<u32>,
140	/// Maximum number of connections that can exceed `pool_size`.
141	pub max_overflow: Option<u32>,
142	/// Connection timeout in seconds.
143	pub timeout: Option<u64>,
144}
145
146impl ApplicationDatabaseConfig {
147	/// Create a new database configuration.
148	///
149	/// The URL is stored as-is at this stage; scheme validation is performed
150	/// later by [`ApplicationBuilder::build`] so that
151	/// [`ApplicationDatabaseConfig`] remains a plain data carrier. Build-time
152	/// validation rejects unrecognized schemes (see issue
153	/// [#485](https://github.com/kent8192/reinhardt-web/issues/485)) before any
154	/// connection attempt is made.
155	///
156	/// A future major release may move this validation into the constructor
157	/// itself; see issue
158	/// [#4056](https://github.com/kent8192/reinhardt-web/issues/4056) for the
159	/// breaking-change proposal.
160	///
161	/// # Examples
162	///
163	/// ```
164	/// use reinhardt_apps::builder::ApplicationDatabaseConfig;
165	///
166	/// let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb");
167	/// assert_eq!(db_config.url, "postgresql://localhost/mydb");
168	/// ```
169	pub fn new(url: impl Into<String>) -> Self {
170		Self {
171			url: url.into(),
172			pool_size: None,
173			max_overflow: None,
174			timeout: None,
175		}
176	}
177
178	/// Set the connection pool size
179	///
180	/// # Examples
181	///
182	/// ```
183	/// use reinhardt_apps::builder::ApplicationDatabaseConfig;
184	///
185	/// let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb")
186	///     .with_pool_size(10);
187	/// assert_eq!(db_config.pool_size, Some(10));
188	/// ```
189	pub fn with_pool_size(mut self, size: u32) -> Self {
190		self.pool_size = Some(size);
191		self
192	}
193
194	/// Set the maximum overflow connections
195	///
196	/// # Examples
197	///
198	/// ```
199	/// use reinhardt_apps::builder::ApplicationDatabaseConfig;
200	///
201	/// let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb")
202	///     .with_max_overflow(5);
203	/// assert_eq!(db_config.max_overflow, Some(5));
204	/// ```
205	pub fn with_max_overflow(mut self, overflow: u32) -> Self {
206		self.max_overflow = Some(overflow);
207		self
208	}
209
210	/// Set the connection timeout
211	///
212	/// # Examples
213	///
214	/// ```
215	/// use reinhardt_apps::builder::ApplicationDatabaseConfig;
216	///
217	/// let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb")
218	///     .with_timeout(30);
219	/// assert_eq!(db_config.timeout, Some(30));
220	/// ```
221	pub fn with_timeout(mut self, timeout: u64) -> Self {
222		self.timeout = Some(timeout);
223		self
224	}
225}
226
227/// Builder for constructing Reinhardt applications
228/// Inspired by Django's application configuration system
229pub struct ApplicationBuilder {
230	apps: Vec<AppConfig>,
231	middleware: Vec<String>,
232	url_patterns: Vec<RouteConfig>,
233	database_config: Option<ApplicationDatabaseConfig>,
234	settings: HashMap<String, String>,
235}
236
237impl ApplicationBuilder {
238	/// Create a new application builder
239	///
240	/// # Examples
241	///
242	/// ```
243	/// use reinhardt_apps::builder::ApplicationBuilder;
244	///
245	/// let builder = ApplicationBuilder::new();
246	/// let app = builder.build().unwrap();
247	/// assert_eq!(app.apps().len(), 0);
248	/// ```
249	pub fn new() -> Self {
250		Self {
251			apps: Vec::new(),
252			middleware: Vec::new(),
253			url_patterns: Vec::new(),
254			database_config: None,
255			settings: HashMap::new(),
256		}
257	}
258
259	/// Add an application configuration
260	///
261	/// # Examples
262	///
263	/// ```
264	/// use reinhardt_apps::builder::ApplicationBuilder;
265	/// use reinhardt_apps::AppConfig;
266	///
267	/// let app_config = AppConfig::new("myapp", "myapp");
268	/// let builder = ApplicationBuilder::new()
269	///     .add_app(app_config);
270	/// let app = builder.build().unwrap();
271	/// assert_eq!(app.apps().len(), 1);
272	/// ```
273	pub fn add_app(mut self, app: AppConfig) -> Self {
274		self.apps.push(app);
275		self
276	}
277
278	/// Add multiple application configurations
279	///
280	/// # Examples
281	///
282	/// ```
283	/// use reinhardt_apps::builder::ApplicationBuilder;
284	/// use reinhardt_apps::AppConfig;
285	///
286	/// let apps = vec![
287	///     AppConfig::new("app1", "app1"),
288	///     AppConfig::new("app2", "app2"),
289	/// ];
290	/// let builder = ApplicationBuilder::new()
291	///     .add_apps(apps);
292	/// let app = builder.build().unwrap();
293	/// assert_eq!(app.apps().len(), 2);
294	/// ```
295	pub fn add_apps(mut self, apps: Vec<AppConfig>) -> Self {
296		self.apps.extend(apps);
297		self
298	}
299
300	/// Add a middleware
301	///
302	/// # Examples
303	///
304	/// ```
305	/// use reinhardt_apps::builder::ApplicationBuilder;
306	///
307	/// let builder = ApplicationBuilder::new()
308	///     .add_middleware("CorsMiddleware");
309	/// let app = builder.build().unwrap();
310	/// assert_eq!(app.middleware().len(), 1);
311	/// ```
312	pub fn add_middleware(mut self, middleware: impl Into<String>) -> Self {
313		self.middleware.push(middleware.into());
314		self
315	}
316
317	/// Add multiple middleware
318	///
319	/// # Examples
320	///
321	/// ```
322	/// use reinhardt_apps::builder::ApplicationBuilder;
323	///
324	/// let middleware = vec!["CorsMiddleware", "AuthMiddleware"];
325	/// let builder = ApplicationBuilder::new()
326	///     .add_middlewares(middleware);
327	/// let app = builder.build().unwrap();
328	/// assert_eq!(app.middleware().len(), 2);
329	/// ```
330	pub fn add_middlewares<S: Into<String>>(mut self, middleware: Vec<S>) -> Self {
331		self.middleware
332			.extend(middleware.into_iter().map(|m| m.into()));
333		self
334	}
335
336	/// Add a URL pattern
337	///
338	/// # Examples
339	///
340	/// ```
341	/// use reinhardt_apps::builder::{ApplicationBuilder, RouteConfig};
342	///
343	/// let route = RouteConfig::new("/users/", "UserListHandler");
344	/// let builder = ApplicationBuilder::new()
345	///     .add_url_pattern(route);
346	/// let app = builder.build().unwrap();
347	/// assert_eq!(app.url_patterns().len(), 1);
348	/// ```
349	pub fn add_url_pattern(mut self, pattern: RouteConfig) -> Self {
350		self.url_patterns.push(pattern);
351		self
352	}
353
354	/// Add multiple URL patterns
355	///
356	/// # Examples
357	///
358	/// ```
359	/// use reinhardt_apps::builder::{ApplicationBuilder, RouteConfig};
360	///
361	/// let patterns = vec![
362	///     RouteConfig::new("/users/", "UserListHandler"),
363	///     RouteConfig::new("/posts/", "PostListHandler"),
364	/// ];
365	/// let builder = ApplicationBuilder::new()
366	///     .add_url_patterns(patterns);
367	/// let app = builder.build().unwrap();
368	/// assert_eq!(app.url_patterns().len(), 2);
369	/// ```
370	pub fn add_url_patterns(mut self, patterns: Vec<RouteConfig>) -> Self {
371		self.url_patterns.extend(patterns);
372		self
373	}
374
375	/// Set the database configuration
376	///
377	/// # Examples
378	///
379	/// ```
380	/// use reinhardt_apps::builder::{ApplicationBuilder, ApplicationDatabaseConfig};
381	///
382	/// let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb");
383	/// let builder = ApplicationBuilder::new()
384	///     .database(db_config);
385	/// let app = builder.build().unwrap();
386	/// assert!(app.database_config().is_some());
387	/// ```
388	pub fn database(mut self, config: ApplicationDatabaseConfig) -> Self {
389		self.database_config = Some(config);
390		self
391	}
392
393	/// Add a custom setting
394	///
395	/// # Examples
396	///
397	/// ```
398	/// use reinhardt_apps::builder::ApplicationBuilder;
399	///
400	/// let builder = ApplicationBuilder::new()
401	///     .add_setting("DEBUG", "true");
402	/// let app = builder.build().unwrap();
403	/// assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
404	/// ```
405	pub fn add_setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
406		self.settings.insert(key.into(), value.into());
407		self
408	}
409
410	/// Add multiple custom settings
411	///
412	/// # Examples
413	///
414	/// ```
415	/// use reinhardt_apps::builder::ApplicationBuilder;
416	/// use std::collections::HashMap;
417	///
418	/// let mut settings = HashMap::new();
419	/// settings.insert("DEBUG".to_string(), "true".to_string());
420	/// settings.insert("SECRET_KEY".to_string(), "secret".to_string());
421	///
422	/// let builder = ApplicationBuilder::new()
423	///     .add_settings(settings);
424	/// let app = builder.build().unwrap();
425	/// assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
426	/// ```
427	pub fn add_settings(mut self, settings: HashMap<String, String>) -> Self {
428		self.settings.extend(settings);
429		self
430	}
431
432	/// Validate the configuration
433	fn validate(&self) -> BuildResult<()> {
434		// Validate all app configurations
435		for app in &self.apps {
436			app.validate_label()?;
437		}
438
439		// Check for duplicate app labels
440		let mut labels = std::collections::HashSet::new();
441		for app in &self.apps {
442			if !labels.insert(&app.label) {
443				return Err(BuildError::InvalidConfig(format!(
444					"Duplicate app label: {}",
445					app.label
446				)));
447			}
448		}
449
450		// Check for duplicate route names
451		let mut route_names = std::collections::HashSet::new();
452		for pattern in &self.url_patterns {
453			if let Some(full_name) = pattern.full_name()
454				&& !route_names.insert(full_name.clone())
455			{
456				return Err(BuildError::RouteError(format!(
457					"Duplicate route name: {}",
458					full_name
459				)));
460			}
461		}
462
463		// Validate the database URL scheme at build time so that obviously
464		// malformed URLs surface as a clear configuration error rather than
465		// later as an opaque connection failure (issue #485).
466		if let Some(db_config) = &self.database_config {
467			reinhardt_conf::settings::database_config::validate_database_url_scheme(&db_config.url)
468				.map_err(BuildError::DatabaseError)?;
469		}
470
471		Ok(())
472	}
473
474	/// Build the application
475	///
476	/// # Examples
477	///
478	/// ```
479	/// use reinhardt_apps::builder::ApplicationBuilder;
480	/// use reinhardt_apps::AppConfig;
481	///
482	/// let app_config = AppConfig::new("myapp", "myapp");
483	/// let builder = ApplicationBuilder::new()
484	///     .add_app(app_config)
485	///     .add_middleware("CorsMiddleware");
486	///
487	/// let app = builder.build().unwrap();
488	/// assert_eq!(app.apps().len(), 1);
489	/// assert_eq!(app.middleware().len(), 1);
490	/// ```
491	pub fn build(self) -> BuildResult<Application> {
492		// Validate configuration
493		self.validate()?;
494
495		// Create the Apps registry
496		let installed_apps: Vec<String> = self.apps.iter().map(|app| app.name.clone()).collect();
497		let apps_registry = Apps::new(installed_apps);
498
499		// Register all app configurations
500		for app in &self.apps {
501			apps_registry.register(app.clone())?;
502		}
503
504		// Populate the registry
505		apps_registry.populate()?;
506
507		Ok(Application {
508			apps: self.apps,
509			middleware: self.middleware,
510			url_patterns: self.url_patterns,
511			database_config: self.database_config,
512			settings: self.settings,
513			apps_registry: Arc::new(apps_registry),
514		})
515	}
516
517	/// Build the application and register it with the DI system
518	///
519	/// This method builds the application and registers both the `Application`
520	/// and `Apps` instances in the provided `SingletonScope`.
521	///
522	/// # Examples
523	///
524	/// ```no_run
525	/// use reinhardt_apps::builder::ApplicationBuilder;
526	/// use reinhardt_apps::AppConfig;
527	/// use reinhardt_di::SingletonScope;
528	/// use std::sync::Arc;
529	///
530	/// let singleton = Arc::new(SingletonScope::new());
531	/// let app_config = AppConfig::new("myapp", "myapp");
532	/// let app = ApplicationBuilder::new()
533	///     .add_app(app_config)
534	///     .build_with_di(singleton.clone())
535	///     .unwrap();
536	/// ```
537	#[cfg(feature = "di")]
538	pub fn build_with_di(
539		self,
540		singleton_scope: Arc<reinhardt_di::SingletonScope>,
541	) -> BuildResult<Arc<Application>> {
542		let app = self.build()?;
543		let app = Arc::new(app);
544
545		// Register Application in SingletonScope
546		singleton_scope.set(app.clone());
547
548		// Register Apps in SingletonScope
549		singleton_scope.set(app.apps_registry.clone());
550
551		Ok(app)
552	}
553}
554
555impl Default for ApplicationBuilder {
556	fn default() -> Self {
557		Self::new()
558	}
559}
560
561/// The built application
562pub struct Application {
563	apps: Vec<AppConfig>,
564	middleware: Vec<String>,
565	url_patterns: Vec<RouteConfig>,
566	database_config: Option<ApplicationDatabaseConfig>,
567	settings: HashMap<String, String>,
568	apps_registry: Arc<Apps>,
569}
570
571impl Application {
572	/// Get the registered applications
573	///
574	/// # Examples
575	///
576	/// ```
577	/// use reinhardt_apps::builder::ApplicationBuilder;
578	/// use reinhardt_apps::AppConfig;
579	///
580	/// let app_config = AppConfig::new("myapp", "myapp");
581	/// let builder = ApplicationBuilder::new()
582	///     .add_app(app_config);
583	/// let app = builder.build().unwrap();
584	///
585	/// assert_eq!(app.apps().len(), 1);
586	/// assert_eq!(app.apps()[0].label, "myapp");
587	/// ```
588	pub fn apps(&self) -> &[AppConfig] {
589		&self.apps
590	}
591
592	/// Get the middleware stack
593	///
594	/// # Examples
595	///
596	/// ```
597	/// use reinhardt_apps::builder::ApplicationBuilder;
598	///
599	/// let builder = ApplicationBuilder::new()
600	///     .add_middleware("CorsMiddleware")
601	///     .add_middleware("AuthMiddleware");
602	/// let app = builder.build().unwrap();
603	///
604	/// assert_eq!(app.middleware().len(), 2);
605	/// assert_eq!(app.middleware()[0], "CorsMiddleware");
606	/// ```
607	pub fn middleware(&self) -> &[String] {
608		&self.middleware
609	}
610
611	/// Get the URL patterns
612	///
613	/// # Examples
614	///
615	/// ```
616	/// use reinhardt_apps::builder::{ApplicationBuilder, RouteConfig};
617	///
618	/// let route = RouteConfig::new("/users/", "UserListHandler");
619	/// let builder = ApplicationBuilder::new()
620	///     .add_url_pattern(route);
621	/// let app = builder.build().unwrap();
622	///
623	/// assert_eq!(app.url_patterns().len(), 1);
624	/// assert_eq!(app.url_patterns()[0].path, "/users/");
625	/// ```
626	pub fn url_patterns(&self) -> &[RouteConfig] {
627		&self.url_patterns
628	}
629
630	/// Get the database configuration
631	///
632	/// # Examples
633	///
634	/// ```
635	/// use reinhardt_apps::builder::{ApplicationBuilder, ApplicationDatabaseConfig};
636	///
637	/// let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb");
638	/// let builder = ApplicationBuilder::new()
639	///     .database(db_config);
640	/// let app = builder.build().unwrap();
641	///
642	/// assert!(app.database_config().is_some());
643	/// assert_eq!(app.database_config().unwrap().url, "postgresql://localhost/mydb");
644	/// ```
645	pub fn database_config(&self) -> Option<&ApplicationDatabaseConfig> {
646		self.database_config.as_ref()
647	}
648
649	/// Get the custom settings
650	///
651	/// # Examples
652	///
653	/// ```
654	/// use reinhardt_apps::builder::ApplicationBuilder;
655	///
656	/// let builder = ApplicationBuilder::new()
657	///     .add_setting("DEBUG", "true");
658	/// let app = builder.build().unwrap();
659	///
660	/// assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
661	/// ```
662	pub fn settings(&self) -> &HashMap<String, String> {
663		&self.settings
664	}
665
666	/// Get the apps registry
667	///
668	/// # Examples
669	///
670	/// ```
671	/// use reinhardt_apps::builder::ApplicationBuilder;
672	/// use reinhardt_apps::AppConfig;
673	///
674	/// let app_config = AppConfig::new("myapp", "myapp");
675	/// let builder = ApplicationBuilder::new()
676	///     .add_app(app_config);
677	/// let app = builder.build().unwrap();
678	///
679	/// assert!(app.apps_registry().is_ready());
680	/// assert!(app.apps_registry().is_installed("myapp"));
681	/// ```
682	pub fn apps_registry(&self) -> &Apps {
683		&self.apps_registry
684	}
685}
686
687#[cfg(test)]
688mod tests {
689	use super::*;
690	use serial_test::serial;
691
692	#[test]
693	fn test_route_config_creation() {
694		let route = RouteConfig::new("/users/", "UserListHandler")
695			.with_name("user-list")
696			.with_namespace("api");
697
698		assert_eq!(route.path, "/users/");
699		assert_eq!(route.handler_name, "UserListHandler");
700		assert_eq!(route.name, Some("user-list".to_string()));
701		assert_eq!(route.namespace, Some("api".to_string()));
702	}
703
704	#[test]
705	fn test_route_config_full_name() {
706		let route = RouteConfig::new("/users/", "UserListHandler")
707			.with_namespace("api")
708			.with_name("list");
709		assert_eq!(route.full_name(), Some("api:list".to_string()));
710
711		let route = RouteConfig::new("/users/", "UserListHandler").with_name("list");
712		assert_eq!(route.full_name(), Some("list".to_string()));
713
714		let route = RouteConfig::new("/users/", "UserListHandler");
715		assert_eq!(route.full_name(), None);
716	}
717
718	#[test]
719	fn test_database_config_creation() {
720		let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb")
721			.with_pool_size(10)
722			.with_max_overflow(5)
723			.with_timeout(30);
724
725		assert_eq!(db_config.url, "postgresql://localhost/mydb");
726		assert_eq!(db_config.pool_size, Some(10));
727		assert_eq!(db_config.max_overflow, Some(5));
728		assert_eq!(db_config.timeout, Some(30));
729	}
730
731	#[test]
732	#[serial(apps_registry)]
733	fn test_application_builder_basic() {
734		// Arrange - Reset global state before test
735		crate::registry::reset_global_registry();
736
737		let app = ApplicationBuilder::new().build().unwrap();
738
739		assert_eq!(app.apps().len(), 0);
740		assert_eq!(app.middleware().len(), 0);
741		assert_eq!(app.url_patterns().len(), 0);
742		assert!(app.database_config().is_none());
743	}
744
745	#[test]
746	#[serial(apps_registry)]
747	fn test_application_builder_with_apps() {
748		// Arrange - Reset global state before test
749		crate::registry::reset_global_registry();
750
751		let app_config = AppConfig::new("myapp", "myapp");
752		let app = ApplicationBuilder::new()
753			.add_app(app_config)
754			.build()
755			.unwrap();
756
757		assert_eq!(app.apps().len(), 1);
758		assert_eq!(app.apps()[0].label, "myapp");
759		assert!(app.apps_registry().is_installed("myapp"));
760	}
761
762	#[test]
763	#[serial(apps_registry)]
764	fn test_application_builder_with_multiple_apps() {
765		// Arrange - Reset global state before test
766		crate::registry::reset_global_registry();
767
768		let apps = vec![
769			AppConfig::new("app1", "app1"),
770			AppConfig::new("app2", "app2"),
771		];
772		let app = ApplicationBuilder::new().add_apps(apps).build().unwrap();
773
774		assert_eq!(app.apps().len(), 2);
775		assert!(app.apps_registry().is_installed("app1"));
776		assert!(app.apps_registry().is_installed("app2"));
777	}
778
779	#[test]
780	#[serial(apps_registry)]
781	fn test_application_builder_with_middleware() {
782		// Arrange - Reset global state before test
783		crate::registry::reset_global_registry();
784
785		let app = ApplicationBuilder::new()
786			.add_middleware("CorsMiddleware")
787			.add_middleware("AuthMiddleware")
788			.build()
789			.unwrap();
790
791		assert_eq!(app.middleware().len(), 2);
792		assert_eq!(app.middleware()[0], "CorsMiddleware");
793		assert_eq!(app.middleware()[1], "AuthMiddleware");
794	}
795
796	#[test]
797	#[serial(apps_registry)]
798	fn test_application_builder_with_middlewares() {
799		// Arrange - Reset global state before test
800		crate::registry::reset_global_registry();
801
802		let middleware = vec!["CorsMiddleware", "AuthMiddleware"];
803		let app = ApplicationBuilder::new()
804			.add_middlewares(middleware)
805			.build()
806			.unwrap();
807
808		assert_eq!(app.middleware().len(), 2);
809	}
810
811	#[test]
812	#[serial(apps_registry)]
813	fn test_application_builder_with_url_patterns() {
814		// Arrange - Reset global state before test
815		crate::registry::reset_global_registry();
816
817		let route = RouteConfig::new("/users/", "UserListHandler");
818		let app = ApplicationBuilder::new()
819			.add_url_pattern(route)
820			.build()
821			.unwrap();
822
823		assert_eq!(app.url_patterns().len(), 1);
824		assert_eq!(app.url_patterns()[0].path, "/users/");
825	}
826
827	#[test]
828	#[serial(apps_registry)]
829	fn test_application_builder_with_database() {
830		// Arrange - Reset global state before test
831		crate::registry::reset_global_registry();
832
833		let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb");
834		let app = ApplicationBuilder::new()
835			.database(db_config)
836			.build()
837			.unwrap();
838
839		assert!(app.database_config().is_some());
840		assert_eq!(
841			app.database_config().unwrap().url,
842			"postgresql://localhost/mydb"
843		);
844	}
845
846	#[test]
847	#[serial(apps_registry)]
848	fn test_application_builder_with_settings() {
849		// Arrange - Reset global state before test
850		crate::registry::reset_global_registry();
851
852		let app = ApplicationBuilder::new()
853			.add_setting("DEBUG", "true")
854			.add_setting("SECRET_KEY", "secret")
855			.build()
856			.unwrap();
857
858		assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
859		assert_eq!(
860			app.settings().get("SECRET_KEY"),
861			Some(&"secret".to_string())
862		);
863	}
864
865	#[test]
866	#[serial(apps_registry)]
867	fn test_application_builder_validation_duplicate_apps() {
868		// Arrange - Reset global state before test
869		crate::registry::reset_global_registry();
870
871		let result = ApplicationBuilder::new()
872			.add_app(AppConfig::new("myapp", "myapp"))
873			.add_app(AppConfig::new("another", "myapp"))
874			.build();
875
876		assert!(result.is_err());
877		match result {
878			Err(BuildError::InvalidConfig(msg)) => {
879				assert_eq!(msg, "Duplicate app label: myapp");
880			}
881			_ => panic!("Expected InvalidConfig error"),
882		}
883	}
884
885	#[test]
886	#[serial(apps_registry)]
887	fn test_application_builder_validation_duplicate_routes() {
888		// Arrange - Reset global state before test
889		crate::registry::reset_global_registry();
890
891		let result = ApplicationBuilder::new()
892			.add_url_pattern(RouteConfig::new("/users/", "Handler1").with_name("users"))
893			.add_url_pattern(RouteConfig::new("/posts/", "Handler2").with_name("users"))
894			.build();
895
896		assert!(result.is_err());
897		match result {
898			Err(BuildError::RouteError(msg)) => {
899				assert_eq!(msg, "Duplicate route name: users");
900			}
901			_ => panic!("Expected RouteError"),
902		}
903	}
904
905	#[test]
906	#[serial(apps_registry)]
907	fn test_application_builder_method_chaining() {
908		// Arrange - Reset global state before test
909		crate::registry::reset_global_registry();
910
911		let app = ApplicationBuilder::new()
912			.add_app(AppConfig::new("app1", "app1"))
913			.add_middleware("CorsMiddleware")
914			.add_url_pattern(RouteConfig::new("/api/", "ApiHandler"))
915			.database(ApplicationDatabaseConfig::new("postgresql://localhost/db"))
916			.add_setting("DEBUG", "true")
917			.build()
918			.unwrap();
919
920		assert_eq!(app.apps().len(), 1);
921		assert_eq!(app.middleware().len(), 1);
922		assert_eq!(app.url_patterns().len(), 1);
923		assert!(app.database_config().is_some());
924		assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
925	}
926
927	#[test]
928	#[serial(apps_registry)]
929	fn test_application_builder_apps_registry_ready() {
930		// Arrange - Reset global state before test
931		crate::registry::reset_global_registry();
932
933		let app = ApplicationBuilder::new()
934			.add_app(AppConfig::new("myapp", "myapp"))
935			.build()
936			.unwrap();
937
938		assert!(app.apps_registry().is_ready());
939		assert!(app.apps_registry().is_apps_ready());
940		assert!(app.apps_registry().is_models_ready());
941	}
942
943	#[test]
944	#[serial(apps_registry)]
945	fn test_application_builder_invalid_app_label() {
946		// Arrange - Reset global state before test
947		crate::registry::reset_global_registry();
948
949		let result = ApplicationBuilder::new()
950			.add_app(AppConfig::new("myapp", "my-app"))
951			.build();
952
953		assert!(result.is_err());
954		match result {
955			Err(BuildError::App(AppError::InvalidLabel(_))) => {}
956			_ => panic!("Expected InvalidLabel error"),
957		}
958	}
959
960	#[test]
961	fn test_route_config_without_name() {
962		let route = RouteConfig::new("/api/v1/users/", "UserHandler");
963		assert_eq!(route.full_name(), None);
964	}
965
966	#[test]
967	fn test_database_config_minimal() {
968		let db_config = ApplicationDatabaseConfig::new("sqlite::memory:");
969		assert_eq!(db_config.url, "sqlite::memory:");
970		assert_eq!(db_config.pool_size, None);
971		assert_eq!(db_config.max_overflow, None);
972		assert_eq!(db_config.timeout, None);
973	}
974
975	// Issue #485: build-time validation of the database URL scheme. The
976	// constructor stays infallible (data-carrier role); rejection happens
977	// once, at build() time.
978
979	#[rstest::rstest]
980	#[case::postgres("postgres://localhost/db")]
981	#[case::postgresql("postgresql://user:pass@localhost:5432/db")]
982	#[case::sqlite_memory("sqlite::memory:")]
983	#[case::sqlite_absolute("sqlite:///var/data/db.sqlite3")]
984	#[case::sqlite_relative("sqlite:db.sqlite3")]
985	#[case::mysql("mysql://root@localhost/db")]
986	#[case::mariadb("mariadb://root@localhost/db")]
987	#[serial(apps_registry)]
988	fn test_application_builder_accepts_valid_database_url_scheme(#[case] url: &str) {
989		// Arrange
990		crate::registry::reset_global_registry();
991		let db_config = ApplicationDatabaseConfig::new(url);
992
993		// Act
994		let result = ApplicationBuilder::new().database(db_config).build();
995
996		// Assert
997		assert!(
998			result.is_ok(),
999			"expected URL {:?} to be accepted but got {:?}",
1000			url,
1001			result.err()
1002		);
1003	}
1004
1005	#[rstest::rstest]
1006	#[case::empty("")]
1007	#[case::not_a_url("not a url")]
1008	#[case::http("http://localhost/db")]
1009	#[case::ftp("ftp://localhost/db")]
1010	#[case::redis("redis://localhost")]
1011	#[case::missing_scheme("localhost/db")]
1012	fn test_application_builder_rejects_invalid_database_url_scheme(#[case] url: &str) {
1013		// Arrange
1014		let db_config = ApplicationDatabaseConfig::new(url);
1015
1016		// Act
1017		// `Application` does not implement `Debug`, so we cannot use
1018		// `expect_err()` here (it would require `T: Debug`). Match instead.
1019		let result = ApplicationBuilder::new().database(db_config).build();
1020
1021		// Assert
1022		match result {
1023			Err(BuildError::DatabaseError(msg)) => {
1024				assert!(
1025					msg.contains("Invalid database URL"),
1026					"unexpected error message for {:?}: {}",
1027					url,
1028					msg
1029				);
1030			}
1031			Err(other) => panic!("expected BuildError::DatabaseError, got {:?}", other),
1032			Ok(_) => panic!("expected build to fail for invalid URL: {:?}", url),
1033		}
1034	}
1035
1036	#[test]
1037	#[serial(apps_registry)]
1038	fn test_application_builder_empty_settings() {
1039		// Arrange - Reset global state before test
1040		crate::registry::reset_global_registry();
1041
1042		let app = ApplicationBuilder::new().build().unwrap();
1043		assert!(app.settings().is_empty());
1044	}
1045}