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