1#![allow(clippy::collapsible_if)]
2#![allow(clippy::collapsible_match)]
3#![allow(clippy::or_fun_call)]
4
5use serde_yaml::Value;
34use std::collections::HashMap;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum InferredType {
43 Scalar,
45 List,
47 Dict,
49 Unknown,
51}
52
53impl InferredType {
54 #[inline]
56 pub fn is_collection(&self) -> bool {
57 matches!(self, Self::List | Self::Dict)
58 }
59
60 #[inline]
62 pub fn is_dict(&self) -> bool {
63 matches!(self, Self::Dict)
64 }
65
66 #[inline]
68 pub fn is_list(&self) -> bool {
69 matches!(self, Self::List)
70 }
71}
72
73impl std::fmt::Display for InferredType {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::Scalar => write!(f, "scalar"),
77 Self::List => write!(f, "list"),
78 Self::Dict => write!(f, "dict"),
79 Self::Unknown => write!(f, "unknown"),
80 }
81 }
82}
83
84#[derive(Debug, Default, Clone)]
93pub struct TypeContext {
94 types: HashMap<String, InferredType>,
97}
98
99impl TypeContext {
100 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
111 let value: Value = serde_yaml::from_str(yaml)?;
112 Ok(Self::from_value(&value))
113 }
114
115 pub fn from_value(value: &Value) -> Self {
117 let mut ctx = Self::new();
118 ctx.collect_types_recursive("", value);
119 ctx
120 }
121
122 fn collect_types_recursive(&mut self, prefix: &str, value: &Value) {
124 match value {
125 Value::Mapping(map) => {
126 if !prefix.is_empty() {
128 self.types.insert(prefix.to_string(), InferredType::Dict);
129 }
130
131 for (key, child) in map {
133 if let Some(key_str) = key.as_str() {
134 let child_path = if prefix.is_empty() {
135 key_str.to_string()
136 } else {
137 format!("{}.{}", prefix, key_str)
138 };
139 self.collect_types_recursive(&child_path, child);
140 }
141 }
142 }
143 Value::Sequence(seq) => {
144 if !prefix.is_empty() {
146 self.types.insert(prefix.to_string(), InferredType::List);
147 }
148
149 if let Some(first) = seq.first() {
152 if let Value::Mapping(_) = first {
153 }
156 }
157 }
158 _ => {
159 if !prefix.is_empty() {
161 self.types.insert(prefix.to_string(), InferredType::Scalar);
162 }
163 }
164 }
165 }
166
167 pub fn get_type(&self, path: &str) -> InferredType {
176 let normalized = Self::normalize_path(path);
177 self.types
178 .get(&normalized)
179 .copied()
180 .unwrap_or(InferredType::Unknown)
181 }
182
183 pub fn contains(&self, path: &str) -> bool {
185 let normalized = Self::normalize_path(path);
186 self.types.contains_key(&normalized)
187 }
188
189 pub fn all_types(&self) -> impl Iterator<Item = (&str, InferredType)> {
191 self.types.iter().map(|(k, v)| (k.as_str(), *v))
192 }
193
194 pub fn len(&self) -> usize {
196 self.types.len()
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.types.is_empty()
202 }
203
204 fn normalize_path(path: &str) -> String {
210 let path = path.trim();
211
212 let path = path.strip_prefix('.').unwrap_or(path);
214
215 let path = path
217 .strip_prefix("Values.")
218 .or_else(|| path.strip_prefix("values."))
219 .unwrap_or(path);
220
221 path.to_string()
222 }
223}
224
225pub struct TypeHeuristics;
234
235impl TypeHeuristics {
236 const DICT_SUFFIXES: &'static [&'static str] = &[
238 "annotations",
239 "labels",
240 "selector",
241 "matchLabels",
242 "nodeSelector",
243 "config",
244 "configMap",
245 "data",
246 "stringData",
247 "env",
248 "ports",
249 "containerPort",
250 "hostPort",
251 "resources",
252 "limits",
253 "requests",
254 "securityContext",
255 "podSecurityContext",
256 "affinity",
257 "tolerations",
258 "headers",
259 "proxyHeaders",
260 "extraArgs",
261 ];
262
263 const LIST_SUFFIXES: &'static [&'static str] = &[
265 "items",
266 "containers",
267 "initContainers",
268 "volumes",
269 "volumeMounts",
270 "envFrom",
271 "imagePullSecrets",
272 "hosts",
273 "rules",
274 "paths",
275 "tls",
276 "extraVolumes",
277 "extraVolumeMounts",
278 "extraContainers",
279 "extraInitContainers",
280 "extraEnvs",
281 ];
282
283 pub fn guess_type(path: &str) -> Option<InferredType> {
287 let last_segment = path.rsplit('.').next().unwrap_or(path);
288 let lower = last_segment.to_ascii_lowercase();
289
290 for suffix in Self::DICT_SUFFIXES {
292 let suffix_lower = suffix.to_ascii_lowercase();
293 if lower == suffix_lower || lower.ends_with(&suffix_lower) {
294 return Some(InferredType::Dict);
295 }
296 }
297
298 for suffix in Self::LIST_SUFFIXES {
300 let suffix_lower = suffix.to_ascii_lowercase();
301 if lower == suffix_lower || lower.ends_with(&suffix_lower) {
302 return Some(InferredType::List);
303 }
304 }
305
306 None
307 }
308}
309
310#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_basic_types() {
320 let yaml = r#"
321controller:
322 replicas: 3
323 enabled: true
324 name: nginx
325"#;
326 let ctx = TypeContext::from_yaml(yaml).unwrap();
327
328 assert_eq!(ctx.get_type("controller"), InferredType::Dict);
329 assert_eq!(ctx.get_type("controller.replicas"), InferredType::Scalar);
330 assert_eq!(ctx.get_type("controller.enabled"), InferredType::Scalar);
331 assert_eq!(ctx.get_type("controller.name"), InferredType::Scalar);
332 }
333
334 #[test]
335 fn test_nested_dict() {
336 let yaml = r#"
337controller:
338 containerPort:
339 http: 80
340 https: 443
341 image:
342 repository: nginx
343 tag: latest
344"#;
345 let ctx = TypeContext::from_yaml(yaml).unwrap();
346
347 assert_eq!(ctx.get_type("controller"), InferredType::Dict);
348 assert_eq!(ctx.get_type("controller.containerPort"), InferredType::Dict);
349 assert_eq!(
350 ctx.get_type("controller.containerPort.http"),
351 InferredType::Scalar
352 );
353 assert_eq!(ctx.get_type("controller.image"), InferredType::Dict);
354 assert_eq!(
355 ctx.get_type("controller.image.repository"),
356 InferredType::Scalar
357 );
358 }
359
360 #[test]
361 fn test_list_types() {
362 let yaml = r#"
363controller:
364 extraEnvs:
365 - name: FOO
366 value: bar
367 - name: BAZ
368 value: qux
369 labels:
370 - app
371 - version
372"#;
373 let ctx = TypeContext::from_yaml(yaml).unwrap();
374
375 assert_eq!(ctx.get_type("controller.extraEnvs"), InferredType::List);
376 assert_eq!(ctx.get_type("controller.labels"), InferredType::List);
377 }
378
379 #[test]
380 fn test_path_normalization() {
381 let yaml = r#"
382controller:
383 replicas: 3
384"#;
385 let ctx = TypeContext::from_yaml(yaml).unwrap();
386
387 assert_eq!(ctx.get_type("controller.replicas"), InferredType::Scalar);
389 assert_eq!(
390 ctx.get_type("values.controller.replicas"),
391 InferredType::Scalar
392 );
393 assert_eq!(
394 ctx.get_type(".Values.controller.replicas"),
395 InferredType::Scalar
396 );
397 assert_eq!(
398 ctx.get_type("Values.controller.replicas"),
399 InferredType::Scalar
400 );
401 }
402
403 #[test]
404 fn test_unknown_path() {
405 let yaml = r#"
406controller:
407 replicas: 3
408"#;
409 let ctx = TypeContext::from_yaml(yaml).unwrap();
410
411 assert_eq!(ctx.get_type("nonexistent"), InferredType::Unknown);
412 assert_eq!(ctx.get_type("controller.unknown"), InferredType::Unknown);
413 }
414
415 #[test]
416 fn test_heuristics_dict() {
417 assert_eq!(
418 TypeHeuristics::guess_type("controller.annotations"),
419 Some(InferredType::Dict)
420 );
421 assert_eq!(
422 TypeHeuristics::guess_type("controller.labels"),
423 Some(InferredType::Dict)
424 );
425 assert_eq!(
426 TypeHeuristics::guess_type("pod.nodeSelector"),
427 Some(InferredType::Dict)
428 );
429 assert_eq!(
430 TypeHeuristics::guess_type("controller.containerPort"),
431 Some(InferredType::Dict)
432 );
433 }
434
435 #[test]
436 fn test_heuristics_list() {
437 assert_eq!(
438 TypeHeuristics::guess_type("spec.containers"),
439 Some(InferredType::List)
440 );
441 assert_eq!(
442 TypeHeuristics::guess_type("controller.extraVolumes"),
443 Some(InferredType::List)
444 );
445 assert_eq!(
446 TypeHeuristics::guess_type("pod.imagePullSecrets"),
447 Some(InferredType::List)
448 );
449 }
450
451 #[test]
452 fn test_heuristics_unknown() {
453 assert_eq!(TypeHeuristics::guess_type("controller.replicas"), None);
454 assert_eq!(TypeHeuristics::guess_type("custom.field"), None);
455 }
456
457 #[test]
458 fn test_complex_structure() {
459 let yaml = r#"
460global:
461 image:
462 registry: docker.io
463controller:
464 kind: Deployment
465 hostNetwork: false
466 containerPort:
467 http: 80
468 https: 443
469 admissionWebhooks:
470 enabled: true
471 patch:
472 image:
473 registry: registry.k8s.io
474 image: ingress-nginx/kube-webhook-certgen
475 tag: v1.4.1
476tcp: {}
477udp: {}
478"#;
479 let ctx = TypeContext::from_yaml(yaml).unwrap();
480
481 assert_eq!(ctx.get_type("global"), InferredType::Dict);
483 assert_eq!(ctx.get_type("controller"), InferredType::Dict);
484 assert_eq!(ctx.get_type("tcp"), InferredType::Dict);
485 assert_eq!(ctx.get_type("udp"), InferredType::Dict);
486
487 assert_eq!(ctx.get_type("global.image"), InferredType::Dict);
489 assert_eq!(ctx.get_type("controller.containerPort"), InferredType::Dict);
490 assert_eq!(
491 ctx.get_type("controller.admissionWebhooks.patch.image"),
492 InferredType::Dict
493 );
494
495 assert_eq!(ctx.get_type("controller.kind"), InferredType::Scalar);
497 assert_eq!(ctx.get_type("controller.hostNetwork"), InferredType::Scalar);
498 assert_eq!(
499 ctx.get_type("controller.admissionWebhooks.enabled"),
500 InferredType::Scalar
501 );
502 }
503
504 #[test]
505 fn test_is_methods() {
506 assert!(InferredType::Dict.is_dict());
507 assert!(InferredType::Dict.is_collection());
508 assert!(!InferredType::Dict.is_list());
509
510 assert!(InferredType::List.is_list());
511 assert!(InferredType::List.is_collection());
512 assert!(!InferredType::List.is_dict());
513
514 assert!(!InferredType::Scalar.is_collection());
515 assert!(!InferredType::Unknown.is_collection());
516 }
517
518 #[test]
519 fn test_display() {
520 assert_eq!(format!("{}", InferredType::Scalar), "scalar");
521 assert_eq!(format!("{}", InferredType::List), "list");
522 assert_eq!(format!("{}", InferredType::Dict), "dict");
523 assert_eq!(format!("{}", InferredType::Unknown), "unknown");
524 }
525
526 #[test]
527 fn test_len_and_is_empty() {
528 let empty = TypeContext::new();
529 assert!(empty.is_empty());
530 assert_eq!(empty.len(), 0);
531
532 let ctx = TypeContext::from_yaml("foo: bar").unwrap();
533 assert!(!ctx.is_empty());
534 assert_eq!(ctx.len(), 1);
535 }
536}