#![allow(clippy::collapsible_if)]
#![allow(clippy::collapsible_match)]
#![allow(clippy::or_fun_call)]
use serde_yaml::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InferredType {
Scalar,
List,
Dict,
Unknown,
}
impl InferredType {
#[inline]
pub fn is_collection(&self) -> bool {
matches!(self, Self::List | Self::Dict)
}
#[inline]
pub fn is_dict(&self) -> bool {
matches!(self, Self::Dict)
}
#[inline]
pub fn is_list(&self) -> bool {
matches!(self, Self::List)
}
}
impl std::fmt::Display for InferredType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Scalar => write!(f, "scalar"),
Self::List => write!(f, "list"),
Self::Dict => write!(f, "dict"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct TypeContext {
types: HashMap<String, InferredType>,
}
impl TypeContext {
pub fn new() -> Self {
Self::default()
}
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
let value: Value = serde_yaml::from_str(yaml)?;
Ok(Self::from_value(&value))
}
pub fn from_value(value: &Value) -> Self {
let mut ctx = Self::new();
ctx.collect_types_recursive("", value);
ctx
}
fn collect_types_recursive(&mut self, prefix: &str, value: &Value) {
match value {
Value::Mapping(map) => {
if !prefix.is_empty() {
self.types.insert(prefix.to_string(), InferredType::Dict);
}
for (key, child) in map {
if let Some(key_str) = key.as_str() {
let child_path = if prefix.is_empty() {
key_str.to_string()
} else {
format!("{}.{}", prefix, key_str)
};
self.collect_types_recursive(&child_path, child);
}
}
}
Value::Sequence(seq) => {
if !prefix.is_empty() {
self.types.insert(prefix.to_string(), InferredType::List);
}
if let Some(first) = seq.first() {
if let Value::Mapping(_) = first {
}
}
}
_ => {
if !prefix.is_empty() {
self.types.insert(prefix.to_string(), InferredType::Scalar);
}
}
}
}
pub fn get_type(&self, path: &str) -> InferredType {
let normalized = Self::normalize_path(path);
self.types
.get(&normalized)
.copied()
.unwrap_or(InferredType::Unknown)
}
pub fn contains(&self, path: &str) -> bool {
let normalized = Self::normalize_path(path);
self.types.contains_key(&normalized)
}
pub fn all_types(&self) -> impl Iterator<Item = (&str, InferredType)> {
self.types.iter().map(|(k, v)| (k.as_str(), *v))
}
pub fn len(&self) -> usize {
self.types.len()
}
pub fn is_empty(&self) -> bool {
self.types.is_empty()
}
fn normalize_path(path: &str) -> String {
let path = path.trim();
let path = path.strip_prefix('.').unwrap_or(path);
let path = path
.strip_prefix("Values.")
.or_else(|| path.strip_prefix("values."))
.unwrap_or(path);
path.to_string()
}
}
pub struct TypeHeuristics;
impl TypeHeuristics {
const DICT_SUFFIXES: &'static [&'static str] = &[
"annotations",
"labels",
"selector",
"matchLabels",
"nodeSelector",
"config",
"configMap",
"data",
"stringData",
"env",
"ports",
"containerPort",
"hostPort",
"resources",
"limits",
"requests",
"securityContext",
"podSecurityContext",
"affinity",
"tolerations",
"headers",
"proxyHeaders",
"extraArgs",
];
const LIST_SUFFIXES: &'static [&'static str] = &[
"items",
"containers",
"initContainers",
"volumes",
"volumeMounts",
"envFrom",
"imagePullSecrets",
"hosts",
"rules",
"paths",
"tls",
"extraVolumes",
"extraVolumeMounts",
"extraContainers",
"extraInitContainers",
"extraEnvs",
];
pub fn guess_type(path: &str) -> Option<InferredType> {
let last_segment = path.rsplit('.').next().unwrap_or(path);
let lower = last_segment.to_ascii_lowercase();
for suffix in Self::DICT_SUFFIXES {
let suffix_lower = suffix.to_ascii_lowercase();
if lower == suffix_lower || lower.ends_with(&suffix_lower) {
return Some(InferredType::Dict);
}
}
for suffix in Self::LIST_SUFFIXES {
let suffix_lower = suffix.to_ascii_lowercase();
if lower == suffix_lower || lower.ends_with(&suffix_lower) {
return Some(InferredType::List);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_types() {
let yaml = r#"
controller:
replicas: 3
enabled: true
name: nginx
"#;
let ctx = TypeContext::from_yaml(yaml).unwrap();
assert_eq!(ctx.get_type("controller"), InferredType::Dict);
assert_eq!(ctx.get_type("controller.replicas"), InferredType::Scalar);
assert_eq!(ctx.get_type("controller.enabled"), InferredType::Scalar);
assert_eq!(ctx.get_type("controller.name"), InferredType::Scalar);
}
#[test]
fn test_nested_dict() {
let yaml = r#"
controller:
containerPort:
http: 80
https: 443
image:
repository: nginx
tag: latest
"#;
let ctx = TypeContext::from_yaml(yaml).unwrap();
assert_eq!(ctx.get_type("controller"), InferredType::Dict);
assert_eq!(ctx.get_type("controller.containerPort"), InferredType::Dict);
assert_eq!(
ctx.get_type("controller.containerPort.http"),
InferredType::Scalar
);
assert_eq!(ctx.get_type("controller.image"), InferredType::Dict);
assert_eq!(
ctx.get_type("controller.image.repository"),
InferredType::Scalar
);
}
#[test]
fn test_list_types() {
let yaml = r#"
controller:
extraEnvs:
- name: FOO
value: bar
- name: BAZ
value: qux
labels:
- app
- version
"#;
let ctx = TypeContext::from_yaml(yaml).unwrap();
assert_eq!(ctx.get_type("controller.extraEnvs"), InferredType::List);
assert_eq!(ctx.get_type("controller.labels"), InferredType::List);
}
#[test]
fn test_path_normalization() {
let yaml = r#"
controller:
replicas: 3
"#;
let ctx = TypeContext::from_yaml(yaml).unwrap();
assert_eq!(ctx.get_type("controller.replicas"), InferredType::Scalar);
assert_eq!(
ctx.get_type("values.controller.replicas"),
InferredType::Scalar
);
assert_eq!(
ctx.get_type(".Values.controller.replicas"),
InferredType::Scalar
);
assert_eq!(
ctx.get_type("Values.controller.replicas"),
InferredType::Scalar
);
}
#[test]
fn test_unknown_path() {
let yaml = r#"
controller:
replicas: 3
"#;
let ctx = TypeContext::from_yaml(yaml).unwrap();
assert_eq!(ctx.get_type("nonexistent"), InferredType::Unknown);
assert_eq!(ctx.get_type("controller.unknown"), InferredType::Unknown);
}
#[test]
fn test_heuristics_dict() {
assert_eq!(
TypeHeuristics::guess_type("controller.annotations"),
Some(InferredType::Dict)
);
assert_eq!(
TypeHeuristics::guess_type("controller.labels"),
Some(InferredType::Dict)
);
assert_eq!(
TypeHeuristics::guess_type("pod.nodeSelector"),
Some(InferredType::Dict)
);
assert_eq!(
TypeHeuristics::guess_type("controller.containerPort"),
Some(InferredType::Dict)
);
}
#[test]
fn test_heuristics_list() {
assert_eq!(
TypeHeuristics::guess_type("spec.containers"),
Some(InferredType::List)
);
assert_eq!(
TypeHeuristics::guess_type("controller.extraVolumes"),
Some(InferredType::List)
);
assert_eq!(
TypeHeuristics::guess_type("pod.imagePullSecrets"),
Some(InferredType::List)
);
}
#[test]
fn test_heuristics_unknown() {
assert_eq!(TypeHeuristics::guess_type("controller.replicas"), None);
assert_eq!(TypeHeuristics::guess_type("custom.field"), None);
}
#[test]
fn test_complex_structure() {
let yaml = r#"
global:
image:
registry: docker.io
controller:
kind: Deployment
hostNetwork: false
containerPort:
http: 80
https: 443
admissionWebhooks:
enabled: true
patch:
image:
registry: registry.k8s.io
image: ingress-nginx/kube-webhook-certgen
tag: v1.4.1
tcp: {}
udp: {}
"#;
let ctx = TypeContext::from_yaml(yaml).unwrap();
assert_eq!(ctx.get_type("global"), InferredType::Dict);
assert_eq!(ctx.get_type("controller"), InferredType::Dict);
assert_eq!(ctx.get_type("tcp"), InferredType::Dict);
assert_eq!(ctx.get_type("udp"), InferredType::Dict);
assert_eq!(ctx.get_type("global.image"), InferredType::Dict);
assert_eq!(ctx.get_type("controller.containerPort"), InferredType::Dict);
assert_eq!(
ctx.get_type("controller.admissionWebhooks.patch.image"),
InferredType::Dict
);
assert_eq!(ctx.get_type("controller.kind"), InferredType::Scalar);
assert_eq!(ctx.get_type("controller.hostNetwork"), InferredType::Scalar);
assert_eq!(
ctx.get_type("controller.admissionWebhooks.enabled"),
InferredType::Scalar
);
}
#[test]
fn test_is_methods() {
assert!(InferredType::Dict.is_dict());
assert!(InferredType::Dict.is_collection());
assert!(!InferredType::Dict.is_list());
assert!(InferredType::List.is_list());
assert!(InferredType::List.is_collection());
assert!(!InferredType::List.is_dict());
assert!(!InferredType::Scalar.is_collection());
assert!(!InferredType::Unknown.is_collection());
}
#[test]
fn test_display() {
assert_eq!(format!("{}", InferredType::Scalar), "scalar");
assert_eq!(format!("{}", InferredType::List), "list");
assert_eq!(format!("{}", InferredType::Dict), "dict");
assert_eq!(format!("{}", InferredType::Unknown), "unknown");
}
#[test]
fn test_len_and_is_empty() {
let empty = TypeContext::new();
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let ctx = TypeContext::from_yaml("foo: bar").unwrap();
assert!(!ctx.is_empty());
assert_eq!(ctx.len(), 1);
}
}