Skip to main content

oag_core/engine/
pack.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use serde::Deserialize;
5
6use crate::GeneratorError;
7
8/// A loaded template pack ready for code generation.
9#[derive(Debug, Clone)]
10pub struct TemplatePack {
11    pub manifest: PackManifest,
12    /// Template name → template content.
13    pub templates: HashMap<String, String>,
14}
15
16/// The `oag.pack.toml` manifest.
17#[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    /// Inherit templates, type_map, filters, layouts from another pack.
44    pub extends: Option<String>,
45    /// Field name casing: "camel", "snake", "original".
46    #[serde(default = "default_camel")]
47    pub field_casing: String,
48    /// Operation name casing: "camel", "snake", "pascal".
49    #[serde(default = "default_camel")]
50    pub operation_casing: String,
51}
52
53fn default_camel() -> String {
54    "camel".to_string()
55}
56
57/// Declarative type mapping from IR types to language type strings.
58#[derive(Debug, Clone, Default, Deserialize)]
59#[serde(default)]
60pub struct TypeMapConfig {
61    // Primitives
62    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    // Parameterized
73    pub string_literal: String,
74    pub integer_literal: String,
75    #[serde(rename = "ref")]
76    pub ref_type: String,
77    pub array: String,
78    /// Used when inner type is a union; falls back to `array` if absent.
79    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    /// Suffix for optional fields (e.g. " | None = None" for Python).
93    #[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    /// Additional files appended when pack extends another (used by react-swr-client).
126    #[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    /// Optional condition expression (evaluated as minijinja expression).
135    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    /// Optional setup command to run before validation (e.g. `npm install`).
183    pub setup: Option<String>,
184}
185
186impl TemplatePack {
187    /// Load a template pack from a directory containing `oag.pack.toml` and `templates/`.
188    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    /// Merge another pack into this one (for `extends` support).
226    /// The extending pack's entries override the base.
227    pub fn merge_from(&mut self, extending: &TemplatePack) {
228        // Override templates
229        for (name, content) in &extending.templates {
230            self.templates.insert(name.clone(), content.clone());
231        }
232
233        // Override type_map if the extending pack defines non-empty entries
234        // (we use the extending manifest wholesale since TOML defaults fill in)
235        if !extending.manifest.type_map.string.is_empty() {
236            self.manifest.type_map = extending.manifest.type_map.clone();
237        }
238
239        // Merge filters
240        for (name, filter) in &extending.manifest.filters {
241            self.manifest.filters.insert(name.clone(), filter.clone());
242        }
243
244        // Merge layouts: extra_files for modular
245        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                // Also add the extending pack's own files list
249                base_modular.files.extend(ext_modular.files.clone());
250            } else {
251                self.manifest.layouts.modular = Some(ext_modular.clone());
252            }
253        }
254
255        // Override bundled/split if provided
256        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        // Merge scaffold
264        if !extending.manifest.scaffold.files.is_empty() {
265            self.manifest.scaffold = extending.manifest.scaffold.clone();
266        }
267
268        // Merge formatters
269        for (name, fmt) in &extending.manifest.formatters {
270            self.manifest.formatters.insert(name.clone(), fmt.clone());
271        }
272
273        // Merge validators
274        for (name, val) in &extending.manifest.validators {
275            self.manifest.validators.insert(name.clone(), val.clone());
276        }
277
278        // Update pack metadata
279        self.manifest.pack = extending.manifest.pack.clone();
280    }
281}