Skip to main content

helm_schema_core/
value_path.rs

1use std::cmp::Ordering;
2use std::hash::{Hash, Hasher};
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6/// A structural path beneath Helm's `.Values` root.
7///
8/// Segments are the semantic representation. Encoding is explicit because
9/// dots and backslashes in literal keys must not become selector boundaries.
10#[derive(Clone, Debug, Default, Eq)]
11pub struct ValuesPath {
12    segments: Vec<Segment>,
13}
14
15/// One structural component of a [`ValuesPath`].
16#[derive(Clone, Debug, Eq, Hash, PartialEq)]
17pub enum Segment {
18    /// A chart-authored mapping key, including a literal `*` key.
19    Literal(String),
20    /// Every member reached by a Helm `range`.
21    EachMember,
22}
23
24impl PartialOrd for Segment {
25    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
26        Some(self.cmp(other))
27    }
28}
29
30impl Ord for Segment {
31    fn cmp(&self, other: &Self) -> Ordering {
32        self.encode_component().cmp(&other.encode_component())
33    }
34}
35
36impl Segment {
37    /// Returns the chart-authored key, or `None` for a member wildcard.
38    #[must_use]
39    pub fn literal(&self) -> Option<&str> {
40        match self {
41            Self::Literal(value) => Some(value),
42            Self::EachMember => None,
43        }
44    }
45
46    /// Reports whether this segment selects every ranged member.
47    #[must_use]
48    pub const fn is_each_member(&self) -> bool {
49        matches!(self, Self::EachMember)
50    }
51
52    /// Encodes this segment for legacy string-based phase boundaries.
53    #[must_use]
54    pub fn encode_component(&self) -> String {
55        match self {
56            Self::Literal(value) if value == "*" => r"\*".to_string(),
57            Self::Literal(value) => value.replace('\\', r"\\"),
58            Self::EachMember => "*".to_string(),
59        }
60    }
61
62    /// Decodes one component emitted by [`Self::encode_component`].
63    #[must_use]
64    pub fn from_encoded_component(encoded: &str) -> Self {
65        if encoded == "*" {
66            return Self::EachMember;
67        }
68        let mut literal = String::new();
69        let mut characters = encoded.chars().peekable();
70        while let Some(character) = characters.next() {
71            if character == '\\'
72                && characters
73                    .peek()
74                    .is_some_and(|next| matches!(next, '\\' | '*'))
75            {
76                if let Some(escaped) = characters.next() {
77                    literal.push(escaped);
78                }
79            } else {
80                literal.push(character);
81            }
82        }
83        Self::Literal(literal)
84    }
85}
86
87impl From<String> for Segment {
88    fn from(value: String) -> Self {
89        Self::Literal(value)
90    }
91}
92
93impl From<&str> for Segment {
94    fn from(value: &str) -> Self {
95        Self::Literal(value.to_string())
96    }
97}
98
99impl From<&Segment> for Segment {
100    fn from(value: &Segment) -> Self {
101        value.clone()
102    }
103}
104
105impl ValuesPath {
106    /// Parses the legacy escaped-dot path spelling into structural segments.
107    #[must_use]
108    pub fn parse(path: &str) -> Self {
109        let mut segments = Vec::new();
110        let mut segment = String::new();
111        let mut escaped_star = false;
112        let mut characters = path.chars().peekable();
113        while let Some(character) = characters.next() {
114            match character {
115                '.' => {
116                    push_parsed_segment(&mut segments, &mut segment, &mut escaped_star);
117                }
118                '\\' if characters
119                    .peek()
120                    .is_some_and(|next| matches!(next, '.' | '\\' | '*')) =>
121                {
122                    if let Some(escaped) = characters.next() {
123                        escaped_star |= escaped == '*';
124                        segment.push(escaped);
125                    }
126                }
127                _ => segment.push(character),
128            }
129        }
130        push_parsed_segment(&mut segments, &mut segment, &mut escaped_star);
131        Self { segments }
132    }
133
134    /// Constructs a path from literal structural segments.
135    #[must_use]
136    pub fn from_segments<I, S>(segments: I) -> Self
137    where
138        I: IntoIterator<Item = S>,
139        S: Into<Segment>,
140    {
141        Self {
142            segments: segments
143                .into_iter()
144                .map(Into::into)
145                .filter(|segment| !matches!(segment, Segment::Literal(value) if value.is_empty()))
146                .collect(),
147        }
148    }
149
150    /// Iterates structural segments from root to leaf.
151    #[must_use]
152    pub fn segments(&self) -> impl DoubleEndedIterator<Item = &Segment> + ExactSizeIterator {
153        self.segments.iter()
154    }
155
156    /// Encodes the path in the stable escaped-dot wire spelling.
157    #[must_use]
158    pub fn encode(&self) -> String {
159        let mut encoded = String::new();
160        for (index, segment) in self.segments.iter().enumerate() {
161            if index != 0 {
162                encoded.push('.');
163            }
164            match segment {
165                Segment::EachMember => encoded.push('*'),
166                Segment::Literal(value) if value == "*" => encoded.push_str(r"\*"),
167                Segment::Literal(value) => {
168                    for character in value.chars() {
169                        if matches!(character, '.' | '\\') {
170                            encoded.push('\\');
171                        }
172                        encoded.push(character);
173                    }
174                }
175            }
176        }
177        encoded
178    }
179
180    /// Returns the strict structural parent, if this path is non-root.
181    #[must_use]
182    pub fn parent(&self) -> Option<Self> {
183        let (_, parent) = self.segments.split_last()?;
184        Some(Self {
185            segments: parent.to_vec(),
186        })
187    }
188
189    /// Appends one literal segment. Empty segments preserve legacy no-op behavior.
190    pub fn push(&mut self, segment: impl Into<String>) {
191        let segment = segment.into();
192        if !segment.is_empty() {
193            self.segments.push(Segment::Literal(segment));
194        }
195    }
196
197    /// Appends the structural marker for every ranged member.
198    pub fn push_each_member(&mut self) {
199        self.segments.push(Segment::EachMember);
200    }
201
202    /// Reports whether this path is a strict descendant of `ancestor`.
203    #[must_use]
204    pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
205        self.segments.len() > ancestor.segments.len()
206            && self.segments.starts_with(&ancestor.segments)
207    }
208
209    /// Returns the collection path for a trailing member segment.
210    #[must_use]
211    pub fn item_parent(&self) -> Option<Self> {
212        (self.segments.last() == Some(&Segment::EachMember))
213            .then(|| self.parent())
214            .flatten()
215    }
216}
217
218fn push_parsed_segment(segments: &mut Vec<Segment>, segment: &mut String, escaped_star: &mut bool) {
219    if segment.is_empty() {
220        *escaped_star = false;
221        return;
222    }
223    let segment = std::mem::take(segment);
224    if segment == "*" && !*escaped_star {
225        segments.push(Segment::EachMember);
226    } else {
227        segments.push(Segment::Literal(segment));
228    }
229    *escaped_star = false;
230}
231
232impl PartialEq for ValuesPath {
233    fn eq(&self, other: &Self) -> bool {
234        self.segments == other.segments
235    }
236}
237
238impl PartialOrd for ValuesPath {
239    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
240        Some(self.cmp(other))
241    }
242}
243
244impl Ord for ValuesPath {
245    fn cmp(&self, other: &Self) -> Ordering {
246        self.encode().cmp(&other.encode())
247    }
248}
249
250impl Hash for ValuesPath {
251    fn hash<H: Hasher>(&self, state: &mut H) {
252        self.segments.hash(state);
253    }
254}
255
256impl Serialize for ValuesPath {
257    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
258    where
259        S: Serializer,
260    {
261        serializer.serialize_str(&self.encode())
262    }
263}
264
265impl<'de> Deserialize<'de> for ValuesPath {
266    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
267    where
268        D: Deserializer<'de>,
269    {
270        String::deserialize(deserializer).map(|path| Self::parse(&path))
271    }
272}
273
274/// Joins structural `.Values` path segments into the contract path currency.
275///
276/// Dots and backslashes inside a segment are escaped so literal YAML keys
277/// remain distinct from selector boundaries.
278#[must_use]
279pub fn join_value_path<I, S>(segments: I) -> String
280where
281    I: IntoIterator<Item = S>,
282    S: AsRef<str>,
283{
284    ValuesPath::from_segments(
285        segments
286            .into_iter()
287            .map(|segment| segment.as_ref().to_string()),
288    )
289    .encode()
290}
291
292/// Joins components previously emitted by [`Segment::encode_component`].
293#[must_use]
294pub fn join_encoded_value_path<I, S>(segments: I) -> String
295where
296    I: IntoIterator<Item = S>,
297    S: AsRef<str>,
298{
299    ValuesPath::from_segments(
300        segments
301            .into_iter()
302            .map(|segment| Segment::from_encoded_component(segment.as_ref())),
303    )
304    .encode()
305}
306
307/// Splits the contract `.Values` path currency into structural segments.
308#[must_use]
309pub fn split_value_path(path: &str) -> Vec<String> {
310    ValuesPath::parse(path)
311        .segments
312        .into_iter()
313        .map(|segment| match segment {
314            Segment::Literal(value) => value,
315            Segment::EachMember => "*".to_string(),
316        })
317        .collect()
318}
319
320/// Appends one structural segment to an encoded `.Values` path.
321#[must_use]
322pub fn append_value_path(path: &str, segment: &str) -> String {
323    let mut path = ValuesPath::parse(path);
324    path.push(segment);
325    path.encode()
326}
327
328/// Appends one ranged-member segment to an encoded `.Values` path.
329#[must_use]
330pub fn append_each_member_value_path(path: &str) -> String {
331    let mut path = ValuesPath::parse(path);
332    path.push_each_member();
333    path.encode()
334}