1use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5use std::path::Path;
6
7use crate::error::{CoreError, Result};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct Values(pub JsonValue);
13
14impl Values {
15 pub fn new() -> Self {
17 Self(JsonValue::Object(serde_json::Map::new()))
18 }
19
20 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
22 let content = std::fs::read_to_string(path.as_ref())?;
23 Self::from_yaml(&content)
24 }
25
26 pub fn from_yaml(yaml: &str) -> Result<Self> {
28 let value: JsonValue = serde_yaml::from_str(yaml)?;
29 Ok(Self(value))
30 }
31
32 pub fn from_json(json: &str) -> Result<Self> {
34 let value: JsonValue = serde_json::from_str(json)?;
35 Ok(Self(value))
36 }
37
38 pub fn merge(&mut self, overlay: &Values) {
45 deep_merge(&mut self.0, &overlay.0);
46 }
47
48 pub fn merge_all(values: Vec<Values>) -> Self {
50 let mut result = Values::new();
51 for v in values {
52 result.merge(&v);
53 }
54 result
55 }
56
57 pub fn set(&mut self, path: &str, value: JsonValue) -> Result<()> {
59 let parts: Vec<&str> = path.split('.').collect();
60 set_nested(&mut self.0, &parts, value)
61 }
62
63 pub fn get(&self, path: &str) -> Option<&JsonValue> {
65 let parts: Vec<&str> = path.split('.').collect();
66 get_nested(&self.0, &parts)
67 }
68
69 pub fn inner(&self) -> &JsonValue {
71 &self.0
72 }
73
74 pub fn into_inner(self) -> JsonValue {
76 self.0
77 }
78
79 pub fn is_empty(&self) -> bool {
81 match &self.0 {
82 JsonValue::Object(map) => map.is_empty(),
83 JsonValue::Null => true,
84 _ => false,
85 }
86 }
87
88 pub fn with_schema_defaults(schema_defaults: Values, base: Values) -> Self {
93 let mut result = schema_defaults;
95 result.merge(&base);
96 result
97 }
98
99 pub fn scope_for_subchart(&self, subchart_name: &str) -> Values {
132 let mut scoped = serde_json::Map::new();
133
134 if let JsonValue::Object(parent_obj) = &self.0 {
135 if let Some(global) = parent_obj.get("global") {
137 scoped.insert("global".to_string(), global.clone());
138 }
139
140 if let Some(JsonValue::Object(subchart_obj)) = parent_obj.get(subchart_name) {
142 for (k, v) in subchart_obj {
143 scoped.insert(k.clone(), v.clone());
144 }
145 }
146 }
147
148 Values(JsonValue::Object(scoped))
149 }
150
151 pub fn for_subchart(
162 subchart_defaults: Values,
163 parent_values: &Values,
164 subchart_name: &str,
165 ) -> Values {
166 let mut result = subchart_defaults;
167 let scoped = parent_values.scope_for_subchart(subchart_name);
168 result.merge(&scoped);
169 result
170 }
171
172 pub fn export_to_parent(&self, subchart_name: &str) -> Values {
196 let mut parent = serde_json::Map::new();
197 let mut subchart_values = serde_json::Map::new();
198
199 if let JsonValue::Object(obj) = &self.0 {
200 for (k, v) in obj {
201 if k == "global" {
202 parent.insert(k.clone(), v.clone());
204 } else {
205 subchart_values.insert(k.clone(), v.clone());
207 }
208 }
209 }
210
211 if !subchart_values.is_empty() {
212 parent.insert(
213 subchart_name.to_string(),
214 JsonValue::Object(subchart_values),
215 );
216 }
217
218 Values(JsonValue::Object(parent))
219 }
220
221 pub fn scope_json_for_subchart(parent_json: &JsonValue, subchart_name: &str) -> Values {
230 let mut scoped = serde_json::Map::new();
231
232 if let JsonValue::Object(parent_obj) = parent_json {
233 if let Some(global) = parent_obj.get("global") {
235 scoped.insert("global".to_string(), global.clone());
236 }
237
238 if let Some(JsonValue::Object(subchart_obj)) = parent_obj.get(subchart_name) {
240 for (k, v) in subchart_obj {
241 scoped.insert(k.clone(), v.clone());
242 }
243 }
244 }
245
246 Values(JsonValue::Object(scoped))
247 }
248
249 pub fn for_subchart_json(
253 subchart_defaults: Values,
254 parent_json: &JsonValue,
255 subchart_name: &str,
256 ) -> Values {
257 let mut result = subchart_defaults;
258 let scoped = Self::scope_json_for_subchart(parent_json, subchart_name);
259 result.merge(&scoped);
260 result
261 }
262}
263
264fn deep_merge(base: &mut JsonValue, overlay: &JsonValue) {
266 match (base, overlay) {
267 (JsonValue::Object(base_map), JsonValue::Object(overlay_map)) => {
268 for (key, overlay_value) in overlay_map {
269 match base_map.get_mut(key) {
270 Some(base_value) => deep_merge(base_value, overlay_value),
271 None => {
272 base_map.insert(key.clone(), overlay_value.clone());
273 }
274 }
275 }
276 }
277 (base, overlay) => {
278 *base = overlay.clone();
279 }
280 }
281}
282
283fn set_nested(value: &mut JsonValue, path: &[&str], new_value: JsonValue) -> Result<()> {
285 if path.is_empty() {
286 *value = new_value;
287 return Ok(());
288 }
289
290 let key = path[0];
291 let remaining = &path[1..];
292
293 if !value.is_object() {
295 *value = JsonValue::Object(serde_json::Map::new());
296 }
297
298 let map = value
300 .as_object_mut()
301 .expect("value should be an object after initialization");
302
303 if remaining.is_empty() {
304 map.insert(key.to_string(), new_value);
305 } else {
306 let entry = map
307 .entry(key.to_string())
308 .or_insert_with(|| JsonValue::Object(serde_json::Map::new()));
309 set_nested(entry, remaining, new_value)?;
310 }
311
312 Ok(())
313}
314
315fn get_nested<'a>(value: &'a JsonValue, path: &[&str]) -> Option<&'a JsonValue> {
317 if path.is_empty() {
318 return Some(value);
319 }
320
321 let key = path[0];
322 let remaining = &path[1..];
323
324 match value {
325 JsonValue::Object(map) => map.get(key).and_then(|v| get_nested(v, remaining)),
326 _ => None,
327 }
328}
329
330pub fn parse_set_values(set_args: &[String]) -> Result<Values> {
332 let mut values = Values::new();
333
334 for arg in set_args {
335 let (key, val) = arg.split_once('=').ok_or_else(|| CoreError::ValuesMerge {
336 message: format!("Invalid --set format: '{}'. Expected key=value", arg),
337 })?;
338
339 let json_value = if val == "true" {
341 JsonValue::Bool(true)
342 } else if val == "false" {
343 JsonValue::Bool(false)
344 } else if val == "null" {
345 JsonValue::Null
346 } else if let Ok(num) = val.parse::<i64>() {
347 JsonValue::Number(num.into())
348 } else if let Ok(num) = val.parse::<f64>() {
349 JsonValue::Number(serde_json::Number::from_f64(num).unwrap_or(0.into()))
350 } else if val.starts_with('[') || val.starts_with('{') {
351 serde_json::from_str(val).unwrap_or(JsonValue::String(val.to_string()))
352 } else {
353 JsonValue::String(val.to_string())
354 };
355
356 values.set(key, json_value)?;
357 }
358
359 Ok(values)
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_deep_merge() {
368 let mut base = Values::from_yaml(
369 r#"
370image:
371 repository: nginx
372 tag: "1.0"
373replicas: 1
374"#,
375 )
376 .unwrap();
377
378 let overlay = Values::from_yaml(
379 r#"
380image:
381 tag: "2.0"
382 pullPolicy: Always
383replicas: 3
384"#,
385 )
386 .unwrap();
387
388 base.merge(&overlay);
389
390 assert_eq!(base.get("image.repository").unwrap(), "nginx");
391 assert_eq!(base.get("image.tag").unwrap(), "2.0");
392 assert_eq!(base.get("image.pullPolicy").unwrap(), "Always");
393 assert_eq!(base.get("replicas").unwrap(), 3);
394 }
395
396 #[test]
397 fn test_set_nested() {
398 let mut values = Values::new();
399 values
400 .set("image.tag", JsonValue::String("v1".into()))
401 .unwrap();
402 values.set("replicas", JsonValue::Number(3.into())).unwrap();
403
404 assert_eq!(values.get("image.tag").unwrap(), "v1");
405 assert_eq!(values.get("replicas").unwrap(), 3);
406 }
407
408 #[test]
409 fn test_parse_set_values() {
410 let args = vec![
411 "image.tag=v2".to_string(),
412 "replicas=5".to_string(),
413 "debug=true".to_string(),
414 ];
415
416 let values = parse_set_values(&args).unwrap();
417
418 assert_eq!(values.get("image.tag").unwrap(), "v2");
419 assert_eq!(values.get("replicas").unwrap(), 5);
420 assert_eq!(values.get("debug").unwrap(), true);
421 }
422
423 #[test]
424 fn test_scope_for_subchart_basic() {
425 let parent = Values::from_yaml(
426 r#"
427global:
428 imageRegistry: docker.io
429redis:
430 enabled: true
431 replicas: 3
432postgresql:
433 enabled: false
434"#,
435 )
436 .unwrap();
437
438 let scoped = parent.scope_for_subchart("redis");
439
440 assert_eq!(scoped.get("global.imageRegistry").unwrap(), "docker.io");
442
443 assert_eq!(scoped.get("enabled").unwrap(), true);
445 assert_eq!(scoped.get("replicas").unwrap(), 3);
446
447 assert!(scoped.get("postgresql").is_none());
449 assert!(scoped.get("redis").is_none());
450 }
451
452 #[test]
453 fn test_scope_for_subchart_no_global() {
454 let parent = Values::from_yaml(
455 r#"
456redis:
457 host: localhost
458 port: 6379
459"#,
460 )
461 .unwrap();
462
463 let scoped = parent.scope_for_subchart("redis");
464
465 assert_eq!(scoped.get("host").unwrap(), "localhost");
466 assert_eq!(scoped.get("port").unwrap(), 6379);
467 assert!(scoped.get("global").is_none());
468 }
469
470 #[test]
471 fn test_scope_for_subchart_missing_subchart() {
472 let parent = Values::from_yaml(
473 r#"
474global:
475 debug: true
476redis:
477 enabled: true
478"#,
479 )
480 .unwrap();
481
482 let scoped = parent.scope_for_subchart("postgresql");
483
484 assert_eq!(scoped.get("global.debug").unwrap(), true);
486 assert!(scoped.get("enabled").is_none());
487 }
488
489 #[test]
490 fn test_for_subchart_with_defaults() {
491 let subchart_defaults = Values::from_yaml(
492 r#"
493enabled: false
494replicas: 1
495image:
496 repository: redis
497 tag: "7.0"
498"#,
499 )
500 .unwrap();
501
502 let parent = Values::from_yaml(
503 r#"
504global:
505 pullPolicy: Always
506redis:
507 enabled: true
508 replicas: 3
509"#,
510 )
511 .unwrap();
512
513 let result = Values::for_subchart(subchart_defaults, &parent, "redis");
514
515 assert_eq!(result.get("global.pullPolicy").unwrap(), "Always");
517
518 assert_eq!(result.get("enabled").unwrap(), true);
520 assert_eq!(result.get("replicas").unwrap(), 3);
521
522 assert_eq!(result.get("image.repository").unwrap(), "redis");
524 assert_eq!(result.get("image.tag").unwrap(), "7.0");
525 }
526
527 #[test]
528 fn test_export_to_parent() {
529 let subchart = Values::from_yaml(
530 r#"
531global:
532 imageRegistry: docker.io
533enabled: true
534replicas: 3
535image:
536 tag: "7.0"
537"#,
538 )
539 .unwrap();
540
541 let exported = subchart.export_to_parent("redis");
542
543 assert_eq!(exported.get("global.imageRegistry").unwrap(), "docker.io");
545
546 assert_eq!(exported.get("redis.enabled").unwrap(), true);
548 assert_eq!(exported.get("redis.replicas").unwrap(), 3);
549 assert_eq!(exported.get("redis.image.tag").unwrap(), "7.0");
550 }
551
552 #[test]
553 fn test_scope_and_export_roundtrip() {
554 let original_parent = Values::from_yaml(
555 r#"
556global:
557 env: production
558redis:
559 enabled: true
560 maxMemory: 256mb
561"#,
562 )
563 .unwrap();
564
565 let scoped = original_parent.scope_for_subchart("redis");
567
568 let exported = scoped.export_to_parent("redis");
570
571 assert_eq!(exported.get("global.env").unwrap(), "production");
573 assert_eq!(exported.get("redis.enabled").unwrap(), true);
574 assert_eq!(exported.get("redis.maxMemory").unwrap(), "256mb");
575 }
576
577 #[test]
578 fn test_scope_json_for_subchart() {
579 let parent_json = serde_json::json!({
580 "global": {
581 "imageRegistry": "docker.io"
582 },
583 "redis": {
584 "enabled": true,
585 "replicas": 3
586 },
587 "postgresql": {
588 "enabled": false
589 }
590 });
591
592 let scoped = Values::scope_json_for_subchart(&parent_json, "redis");
593
594 assert_eq!(scoped.get("global.imageRegistry").unwrap(), "docker.io");
596
597 assert_eq!(scoped.get("enabled").unwrap(), true);
599 assert_eq!(scoped.get("replicas").unwrap(), 3);
600
601 assert!(scoped.get("postgresql").is_none());
603 assert!(scoped.get("redis").is_none());
604 }
605
606 #[test]
607 fn test_for_subchart_json() {
608 let subchart_defaults = Values::from_yaml(
609 r#"
610enabled: false
611replicas: 1
612image:
613 repository: redis
614 tag: "7.0"
615"#,
616 )
617 .unwrap();
618
619 let parent_json = serde_json::json!({
620 "global": {
621 "pullPolicy": "Always"
622 },
623 "redis": {
624 "enabled": true,
625 "replicas": 3
626 }
627 });
628
629 let result = Values::for_subchart_json(subchart_defaults, &parent_json, "redis");
630
631 assert_eq!(result.get("global.pullPolicy").unwrap(), "Always");
633
634 assert_eq!(result.get("enabled").unwrap(), true);
636 assert_eq!(result.get("replicas").unwrap(), 3);
637
638 assert_eq!(result.get("image.repository").unwrap(), "redis");
640 assert_eq!(result.get("image.tag").unwrap(), "7.0");
641 }
642}