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