vika_cli/config/
model.rs

1use serde::{Deserialize, Serialize};
2
3/// Represents a single OpenAPI specification entry in multi-spec mode.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct SpecEntry {
6    /// Unique name for this spec (kebab-case recommended)
7    pub name: String,
8    /// Path or URL to the OpenAPI specification file
9    pub path: String,
10
11    /// Required per-spec schema output directory and naming configuration
12    pub schemas: SchemasConfig,
13
14    /// Required per-spec API output directory and configuration
15    pub apis: ApisConfig,
16
17    /// Required per-spec module selection configuration
18    pub modules: ModulesConfig,
19}
20
21/// Main configuration structure for vika-cli.
22///
23/// Represents the `.vika.json` configuration file that controls
24/// code generation behavior, output directories, and module selection.
25///
26/// # Example
27///
28/// ```no_run
29/// use vika_cli::Config;
30///
31/// let config = Config::default();
32/// println!("Root directory: {}", config.root_dir);
33/// ```
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Config {
36    #[serde(rename = "$schema", default = "default_schema")]
37    pub schema: String,
38
39    #[serde(default = "default_root_dir")]
40    pub root_dir: String,
41
42    #[serde(default)]
43    pub generation: GenerationConfig,
44
45    /// Specs configuration - always use array, even for single spec
46    #[serde(default)]
47    pub specs: Vec<SpecEntry>,
48}
49
50pub fn default_schema() -> String {
51    "https://raw.githubusercontent.com/vikarno/vika-cli/main/schema/vika-config.schema.json"
52        .to_string()
53}
54
55fn default_root_dir() -> String {
56    "src".to_string()
57}
58
59/// Configuration for schema generation (TypeScript types and Zod schemas).
60///
61/// Controls where schemas are generated and how they are named.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SchemasConfig {
64    #[serde(default = "default_schemas_output")]
65    pub output: String,
66
67    #[serde(default = "default_naming")]
68    pub naming: String,
69}
70
71fn default_naming() -> String {
72    "PascalCase".to_string()
73}
74
75fn default_schemas_output() -> String {
76    "src/schemas".to_string()
77}
78
79/// Configuration for API client generation.
80///
81/// Controls API client output location, style, base URL, and header strategy.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ApisConfig {
84    #[serde(default = "default_apis_output")]
85    pub output: String,
86
87    #[serde(default = "default_style")]
88    pub style: String,
89
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub base_url: Option<String>,
92
93    #[serde(default = "default_header_strategy")]
94    pub header_strategy: String,
95}
96
97fn default_header_strategy() -> String {
98    "consumerInjected".to_string()
99}
100
101fn default_apis_output() -> String {
102    "src/apis".to_string()
103}
104
105fn default_style() -> String {
106    "fetch".to_string()
107}
108
109/// Configuration for module selection and filtering.
110///
111/// Controls which OpenAPI tags/modules are included or excluded from generation.
112#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct ModulesConfig {
114    #[serde(default)]
115    pub ignore: Vec<String>,
116
117    #[serde(default)]
118    pub selected: Vec<String>,
119}
120
121/// Configuration for generation behavior and preferences.
122///
123/// Controls caching, backups, and conflict resolution strategy.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct GenerationConfig {
126    #[serde(default = "default_enable_cache")]
127    pub enable_cache: bool,
128
129    #[serde(default = "default_enable_backup")]
130    pub enable_backup: bool,
131
132    #[serde(default = "default_conflict_strategy")]
133    pub conflict_strategy: String,
134}
135
136fn default_enable_cache() -> bool {
137    true
138}
139
140fn default_enable_backup() -> bool {
141    false
142}
143
144fn default_conflict_strategy() -> String {
145    "ask".to_string()
146}
147
148impl Default for GenerationConfig {
149    fn default() -> Self {
150        Self {
151            enable_cache: default_enable_cache(),
152            enable_backup: default_enable_backup(),
153            conflict_strategy: default_conflict_strategy(),
154        }
155    }
156}
157
158impl Default for Config {
159    fn default() -> Self {
160        Self {
161            schema: default_schema(),
162            root_dir: default_root_dir(),
163            generation: GenerationConfig::default(),
164            specs: vec![],
165        }
166    }
167}
168
169impl Default for SchemasConfig {
170    fn default() -> Self {
171        Self {
172            output: default_schemas_output(),
173            naming: default_naming(),
174        }
175    }
176}
177
178impl Default for ApisConfig {
179    fn default() -> Self {
180        Self {
181            output: default_apis_output(),
182            style: default_style(),
183            base_url: None,
184            header_strategy: default_header_strategy(),
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_load_default_config() {
195        let config = Config::default();
196        assert_eq!(config.root_dir, "src");
197        assert!(!config.schema.is_empty());
198        assert_eq!(config.specs.len(), 0);
199    }
200
201    #[test]
202    fn test_config_serialization() {
203        let config = Config::default();
204        let json = serde_json::to_string_pretty(&config).unwrap();
205
206        assert!(json.contains("\"root_dir\""));
207        assert!(json.contains("\"specs\""));
208        assert!(json.contains("\"$schema\""));
209    }
210
211    #[test]
212    fn test_config_deserialization() {
213        let json = r#"
214        {
215            "$schema": "https://example.com/schema.json",
216            "root_dir": "test",
217            "specs": [
218                {
219                    "name": "test-spec",
220                    "path": "test.yaml",
221                    "schemas": {
222                        "output": "test/schemas",
223                        "naming": "camelCase"
224                    },
225                    "apis": {
226                        "output": "test/apis",
227                        "style": "fetch",
228                        "header_strategy": "bearerToken"
229                    },
230                    "modules": {
231                        "ignore": ["test"],
232                        "selected": []
233                    }
234                }
235            ]
236        }
237        "#;
238
239        let config: Config = serde_json::from_str(json).unwrap();
240        assert_eq!(config.root_dir, "test");
241        assert_eq!(config.specs.len(), 1);
242        assert_eq!(config.specs[0].schemas.output, "test/schemas");
243        assert_eq!(config.specs[0].schemas.naming, "camelCase");
244        assert_eq!(config.specs[0].apis.header_strategy, "bearerToken");
245        assert_eq!(config.specs[0].modules.ignore, vec!["test"]);
246    }
247
248    #[test]
249    fn test_schemas_config_default() {
250        let config = SchemasConfig::default();
251        assert_eq!(config.output, "src/schemas");
252        assert_eq!(config.naming, "PascalCase");
253    }
254
255    #[test]
256    fn test_apis_config_default() {
257        let config = ApisConfig::default();
258        assert_eq!(config.output, "src/apis");
259        assert_eq!(config.style, "fetch");
260        assert_eq!(config.header_strategy, "consumerInjected");
261        assert!(config.base_url.is_none());
262    }
263
264    #[test]
265    fn test_config_with_base_url() {
266        let mut config = Config::default();
267        config.specs.push(SpecEntry {
268            name: "test".to_string(),
269            path: "test.yaml".to_string(),
270            schemas: SchemasConfig::default(),
271            apis: ApisConfig {
272                base_url: Some("/api/v1".to_string()),
273                ..ApisConfig::default()
274            },
275            modules: ModulesConfig::default(),
276        });
277
278        let json = serde_json::to_string_pretty(&config).unwrap();
279        assert!(json.contains("\"base_url\""));
280        assert!(json.contains("/api/v1"));
281    }
282
283    #[test]
284    fn test_config_schema_field() {
285        let config = Config::default();
286        let json = serde_json::to_string_pretty(&config).unwrap();
287
288        // Check that $schema is included
289        assert!(json.contains("\"$schema\""));
290    }
291
292    #[test]
293    fn test_generation_config_defaults() {
294        let config = Config::default();
295        assert!(config.generation.enable_cache);
296        assert!(!config.generation.enable_backup);
297        assert_eq!(config.generation.conflict_strategy, "ask");
298    }
299
300    #[test]
301    fn test_config_with_generation_settings() {
302        let json = r#"
303        {
304            "$schema": "https://example.com/schema.json",
305            "root_dir": "test",
306            "schemas": {
307                "output": "test/schemas",
308                "naming": "camelCase"
309            },
310            "apis": {
311                "output": "test/apis",
312                "style": "fetch",
313                "header_strategy": "bearerToken"
314            },
315            "modules": {
316                "ignore": ["test"],
317                "selected": []
318            },
319            "generation": {
320                "enable_cache": false,
321                "enable_backup": true,
322                "conflict_strategy": "force"
323            }
324        }
325        "#;
326
327        let config: Config = serde_json::from_str(json).unwrap();
328        assert!(!config.generation.enable_cache);
329        assert!(config.generation.enable_backup);
330        assert_eq!(config.generation.conflict_strategy, "force");
331    }
332
333    #[test]
334    fn test_multi_spec_deserialization() {
335        let json = r#"
336        {
337            "$schema": "https://example.com/schema.json",
338            "specs": [
339                { 
340                    "name": "auth", 
341                    "path": "specs/auth.yaml",
342                    "schemas": {},
343                    "apis": {},
344                    "modules": {}
345                },
346                { 
347                    "name": "orders", 
348                    "path": "specs/orders.json",
349                    "schemas": {},
350                    "apis": {},
351                    "modules": {}
352                },
353                { 
354                    "name": "products", 
355                    "path": "specs/products.yaml",
356                    "schemas": {},
357                    "apis": {},
358                    "modules": {}
359                }
360            ]
361        }
362        "#;
363
364        let config: Config = serde_json::from_str(json).unwrap();
365        assert_eq!(config.specs.len(), 3);
366        assert_eq!(config.specs[0].name, "auth");
367        assert_eq!(config.specs[0].path, "specs/auth.yaml");
368        assert_eq!(config.specs[1].name, "orders");
369        assert_eq!(config.specs[1].path, "specs/orders.json");
370        assert_eq!(config.specs[2].name, "products");
371        assert_eq!(config.specs[2].path, "specs/products.yaml");
372    }
373
374    #[test]
375    fn test_spec_entry_serialization() {
376        let entry = SpecEntry {
377            name: "test-spec".to_string(),
378            path: "specs/test.yaml".to_string(),
379            schemas: SchemasConfig::default(),
380            apis: ApisConfig::default(),
381            modules: ModulesConfig::default(),
382        };
383        let json = serde_json::to_string(&entry).unwrap();
384        assert!(json.contains("\"name\""));
385        assert!(json.contains("\"path\""));
386        assert!(json.contains("test-spec"));
387        assert!(json.contains("specs/test.yaml"));
388    }
389}