1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct SpecEntry {
6 pub name: String,
8 pub path: String,
10
11 pub schemas: SchemasConfig,
13
14 pub apis: ApisConfig,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub hooks: Option<HooksConfig>,
20
21 pub modules: ModulesConfig,
23}
24
25#[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 #[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#[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#[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 #[serde(skip_serializing_if = "Option::is_none")]
102 pub timeout: Option<u32>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub retries: Option<u32>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub retry_delay: Option<u32>,
111
112 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct HooksConfig {
134 #[serde(default = "default_hooks_output")]
136 pub output: String,
137
138 #[serde(default = "default_query_keys_output")]
140 pub query_keys_output: String,
141
142 #[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#[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#[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 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}