1use crate::apps::{AppConfig, AppError, Apps};
7use std::collections::HashMap;
8use std::sync::Arc;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13pub enum BuildError {
14 #[error("Application error: {0}")]
16 App(#[from] AppError),
17
18 #[error("Invalid configuration: {0}")]
20 InvalidConfig(String),
21
22 #[error("Missing required configuration: {0}")]
24 MissingConfig(String),
25
26 #[error("Route configuration error: {0}")]
28 RouteError(String),
29
30 #[error("Database configuration error: {0}")]
32 DatabaseError(String),
33}
34
35pub type BuildResult<T> = Result<T, BuildError>;
37
38#[derive(Clone)]
41pub struct RouteConfig {
42 pub path: String,
44 pub handler_name: String,
46 pub name: Option<String>,
48 pub namespace: Option<String>,
50}
51
52impl RouteConfig {
53 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 pub fn with_name(mut self, name: impl Into<String>) -> Self {
85 self.name = Some(name.into());
86 self
87 }
88
89 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
101 self.namespace = Some(namespace.into());
102 self
103 }
104
105 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#[derive(Clone, Debug)]
135pub struct ApplicationDatabaseConfig {
136 pub url: String,
138 pub pool_size: Option<u32>,
140 pub max_overflow: Option<u32>,
142 pub timeout: Option<u64>,
144}
145
146impl ApplicationDatabaseConfig {
147 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 pub fn with_pool_size(mut self, size: u32) -> Self {
190 self.pool_size = Some(size);
191 self
192 }
193
194 pub fn with_max_overflow(mut self, overflow: u32) -> Self {
206 self.max_overflow = Some(overflow);
207 self
208 }
209
210 pub fn with_timeout(mut self, timeout: u64) -> Self {
222 self.timeout = Some(timeout);
223 self
224 }
225}
226
227pub 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 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 pub fn add_app(mut self, app: AppConfig) -> Self {
274 self.apps.push(app);
275 self
276 }
277
278 pub fn add_apps(mut self, apps: Vec<AppConfig>) -> Self {
296 self.apps.extend(apps);
297 self
298 }
299
300 pub fn add_middleware(mut self, middleware: impl Into<String>) -> Self {
313 self.middleware.push(middleware.into());
314 self
315 }
316
317 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 pub fn add_url_pattern(mut self, pattern: RouteConfig) -> Self {
350 self.url_patterns.push(pattern);
351 self
352 }
353
354 pub fn add_url_patterns(mut self, patterns: Vec<RouteConfig>) -> Self {
371 self.url_patterns.extend(patterns);
372 self
373 }
374
375 pub fn database(mut self, config: ApplicationDatabaseConfig) -> Self {
389 self.database_config = Some(config);
390 self
391 }
392
393 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 pub fn add_settings(mut self, settings: HashMap<String, String>) -> Self {
428 self.settings.extend(settings);
429 self
430 }
431
432 fn validate(&self) -> BuildResult<()> {
434 for app in &self.apps {
436 app.validate_label()?;
437 }
438
439 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 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 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 pub fn build(self) -> BuildResult<Application> {
492 self.validate()?;
494
495 let installed_apps: Vec<String> = self.apps.iter().map(|app| app.name.clone()).collect();
497 let apps_registry = Apps::new(installed_apps);
498
499 for app in &self.apps {
501 apps_registry.register(app.clone())?;
502 }
503
504 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 #[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 singleton_scope.set(app.clone());
547
548 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
561pub 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 pub fn apps(&self) -> &[AppConfig] {
589 &self.apps
590 }
591
592 pub fn middleware(&self) -> &[String] {
608 &self.middleware
609 }
610
611 pub fn url_patterns(&self) -> &[RouteConfig] {
627 &self.url_patterns
628 }
629
630 pub fn database_config(&self) -> Option<&ApplicationDatabaseConfig> {
646 self.database_config.as_ref()
647 }
648
649 pub fn settings(&self) -> &HashMap<String, String> {
663 &self.settings
664 }
665
666 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 crate::registry::reset_global_registry();
991 let db_config = ApplicationDatabaseConfig::new(url);
992
993 let result = ApplicationBuilder::new().database(db_config).build();
995
996 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 let db_config = ApplicationDatabaseConfig::new(url);
1015
1016 let result = ApplicationBuilder::new().database(db_config).build();
1020
1021 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 crate::registry::reset_global_registry();
1041
1042 let app = ApplicationBuilder::new().build().unwrap();
1043 assert!(app.settings().is_empty());
1044 }
1045}