1#![allow(dead_code)]
5pub use rrgen::{GenResult, RRgen};
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8mod controller;
9use colored::Colorize;
10use std::fmt::Write;
11use std::{
12 collections::HashMap,
13 fs,
14 path::{Path, PathBuf},
15 sync::OnceLock,
16};
17
18#[cfg(feature = "with-db")]
19mod infer;
20#[cfg(feature = "with-db")]
21mod migration;
22#[cfg(feature = "with-db")]
23mod model;
24#[cfg(feature = "with-db")]
25mod scaffold;
26pub mod template;
27pub mod tera_ext;
28#[cfg(test)]
29mod testutil;
30
31#[derive(Debug)]
32pub struct GenerateResults {
33 rrgen: Vec<rrgen::GenResult>,
34 local_templates: Vec<PathBuf>,
35}
36
37#[derive(thiserror::Error, Debug)]
38pub enum Error {
39 #[error("{0}")]
40 Message(String),
41 #[error("template {} not found", path.display())]
42 TemplateNotFound { path: PathBuf },
43 #[error(transparent)]
44 RRgen(#[from] rrgen::Error),
45 #[error(transparent)]
46 IO(#[from] std::io::Error),
47 #[error(transparent)]
48 Any(#[from] Box<dyn std::error::Error + Send + Sync>),
49}
50
51impl Error {
52 pub fn msg(err: impl std::error::Error + Send + Sync + 'static) -> Self {
53 Self::Message(err.to_string()) }
55}
56
57pub type Result<T> = std::result::Result<T, Error>;
58
59#[derive(Serialize, Deserialize, Debug)]
60struct FieldType {
61 name: String,
62 rust: RustType,
63 schema: String,
64 col_type: String,
65 #[serde(default)]
66 arity: usize,
67}
68
69#[derive(Debug, Deserialize, Serialize)]
70#[serde(untagged)]
71pub enum RustType {
72 String(String),
73 Map(HashMap<String, String>),
74}
75
76#[derive(Serialize, Deserialize, Debug)]
77pub struct Mappings {
78 field_types: Vec<FieldType>,
79}
80impl Mappings {
81 fn error_unrecognized_default_field(&self, field: &str) -> Error {
82 Self::error_unrecognized(field, &self.all_names())
83 }
84
85 fn error_unrecognized(field: &str, allow_fields: &[&String]) -> Error {
86 Error::Message(format!(
87 "type: `{}` not found. try any of: `{}`",
88 field,
89 allow_fields
90 .iter()
91 .map(|&s| s.clone())
92 .collect::<Vec<String>>()
93 .join(",")
94 ))
95 }
96
97 pub fn rust_field_with_params(&self, field: &str, params: &Vec<String>) -> Result<&str> {
103 match field {
104 "array" | "array^" | "array!" => {
105 if let RustType::Map(ref map) = self.rust_field_kind(field)? {
106 if let [single] = params.as_slice() {
107 let keys: Vec<&String> = map.keys().collect();
108 Ok(map
109 .get(single)
110 .ok_or_else(|| Self::error_unrecognized(field, &keys))?)
111 } else {
112 Err(self.error_unrecognized_default_field(field))
113 }
114 } else {
115 Err(Error::Message(
116 "array field should configured as array".to_owned(),
117 ))
118 }
119 }
120
121 _ => self.rust_field(field),
122 }
123 }
124
125 pub fn rust_field_kind(&self, field: &str) -> Result<&RustType> {
131 self.field_types
132 .iter()
133 .find(|f| f.name == field)
134 .map(|f| &f.rust)
135 .ok_or_else(|| self.error_unrecognized_default_field(field))
136 }
137
138 pub fn rust_field(&self, field: &str) -> Result<&str> {
144 self.field_types
145 .iter()
146 .find(|f| f.name == field)
147 .map(|f| &f.rust)
148 .ok_or_else(|| self.error_unrecognized_default_field(field))
149 .and_then(|rust_type| match rust_type {
150 RustType::String(s) => Ok(s),
151 RustType::Map(_) => Err(Error::Message(format!(
152 "type `{field}` need params to get the rust field type"
153 ))),
154 })
155 .map(std::string::String::as_str)
156 }
157
158 pub fn schema_field(&self, field: &str) -> Result<&str> {
164 self.field_types
165 .iter()
166 .find(|f| f.name == field)
167 .map(|f| f.schema.as_str())
168 .ok_or_else(|| self.error_unrecognized_default_field(field))
169 }
170
171 pub fn col_type_field(&self, field: &str) -> Result<&str> {
177 self.field_types
178 .iter()
179 .find(|f| f.name == field)
180 .map(|f| f.col_type.as_str())
181 .ok_or_else(|| self.error_unrecognized_default_field(field))
182 }
183
184 pub fn col_type_arity(&self, field: &str) -> Result<usize> {
190 self.field_types
191 .iter()
192 .find(|f| f.name == field)
193 .map(|f| f.arity)
194 .ok_or_else(|| self.error_unrecognized_default_field(field))
195 }
196
197 #[must_use]
198 pub fn all_names(&self) -> Vec<&String> {
199 self.field_types.iter().map(|f| &f.name).collect::<Vec<_>>()
200 }
201}
202
203static MAPPINGS: OnceLock<Mappings> = OnceLock::new();
204
205pub fn get_mappings() -> &'static Mappings {
211 MAPPINGS.get_or_init(|| {
212 let json_data = include_str!("./mappings.json");
213 serde_json::from_str(json_data).expect("JSON was not well-formatted")
214 })
215}
216
217#[derive(clap::ValueEnum, Clone, Debug)]
218pub enum ScaffoldKind {
219 Api,
220 Html,
221 Htmx,
222}
223
224#[derive(Debug, Clone)]
225pub enum DeploymentKind {
226 Docker {
227 copy_paths: Vec<PathBuf>,
228 is_client_side_rendering: bool,
229 },
230 Nginx {
231 host: String,
232 port: i32,
233 },
234}
235
236#[derive(Debug)]
237pub enum Component {
238 #[cfg(feature = "with-db")]
239 Model {
240 name: String,
242
243 with_tz: bool,
245
246 fields: Vec<(String, String)>,
248 },
249 #[cfg(feature = "with-db")]
250 Migration {
251 name: String,
253
254 with_tz: bool,
256
257 fields: Vec<(String, String)>,
259 },
260 #[cfg(feature = "with-db")]
261 Scaffold {
262 name: String,
264
265 with_tz: bool,
267
268 fields: Vec<(String, String)>,
270
271 kind: ScaffoldKind,
273 },
274 Controller {
275 name: String,
277
278 actions: Vec<String>,
280
281 kind: ScaffoldKind,
283 },
284 Task {
285 name: String,
287 },
288 Scheduler {},
289 Worker {
290 name: String,
292 },
293 Mailer {
294 name: String,
296 },
297 Data {
298 name: String,
300 },
301 Deployment {
302 kind: DeploymentKind,
303 },
304}
305
306pub struct AppInfo {
307 pub app_name: String,
308}
309
310#[must_use]
311pub fn new_generator() -> RRgen {
312 RRgen::default().add_template_engine(tera_ext::new())
313}
314
315pub fn generate(rrgen: &RRgen, component: Component, appinfo: &AppInfo) -> Result<GenerateResults> {
321 let get_result = match component {
329 #[cfg(feature = "with-db")]
330 Component::Model {
331 name,
332 with_tz,
333 fields,
334 } => model::generate(rrgen, &name, with_tz, &fields, appinfo)?,
335 #[cfg(feature = "with-db")]
336 Component::Scaffold {
337 name,
338 with_tz,
339 fields,
340 kind,
341 } => scaffold::generate(rrgen, &name, with_tz, &fields, &kind, appinfo)?,
342 #[cfg(feature = "with-db")]
343 Component::Migration {
344 name,
345 with_tz,
346 fields,
347 } => migration::generate(rrgen, &name, with_tz, &fields, appinfo)?,
348 Component::Controller {
349 name,
350 actions,
351 kind,
352 } => controller::generate(rrgen, &name, &actions, &kind, appinfo)?,
353 Component::Task { name } => {
354 let vars = json!({"name": name, "pkg_name": appinfo.app_name});
355 render_template(rrgen, Path::new("task"), &vars)?
356 }
357 Component::Scheduler {} => {
358 let vars = json!({"pkg_name": appinfo.app_name});
359 render_template(rrgen, Path::new("scheduler"), &vars)?
360 }
361 Component::Worker { name } => {
362 let vars = json!({"name": name, "pkg_name": appinfo.app_name});
363 render_template(rrgen, Path::new("worker"), &vars)?
364 }
365 Component::Mailer { name } => {
366 let vars = json!({ "name": name });
367 render_template(rrgen, Path::new("mailer"), &vars)?
368 }
369 Component::Deployment { kind } => match kind {
370 DeploymentKind::Docker {
371 copy_paths,
372 is_client_side_rendering,
373 } => {
374 let vars = json!({
375 "pkg_name": appinfo.app_name,
376 "copy_paths": copy_paths,
377 "is_client_side_rendering": is_client_side_rendering,
378 });
379 render_template(rrgen, Path::new("deployment/docker"), &vars)?
380 }
381 DeploymentKind::Nginx { host, port } => {
382 let host = host.replace("http://", "").replace("https://", "");
383 let vars = json!({
384 "pkg_name": appinfo.app_name,
385 "domain": host,
386 "port": port
387 });
388 render_template(rrgen, Path::new("deployment/nginx"), &vars)?
389 }
390 },
391 Component::Data { name } => {
392 let vars = json!({ "name": name });
393 render_template(rrgen, Path::new("data"), &vars)?
394 }
395 };
396
397 Ok(get_result)
398}
399
400fn render_template(rrgen: &RRgen, template: &Path, vars: &Value) -> Result<GenerateResults> {
401 let template_files = template::collect_files_from_path(template)?;
402
403 let mut gen_result = vec![];
404 let mut local_templates = vec![];
405 for template in template_files {
406 let custom_template = Path::new(template::DEFAULT_LOCAL_TEMPLATE).join(template.path());
407
408 if custom_template.exists() {
409 let content = fs::read_to_string(&custom_template).map_err(|err| {
410 tracing::error!(custom_template = %custom_template.display(), "could not read custom template");
411 err
412 })?;
413 gen_result.push(rrgen.generate(&content, vars)?);
414 local_templates.push(custom_template);
415 } else {
416 let content = template.contents_utf8().ok_or(Error::Message(format!(
417 "could not get template content: {}",
418 template.path().display()
419 )))?;
420 gen_result.push(rrgen.generate(content, vars)?);
421 }
422 }
423
424 Ok(GenerateResults {
425 rrgen: gen_result,
426 local_templates,
427 })
428}
429
430#[must_use]
431pub fn collect_messages(results: &GenerateResults) -> String {
432 let mut messages = String::new();
433
434 for res in &results.rrgen {
435 if let rrgen::GenResult::Generated {
436 message: Some(message),
437 } = res
438 {
439 let _ = writeln!(messages, "* {message}");
440 }
441 }
442
443 if !results.local_templates.is_empty() {
444 let _ = writeln!(messages);
445 let _ = writeln!(
446 messages,
447 "{}",
448 "The following templates were sourced from the local templates:".green()
449 );
450
451 for f in &results.local_templates {
452 let _ = writeln!(messages, "* {}", f.display());
453 }
454 }
455 messages
456}
457
458pub fn copy_template(path: &Path, to: &Path) -> Result<Vec<PathBuf>> {
468 let copy_template_path = if path == Path::new("/") || path == Path::new(".") {
469 None
470 } else if !template::exists(path) {
471 return Err(Error::TemplateNotFound {
472 path: path.to_path_buf(),
473 });
474 } else {
475 Some(path)
476 };
477
478 let copy_files = if let Some(path) = copy_template_path {
479 template::collect_files_from_path(path)?
480 } else {
481 template::collect_files()
482 };
483
484 let mut copied_files = vec![];
485 for f in copy_files {
486 let copy_to = to.join(f.path());
487 if copy_to.exists() {
488 tracing::debug!(
489 template_file = %copy_to.display(),
490 "skipping copy template file. already exists"
491 );
492 continue;
493 }
494 match copy_to.parent() {
495 Some(parent) => {
496 fs::create_dir_all(parent)?;
497 }
498 None => {
499 return Err(Error::Message(format!(
500 "could not get parent folder of {}",
501 copy_to.display()
502 )))
503 }
504 }
505
506 fs::write(©_to, f.contents())?;
507 tracing::trace!(
508 template = %copy_to.display(),
509 "copy template successfully"
510 );
511 copied_files.push(copy_to);
512 }
513 Ok(copied_files)
514}
515
516#[cfg(test)]
517mod tests {
518 use std::path::Path;
519
520 use super::*;
521
522 #[test]
523 fn test_template_not_found() {
524 let tree_fs = tree_fs::TreeBuilder::default()
525 .drop(true)
526 .create()
527 .expect("create temp file");
528 let path = Path::new("nonexistent-template");
529
530 let result = copy_template(path, tree_fs.root.as_path());
531 assert!(result.is_err());
532 if let Err(Error::TemplateNotFound { path: p }) = result {
533 assert_eq!(p, path.to_path_buf());
534 } else {
535 panic!("Expected TemplateNotFound error");
536 }
537 }
538
539 #[test]
540 fn test_copy_template_valid_folder_template() {
541 let temp_fs = tree_fs::TreeBuilder::default()
542 .drop(true)
543 .create()
544 .expect("Failed to create temporary file system");
545
546 let template_dir = template::tests::find_first_dir();
547
548 let copy_result = copy_template(template_dir.path(), temp_fs.root.as_path());
549 assert!(
550 copy_result.is_ok(),
551 "Failed to copy template from directory {:?}",
552 template_dir.path()
553 );
554
555 let template_files = template::collect_files_from_path(template_dir.path())
556 .expect("Failed to collect files from the template directory");
557
558 assert!(
559 !template_files.is_empty(),
560 "No files found in the template directory"
561 );
562
563 for template_file in template_files {
564 let copy_file_path = temp_fs.root.join(template_file.path());
565
566 assert!(
567 copy_file_path.exists(),
568 "Copy file does not exist: {copy_file_path:?}"
569 );
570
571 let copy_content =
572 fs::read_to_string(©_file_path).expect("Failed to read coped file content");
573
574 assert_eq!(
575 template_file
576 .contents_utf8()
577 .expect("Failed to get template file content"),
578 copy_content,
579 "Content mismatch in file: {copy_file_path:?}"
580 );
581 }
582 }
583
584 fn test_mapping() -> Mappings {
585 Mappings {
586 field_types: vec![
587 FieldType {
588 name: "array".to_string(),
589 rust: RustType::Map(HashMap::from([
590 ("string".to_string(), "Vec<String>".to_string()),
591 ("chat".to_string(), "Vec<String>".to_string()),
592 ("int".to_string(), "Vec<i32>".to_string()),
593 ])),
594 schema: "array".to_string(),
595 col_type: "array_null".to_string(),
596 arity: 1,
597 },
598 FieldType {
599 name: "string^".to_string(),
600 rust: RustType::String("String".to_string()),
601 schema: "string_uniq".to_string(),
602 col_type: "StringUniq".to_string(),
603 arity: 0,
604 },
605 ],
606 }
607 }
608
609 #[test]
610 fn can_get_all_names_from_mapping() {
611 let mapping = test_mapping();
612 assert_eq!(
613 mapping.all_names(),
614 Vec::from([&"array".to_string(), &"string^".to_string()])
615 );
616 }
617
618 #[test]
619 fn can_get_col_type_arity_from_mapping() {
620 let mapping = test_mapping();
621
622 assert_eq!(mapping.col_type_arity("array").expect("Get array arity"), 1);
623 assert_eq!(
624 mapping
625 .col_type_arity("string^")
626 .expect("Get string^ arity"),
627 0
628 );
629
630 assert!(mapping.col_type_arity("unknown").is_err());
631 }
632
633 #[test]
634 fn can_get_col_type_field_from_mapping() {
635 let mapping = test_mapping();
636
637 assert_eq!(
638 mapping.col_type_field("array").expect("Get array field"),
639 "array_null"
640 );
641
642 assert!(mapping.col_type_field("unknown").is_err());
643 }
644
645 #[test]
646 fn can_get_schema_field_from_mapping() {
647 let mapping = test_mapping();
648
649 assert_eq!(
650 mapping.schema_field("string^").expect("Get string^ schema"),
651 "string_uniq"
652 );
653
654 assert!(mapping.schema_field("unknown").is_err());
655 }
656
657 #[test]
658 fn can_get_rust_field_from_mapping() {
659 let mapping = test_mapping();
660
661 assert_eq!(
662 mapping
663 .rust_field("string^")
664 .expect("Get string^ rust field"),
665 "String"
666 );
667
668 assert!(mapping.rust_field("array").is_err());
669
670 assert!(mapping.rust_field("unknown").is_err(),);
671 }
672
673 #[test]
674 fn can_get_rust_field_kind_from_mapping() {
675 let mapping = test_mapping();
676
677 assert!(mapping.rust_field_kind("string^").is_ok());
678
679 assert!(mapping.rust_field_kind("unknown").is_err(),);
680 }
681
682 #[test]
683 fn can_get_rust_field_with_params_from_mapping() {
684 let mapping = test_mapping();
685
686 assert_eq!(
687 mapping
688 .rust_field_with_params("string^", &vec!["string".to_string()])
689 .expect("Get string^ rust field"),
690 "String"
691 );
692
693 assert_eq!(
694 mapping
695 .rust_field_with_params("array", &vec!["string".to_string()])
696 .expect("Get string^ rust field"),
697 "Vec<String>"
698 );
699 assert!(mapping
700 .rust_field_with_params("array", &vec!["unknown".to_string()])
701 .is_err());
702
703 assert!(mapping.rust_field_with_params("unknown", &vec![]).is_err());
704 }
705
706 #[test]
707 fn can_collect_messages() {
708 let gen_result = GenerateResults {
709 rrgen: vec![
710 GenResult::Skipped,
711 GenResult::Generated {
712 message: Some("test".to_string()),
713 },
714 GenResult::Generated {
715 message: Some("test2".to_string()),
716 },
717 GenResult::Generated { message: None },
718 ],
719 local_templates: vec![
720 PathBuf::from("template").join("scheduler.t"),
721 PathBuf::from("template").join("task.t"),
722 ],
723 };
724
725 let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap();
726
727 assert_eq!(
728 re.replace_all(&collect_messages(&gen_result), ""),
729 r"* test
730* test2
731
732The following templates were sourced from the local templates:
733* template/scheduler.t
734* template/task.t
735"
736 );
737 }
738}