1use std::collections::HashMap;
2use std::path::Path;
3
4use serde::Deserialize;
5
6use crate::GeneratorError;
7
8#[derive(Debug, Clone)]
10pub struct TemplatePack {
11 pub manifest: PackManifest,
12 pub templates: HashMap<String, String>,
14}
15
16#[derive(Debug, Clone, Deserialize)]
18pub struct PackManifest {
19 pub pack: PackMeta,
20 #[serde(default)]
21 pub type_map: TypeMapConfig,
22 #[serde(default)]
23 pub filters: HashMap<String, FilterConfig>,
24 #[serde(default)]
25 pub layouts: LayoutsConfig,
26 #[serde(default)]
27 pub scaffold: ScaffoldConfig,
28 #[serde(default)]
29 pub formatters: HashMap<String, FormatterConfig>,
30 #[serde(default)]
31 pub validators: HashMap<String, ValidatorConfig>,
32}
33
34#[derive(Debug, Clone, Deserialize)]
35pub struct PackMeta {
36 pub id: String,
37 #[serde(default)]
38 pub name: String,
39 #[serde(default)]
40 pub version: String,
41 #[serde(default)]
42 pub description: String,
43 pub extends: Option<String>,
45 #[serde(default = "default_camel")]
47 pub field_casing: String,
48 #[serde(default = "default_camel")]
50 pub operation_casing: String,
51}
52
53fn default_camel() -> String {
54 "camel".to_string()
55}
56
57#[derive(Debug, Clone, Default, Deserialize)]
59#[serde(default)]
60pub struct TypeMapConfig {
61 pub string: String,
63 pub number: String,
64 pub integer: String,
65 pub boolean: String,
66 pub null: String,
67 pub datetime: String,
68 pub binary: String,
69 pub any: String,
70 pub void: String,
71
72 pub string_literal: String,
74 pub integer_literal: String,
75 #[serde(rename = "ref")]
76 pub ref_type: String,
77 pub array: String,
78 pub array_union: Option<String>,
80 pub map: String,
81 pub object: String,
82 pub object_empty: String,
83 pub object_field_required: String,
84 pub object_field_optional: String,
85 #[serde(default = "default_semicolon_sep")]
86 pub object_field_separator: String,
87 #[serde(default = "default_pipe_sep")]
88 pub union_separator: String,
89 #[serde(default = "default_ampersand_sep")]
90 pub intersection_separator: String,
91
92 #[serde(default)]
94 pub optional_suffix: String,
95}
96
97fn default_semicolon_sep() -> String {
98 "; ".to_string()
99}
100fn default_pipe_sep() -> String {
101 " | ".to_string()
102}
103fn default_ampersand_sep() -> String {
104 " & ".to_string()
105}
106
107#[derive(Debug, Clone, Deserialize)]
108pub struct FilterConfig {
109 pub replace: String,
110 pub with: String,
111}
112
113#[derive(Debug, Clone, Default, Deserialize)]
114#[serde(default)]
115pub struct LayoutsConfig {
116 pub modular: Option<ModularLayout>,
117 pub bundled: Option<BundledLayout>,
118 pub split: Option<SplitLayout>,
119}
120
121#[derive(Debug, Clone, Deserialize)]
122pub struct ModularLayout {
123 #[serde(default)]
124 pub files: Vec<LayoutFile>,
125 #[serde(default)]
127 pub extra_files: Vec<LayoutFile>,
128}
129
130#[derive(Debug, Clone, Deserialize)]
131pub struct LayoutFile {
132 pub path: String,
133 pub template: String,
134 pub when: Option<String>,
136}
137
138#[derive(Debug, Clone, Deserialize)]
139pub struct BundledLayout {
140 pub output_path: String,
141 #[serde(default)]
142 pub sections: Vec<BundledSection>,
143 #[serde(default)]
144 pub strip_patterns: Vec<String>,
145 #[serde(default)]
146 pub strip_import_patterns: Vec<String>,
147}
148
149#[derive(Debug, Clone, Deserialize)]
150pub struct BundledSection {
151 pub label: String,
152 pub template: String,
153}
154
155#[derive(Debug, Clone, Deserialize)]
156pub struct SplitLayout {
157 #[serde(default)]
158 pub shared_files: Vec<LayoutFile>,
159 pub group_template: Option<String>,
160 pub index_template: Option<String>,
161}
162
163#[derive(Debug, Clone, Default, Deserialize)]
164#[serde(default)]
165pub struct ScaffoldConfig {
166 #[serde(default)]
167 pub files: Vec<LayoutFile>,
168 #[serde(default)]
169 pub test_files: Vec<LayoutFile>,
170}
171
172#[derive(Debug, Clone, Deserialize)]
173pub struct FormatterConfig {
174 pub detect: String,
175 pub command: String,
176}
177
178#[derive(Debug, Clone, Deserialize)]
179pub struct ValidatorConfig {
180 pub detect: String,
181 pub command: String,
182 pub setup: Option<String>,
184}
185
186impl TemplatePack {
187 pub fn from_dir(dir: &Path) -> Result<Self, GeneratorError> {
189 let manifest_path = dir.join("oag.pack.toml");
190 let manifest_str = std::fs::read_to_string(&manifest_path).map_err(|e| {
191 GeneratorError::Other(format!("failed to read {}: {e}", manifest_path.display()))
192 })?;
193 let manifest: PackManifest = toml::from_str(&manifest_str).map_err(|e| {
194 GeneratorError::Other(format!("failed to parse {}: {e}", manifest_path.display()))
195 })?;
196
197 let templates_dir = dir.join("templates");
198 let mut templates = HashMap::new();
199 if templates_dir.is_dir() {
200 for entry in std::fs::read_dir(&templates_dir).map_err(|e| {
201 GeneratorError::Other(format!("failed to read {}: {e}", templates_dir.display()))
202 })? {
203 let entry = entry.map_err(|e| GeneratorError::Other(e.to_string()))?;
204 let path = entry.path();
205 if path.is_file() {
206 let name = path
207 .file_name()
208 .unwrap_or_default()
209 .to_string_lossy()
210 .to_string();
211 let content = std::fs::read_to_string(&path).map_err(|e| {
212 GeneratorError::Other(format!("failed to read {}: {e}", path.display()))
213 })?;
214 templates.insert(name, content);
215 }
216 }
217 }
218
219 Ok(Self {
220 manifest,
221 templates,
222 })
223 }
224
225 pub fn merge_from(&mut self, extending: &TemplatePack) {
228 for (name, content) in &extending.templates {
230 self.templates.insert(name.clone(), content.clone());
231 }
232
233 if !extending.manifest.type_map.string.is_empty() {
236 self.manifest.type_map = extending.manifest.type_map.clone();
237 }
238
239 for (name, filter) in &extending.manifest.filters {
241 self.manifest.filters.insert(name.clone(), filter.clone());
242 }
243
244 if let Some(ref ext_modular) = extending.manifest.layouts.modular {
246 if let Some(ref mut base_modular) = self.manifest.layouts.modular {
247 base_modular.files.extend(ext_modular.extra_files.clone());
248 base_modular.files.extend(ext_modular.files.clone());
250 } else {
251 self.manifest.layouts.modular = Some(ext_modular.clone());
252 }
253 }
254
255 if extending.manifest.layouts.bundled.is_some() {
257 self.manifest.layouts.bundled = extending.manifest.layouts.bundled.clone();
258 }
259 if extending.manifest.layouts.split.is_some() {
260 self.manifest.layouts.split = extending.manifest.layouts.split.clone();
261 }
262
263 if !extending.manifest.scaffold.files.is_empty() {
265 self.manifest.scaffold = extending.manifest.scaffold.clone();
266 }
267
268 for (name, fmt) in &extending.manifest.formatters {
270 self.manifest.formatters.insert(name.clone(), fmt.clone());
271 }
272
273 for (name, val) in &extending.manifest.validators {
275 self.manifest.validators.insert(name.clone(), val.clone());
276 }
277
278 self.manifest.pack = extending.manifest.pack.clone();
280 }
281}