helm_schema_core/
capability.rs1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
7pub struct HelperBranch {
8 pub guard: Option<CapabilityGuard>,
10 pub body: HelperBranchBody,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum HelperBranchBody {
18 Literals {
21 values: Vec<String>,
23 },
24 Nested {
26 branches: Vec<HelperBranch>,
28 },
29}
30
31impl HelperBranchBody {
32 #[must_use]
34 pub fn literals(values: Vec<String>) -> Self {
35 Self::Literals { values }
36 }
37
38 #[must_use]
40 pub fn is_empty(&self) -> bool {
41 match self {
42 Self::Literals { values } => values.is_empty(),
43 Self::Nested { branches } => branches.iter().all(|branch| branch.body.is_empty()),
44 }
45 }
46
47 #[must_use]
49 pub fn all_literals(&self) -> Vec<String> {
50 let mut out = Vec::new();
51 let mut seen = HashSet::new();
52 self.append_all_literals(&mut out, &mut seen);
53 out
54 }
55
56 pub fn append_all_literals(&self, out: &mut Vec<String>, seen: &mut HashSet<String>) {
58 match self {
59 Self::Literals { values } => {
60 for value in values {
61 if seen.insert(value.clone()) {
62 out.push(value.clone());
63 }
64 }
65 }
66 Self::Nested { branches } => {
67 for branch in branches {
68 branch.body.append_all_literals(out, seen);
69 }
70 }
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
77#[serde(tag = "type", rename_all = "snake_case")]
78pub enum CapabilityGuard {
79 Has {
81 api: String,
83 },
84 NotHas {
86 api: String,
88 },
89 Opaque {
91 text: String,
93 },
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
98pub enum ApiPresenceQuery {
99 Resource {
101 api_version: String,
103 kind: String,
105 },
106 GroupVersion {
108 api_version: String,
110 },
111}
112
113impl ApiPresenceQuery {
114 #[must_use]
116 pub fn parse_helm_literal(api: &str) -> Option<Self> {
117 let parts: Vec<&str> = api.split('/').collect();
118 match parts.as_slice() {
119 [group, version, kind]
120 if !group.is_empty() && !version.is_empty() && !kind.is_empty() =>
121 {
122 Some(Self::Resource {
123 api_version: format!("{group}/{version}"),
124 kind: (*kind).to_string(),
125 })
126 }
127 [version, kind] if is_k8s_api_version_segment(version) && !kind.is_empty() => {
128 Some(Self::Resource {
129 api_version: (*version).to_string(),
130 kind: (*kind).to_string(),
131 })
132 }
133 [api_version] if !api_version.is_empty() => Some(Self::GroupVersion {
134 api_version: (*api_version).to_string(),
135 }),
136 [group, version] if !group.is_empty() && !version.is_empty() => {
137 Some(Self::GroupVersion {
138 api_version: format!("{group}/{version}"),
139 })
140 }
141 _ => None,
142 }
143 }
144
145 #[must_use]
147 pub fn canonical_helm_literal(&self) -> String {
148 match self {
149 Self::Resource { api_version, kind } => format!("{api_version}/{kind}"),
150 Self::GroupVersion { api_version } => api_version.clone(),
151 }
152 }
153}
154
155fn is_k8s_api_version_segment(segment: &str) -> bool {
156 let Some(rest) = segment.strip_prefix('v') else {
157 return false;
158 };
159 let digit_count = rest.chars().take_while(char::is_ascii_digit).count();
160 if digit_count == 0 {
161 return false;
162 }
163 let suffix = &rest[digit_count..];
164 if suffix.is_empty() {
165 return true;
166 }
167 for qualifier in ["alpha", "beta"] {
168 if let Some(number) = suffix.strip_prefix(qualifier) {
169 return !number.is_empty()
170 && number.chars().all(|character| character.is_ascii_digit());
171 }
172 }
173 false
174}