1use crate::config::RomanceConfig;
2use crate::entity::{FieldType, ValidationRule};
3use crate::utils;
4use anyhow::Result;
5use std::path::Path;
6
7pub mod markers {
9 pub const MODS: &str = "// === ROMANCE:MODS ===";
10 pub const ROUTES: &str = "// === ROMANCE:ROUTES ===";
11 pub const MIGRATION_MODS: &str = "// === ROMANCE:MIGRATION_MODS ===";
12 pub const MIGRATIONS: &str = "// === ROMANCE:MIGRATIONS ===";
13 pub const RELATIONS: &str = "// === ROMANCE:RELATIONS ===";
14 pub const RELATION_HANDLERS: &str = "// === ROMANCE:RELATION_HANDLERS ===";
15 pub const RELATION_ROUTES: &str = "// === ROMANCE:RELATION_ROUTES ===";
16 pub const MIDDLEWARE: &str = "// === ROMANCE:MIDDLEWARE ===";
17 pub const IMPORTS: &str = "// === ROMANCE:IMPORTS ===";
18 pub const APP_ROUTES: &str = "{/* === ROMANCE:APP_ROUTES === */}";
19 pub const NAV_LINKS: &str = "{/* === ROMANCE:NAV_LINKS === */}";
20 pub const OPENAPI_PATHS: &str = "// === ROMANCE:OPENAPI_PATHS ===";
21 pub const OPENAPI_SCHEMAS: &str = "// === ROMANCE:OPENAPI_SCHEMAS ===";
22 pub const OPENAPI_TAGS: &str = "// === ROMANCE:OPENAPI_TAGS ===";
23 pub const SEEDS: &str = "// === ROMANCE:SEEDS ===";
24 pub const CUSTOM: &str = "// === ROMANCE:CUSTOM ===";
25}
26
27pub struct ProjectFeatures {
29 pub soft_delete: bool,
30 pub has_validation: bool,
31 pub has_search: bool,
32 pub has_audit: bool,
33 pub has_multitenancy: bool,
34 pub has_auth: bool,
35 pub api_prefix: String,
36}
37
38impl ProjectFeatures {
39 pub fn load(project_root: &Path) -> Self {
42 let config = RomanceConfig::load(project_root).ok();
43 let soft_delete = config.as_ref().map(|c| c.has_feature("soft_delete")).unwrap_or(false);
44 let has_validation = config.as_ref().map(|c| c.has_feature("validation")).unwrap_or(false);
45 let has_search = config.as_ref().map(|c| c.has_feature("search")).unwrap_or(false);
46 let has_audit = config.as_ref().map(|c| c.has_feature("audit_log")).unwrap_or(false);
47 let has_multitenancy = config.as_ref().map(|c| c.has_feature("multitenancy")).unwrap_or(false);
48 let has_auth = project_root.join("backend/src/auth.rs").exists();
49 let api_prefix = config.as_ref()
50 .and_then(|c| c.backend.api_prefix.clone())
51 .unwrap_or_else(|| "/api".to_string());
52
53 Self {
54 soft_delete,
55 has_validation,
56 has_search,
57 has_audit,
58 has_multitenancy,
59 has_auth,
60 api_prefix,
61 }
62 }
63}
64
65pub fn validation_rules_to_json(rules: &[ValidationRule]) -> Vec<serde_json::Value> {
67 rules
68 .iter()
69 .map(|v| match v {
70 ValidationRule::Min(n) => serde_json::json!({"type": "min", "value": n}),
71 ValidationRule::Max(n) => serde_json::json!({"type": "max", "value": n}),
72 ValidationRule::Email => serde_json::json!({"type": "email"}),
73 ValidationRule::Url => serde_json::json!({"type": "url"}),
74 ValidationRule::Regex(r) => serde_json::json!({"type": "regex", "value": r}),
75 ValidationRule::Required => serde_json::json!({"type": "required"}),
76 ValidationRule::Unique => serde_json::json!({"type": "unique"}),
77 })
78 .collect()
79}
80
81pub fn filter_method(field_type: &FieldType) -> &'static str {
87 match field_type {
88 FieldType::String | FieldType::Text | FieldType::Enum(_) => "contains",
89 FieldType::Bool
90 | FieldType::Int32
91 | FieldType::Int64
92 | FieldType::Float64
93 | FieldType::Decimal
94 | FieldType::Uuid
95 | FieldType::DateTime
96 | FieldType::Date => "eq",
97 FieldType::Json | FieldType::File | FieldType::Image => "skip",
98 }
99}
100
101pub fn is_numeric(field_type: &FieldType) -> bool {
103 matches!(
104 field_type,
105 FieldType::Int32 | FieldType::Int64 | FieldType::Float64 | FieldType::Decimal
106 )
107}
108
109pub fn register_backend_module(backend_src: &Path, module_name: &str) -> Result<()> {
112 let routes_mod = backend_src.join("routes/mod.rs");
113 utils::insert_at_marker(
114 &routes_mod,
115 markers::ROUTES,
116 &format!(" .merge({module_name}::router())"),
117 )?;
118 utils::insert_at_marker(
119 &routes_mod,
120 markers::MODS,
121 &format!("pub mod {};", module_name),
122 )?;
123
124 let entities_mod = backend_src.join("entities/mod.rs");
125 utils::insert_at_marker(
126 &entities_mod,
127 markers::MODS,
128 &format!("pub mod {};", module_name),
129 )?;
130
131 let handlers_mod = backend_src.join("handlers/mod.rs");
132 utils::insert_at_marker(
133 &handlers_mod,
134 markers::MODS,
135 &format!("pub mod {};", module_name),
136 )?;
137
138 Ok(())
139}
140
141pub fn register_migration(project_root: &Path, migration_module: &str) -> Result<()> {
143 let lib_path = project_root.join("backend/migration/src/lib.rs");
144 utils::insert_at_marker(
145 &lib_path,
146 markers::MIGRATION_MODS,
147 &format!("mod {};", migration_module),
148 )?;
149 utils::insert_at_marker(
150 &lib_path,
151 markers::MIGRATIONS,
152 &format!(" Box::new({}::Migration),", migration_module),
153 )?;
154 Ok(())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use std::io::Write;
161
162 #[test]
163 fn validation_rules_to_json_empty() {
164 let json = validation_rules_to_json(&[]);
165 assert!(json.is_empty());
166 }
167
168 #[test]
169 fn validation_rules_to_json_all_types() {
170 let rules = vec![
171 ValidationRule::Min(3),
172 ValidationRule::Max(100),
173 ValidationRule::Email,
174 ValidationRule::Url,
175 ValidationRule::Regex("^[a-z]+$".to_string()),
176 ValidationRule::Required,
177 ValidationRule::Unique,
178 ];
179 let json = validation_rules_to_json(&rules);
180 assert_eq!(json.len(), 7);
181 assert_eq!(json[0]["type"], "min");
182 assert_eq!(json[0]["value"], 3);
183 assert_eq!(json[1]["type"], "max");
184 assert_eq!(json[1]["value"], 100);
185 assert_eq!(json[2]["type"], "email");
186 assert_eq!(json[3]["type"], "url");
187 assert_eq!(json[4]["type"], "regex");
188 assert_eq!(json[4]["value"], "^[a-z]+$");
189 assert_eq!(json[5]["type"], "required");
190 assert_eq!(json[6]["type"], "unique");
191 }
192
193 #[test]
194 fn filter_method_contains_for_strings() {
195 assert_eq!(filter_method(&FieldType::String), "contains");
196 assert_eq!(filter_method(&FieldType::Text), "contains");
197 assert_eq!(filter_method(&FieldType::Enum(vec!["A".into()])), "contains");
198 }
199
200 #[test]
201 fn filter_method_eq_for_exact_types() {
202 assert_eq!(filter_method(&FieldType::Bool), "eq");
203 assert_eq!(filter_method(&FieldType::Int32), "eq");
204 assert_eq!(filter_method(&FieldType::Uuid), "eq");
205 assert_eq!(filter_method(&FieldType::DateTime), "eq");
206 }
207
208 #[test]
209 fn filter_method_skip_for_complex_types() {
210 assert_eq!(filter_method(&FieldType::Json), "skip");
211 assert_eq!(filter_method(&FieldType::File), "skip");
212 assert_eq!(filter_method(&FieldType::Image), "skip");
213 }
214
215 #[test]
216 fn is_numeric_correct() {
217 assert!(is_numeric(&FieldType::Int32));
218 assert!(is_numeric(&FieldType::Int64));
219 assert!(is_numeric(&FieldType::Float64));
220 assert!(is_numeric(&FieldType::Decimal));
221 assert!(!is_numeric(&FieldType::String));
222 assert!(!is_numeric(&FieldType::Bool));
223 assert!(!is_numeric(&FieldType::Uuid));
224 }
225
226 #[test]
227 fn register_backend_module_inserts_mods_and_routes() {
228 let dir = tempfile::tempdir().unwrap();
229 let base = dir.path();
230
231 std::fs::create_dir_all(base.join("routes")).unwrap();
233 std::fs::create_dir_all(base.join("entities")).unwrap();
234 std::fs::create_dir_all(base.join("handlers")).unwrap();
235
236 let mut f = std::fs::File::create(base.join("routes/mod.rs")).unwrap();
238 writeln!(f, "// === ROMANCE:MODS ===").unwrap();
239 writeln!(f, "// === ROMANCE:ROUTES ===").unwrap();
240
241 let mut f = std::fs::File::create(base.join("entities/mod.rs")).unwrap();
242 writeln!(f, "// === ROMANCE:MODS ===").unwrap();
243
244 let mut f = std::fs::File::create(base.join("handlers/mod.rs")).unwrap();
245 writeln!(f, "// === ROMANCE:MODS ===").unwrap();
246
247 register_backend_module(base, "product").unwrap();
248
249 let routes = std::fs::read_to_string(base.join("routes/mod.rs")).unwrap();
250 assert!(routes.contains("pub mod product;"));
251 assert!(routes.contains(".merge(product::router())"));
252
253 let entities = std::fs::read_to_string(base.join("entities/mod.rs")).unwrap();
254 assert!(entities.contains("pub mod product;"));
255
256 let handlers = std::fs::read_to_string(base.join("handlers/mod.rs")).unwrap();
257 assert!(handlers.contains("pub mod product;"));
258 }
259
260 #[test]
261 fn register_backend_module_errors_on_missing_marker() {
262 let dir = tempfile::tempdir().unwrap();
263 let base = dir.path();
264
265 std::fs::create_dir_all(base.join("routes")).unwrap();
266 std::fs::create_dir_all(base.join("entities")).unwrap();
267 std::fs::create_dir_all(base.join("handlers")).unwrap();
268
269 std::fs::write(base.join("routes/mod.rs"), "// no markers here\n").unwrap();
271 std::fs::write(base.join("entities/mod.rs"), "// === ROMANCE:MODS ===\n").unwrap();
272 std::fs::write(base.join("handlers/mod.rs"), "// === ROMANCE:MODS ===\n").unwrap();
273
274 let result = register_backend_module(base, "product");
275 assert!(result.is_err());
276 }
277
278 #[test]
279 fn register_migration_inserts_mod_and_box() {
280 let dir = tempfile::tempdir().unwrap();
281 std::fs::create_dir_all(dir.path().join("backend/migration/src")).unwrap();
282
283 let lib_content = r#"pub use sea_orm_migration::prelude::*;
284
285// === ROMANCE:MIGRATION_MODS ===
286
287pub struct Migrator;
288
289impl MigratorTrait for Migrator {
290 fn migrations() -> Vec<Box<dyn MigrationTrait>> {
291 vec![
292 // === ROMANCE:MIGRATIONS ===
293 ]
294 }
295}
296"#;
297 std::fs::write(
298 dir.path().join("backend/migration/src/lib.rs"),
299 lib_content,
300 )
301 .unwrap();
302
303 register_migration(dir.path(), "m20260216_create_product_table").unwrap();
304
305 let content =
306 std::fs::read_to_string(dir.path().join("backend/migration/src/lib.rs")).unwrap();
307 assert!(content.contains("mod m20260216_create_product_table;"));
308 assert!(content.contains("Box::new(m20260216_create_product_table::Migration),"));
309 }
310}