doido_generators/
project_generator.rs1use crate::generator::GeneratedFile;
9use crate::generators::{to_pascal, to_snake, to_table_name};
10use doido_core::{anyhow::anyhow, Result};
11use std::path::{Path, PathBuf};
12
13pub fn generators_root() -> PathBuf {
15 PathBuf::from("lib/generators")
16}
17
18pub fn find(name: &str) -> Option<PathBuf> {
20 find_in(&generators_root(), name)
21}
22
23pub fn find_in(root: &Path, name: &str) -> Option<PathBuf> {
25 let dir = root.join(name);
26 dir.is_dir().then_some(dir)
27}
28
29pub fn list() -> Vec<String> {
31 list_in(&generators_root())
32}
33
34pub fn list_in(root: &Path) -> Vec<String> {
36 let mut names: Vec<String> = match std::fs::read_dir(root) {
37 Ok(entries) => entries
38 .flatten()
39 .filter(|e| e.path().is_dir())
40 .filter_map(|e| e.file_name().into_string().ok())
41 .collect(),
42 Err(_) => Vec::new(),
43 };
44 names.sort();
45 names
46}
47
48struct Tokens {
51 name: String,
52 snake: String,
53 pascal: String,
54 plural: String,
55 controller: String,
56}
57
58impl Tokens {
59 fn from_name(name: &str) -> Self {
60 let pascal = to_pascal(name);
61 let plural = to_table_name(name);
62 Self {
63 name: name.to_string(),
64 snake: to_snake(name),
65 controller: format!("{}Controller", to_pascal(&plural)),
66 plural,
67 pascal,
68 }
69 }
70
71 fn apply(&self, s: &str) -> String {
72 s.replace("{name}", &self.name)
73 .replace("{snake}", &self.snake)
74 .replace("{singular}", &self.snake)
75 .replace("{pascal}", &self.pascal)
76 .replace("{Model}", &self.pascal)
77 .replace("{plural}", &self.plural)
78 .replace("{Controller}", &self.controller)
79 }
80}
81
82pub fn run(dir: &Path, args: &[&str]) -> Result<Vec<GeneratedFile>> {
85 let name = args
86 .first()
87 .copied()
88 .ok_or_else(|| anyhow!("this generator requires a name argument"))?;
89 let tokens = Tokens::from_name(name);
90 let mut files = Vec::new();
91 collect(dir, dir, &tokens, &mut files)?;
92 if files.is_empty() {
93 return Err(anyhow!(
94 "generator at {} has no template files",
95 dir.display()
96 ));
97 }
98 Ok(files)
99}
100
101fn collect(base: &Path, dir: &Path, tokens: &Tokens, out: &mut Vec<GeneratedFile>) -> Result<()> {
103 let mut entries: Vec<PathBuf> = std::fs::read_dir(dir)
104 .map_err(|e| anyhow!("reading {}: {e}", dir.display()))?
105 .filter_map(|e| e.ok().map(|e| e.path()))
106 .collect();
107 entries.sort();
108
109 for path in entries {
110 let file_name = path
111 .file_name()
112 .map(|n| n.to_string_lossy().to_string())
113 .unwrap_or_default();
114 if file_name.starts_with('.') {
115 continue;
116 }
117 if path.is_dir() {
118 collect(base, &path, tokens, out)?;
119 } else {
120 if file_name.eq_ignore_ascii_case("README.md") {
121 continue;
122 }
123 let rel = path
124 .strip_prefix(base)
125 .unwrap_or(&path)
126 .to_string_lossy()
127 .replace('\\', "/");
128 let out_path = strip_template_ext(&tokens.apply(&rel));
129 let content = std::fs::read_to_string(&path)
130 .map_err(|e| anyhow!("reading {}: {e}", path.display()))?;
131 out.push(GeneratedFile {
132 path: out_path,
133 content: tokens.apply(&content),
134 });
135 }
136 }
137 Ok(())
138}
139
140fn strip_template_ext(p: &str) -> String {
141 p.strip_suffix(".template")
142 .map(str::to_string)
143 .unwrap_or_else(|| p.to_string())
144}