skp_validator_core/
path.rs

1//! Field path tracking for nested validation errors.
2//!
3//! Provides [`FieldPath`] and [`PathSegment`] for building paths to fields
4//! in nested structures, arrays, and maps.
5
6use std::fmt;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// A segment of a field path.
12///
13/// Paths can navigate through:
14/// - Named fields in structs
15/// - Numeric indices in arrays/vectors
16/// - String keys in maps
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub enum PathSegment {
20    /// A named field (e.g., `user` in `user.email`)
21    Field(String),
22    /// An array/vector index (e.g., `[0]` in `items[0].name`)
23    Index(usize),
24    /// A map key (e.g., `["key"]` in `metadata["key"].value`)
25    Key(String),
26}
27
28impl PathSegment {
29    /// Create a field segment
30    pub fn field(name: impl Into<String>) -> Self {
31        Self::Field(name.into())
32    }
33
34    /// Create an index segment
35    pub fn index(idx: usize) -> Self {
36        Self::Index(idx)
37    }
38
39    /// Create a key segment
40    pub fn key(key: impl Into<String>) -> Self {
41        Self::Key(key.into())
42    }
43}
44
45impl fmt::Display for PathSegment {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Field(name) => write!(f, "{}", name),
49            Self::Index(idx) => write!(f, "[{}]", idx),
50            Self::Key(key) => write!(f, "[\"{}\"]", key),
51        }
52    }
53}
54
55/// A path to a field in a nested structure.
56///
57/// # Example
58///
59/// ```rust
60/// use skp_validator_core::FieldPath;
61///
62/// // Build path: user.addresses[0].city
63/// let path = FieldPath::new()
64///     .push_field("user")
65///     .push_field("addresses")
66///     .push_index(0)
67///     .push_field("city");
68///
69/// assert_eq!(path.to_string(), "user.addresses[0].city");
70/// ```
71#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73pub struct FieldPath {
74    segments: Vec<PathSegment>,
75}
76
77impl FieldPath {
78    /// Create an empty field path
79    pub fn new() -> Self {
80        Self {
81            segments: Vec::new(),
82        }
83    }
84
85    /// Create a path from a single field name
86    pub fn from_field(name: impl Into<String>) -> Self {
87        Self {
88            segments: vec![PathSegment::Field(name.into())],
89        }
90    }
91
92    /// Check if the path is empty (root level)
93    pub fn is_empty(&self) -> bool {
94        self.segments.is_empty()
95    }
96
97    /// Get the number of segments
98    pub fn len(&self) -> usize {
99        self.segments.len()
100    }
101
102    /// Add a field segment and return self (builder pattern)
103    pub fn push_field(mut self, name: impl Into<String>) -> Self {
104        self.segments.push(PathSegment::Field(name.into()));
105        self
106    }
107
108    /// Add an index segment and return self (builder pattern)
109    pub fn push_index(mut self, idx: usize) -> Self {
110        self.segments.push(PathSegment::Index(idx));
111        self
112    }
113
114    /// Add a key segment and return self (builder pattern)
115    pub fn push_key(mut self, key: impl Into<String>) -> Self {
116        self.segments.push(PathSegment::Key(key.into()));
117        self
118    }
119
120    /// Add a field segment in place
121    pub fn append_field(&mut self, name: impl Into<String>) {
122        self.segments.push(PathSegment::Field(name.into()));
123    }
124
125    /// Add an index segment in place
126    pub fn append_index(&mut self, idx: usize) {
127        self.segments.push(PathSegment::Index(idx));
128    }
129
130    /// Add a key segment in place
131    pub fn append_key(&mut self, key: impl Into<String>) {
132        self.segments.push(PathSegment::Key(key.into()));
133    }
134
135    /// Get the segments as a slice
136    pub fn segments(&self) -> &[PathSegment] {
137        &self.segments
138    }
139
140    /// Get the parent path (without the last segment)
141    pub fn parent(&self) -> Option<Self> {
142        if self.segments.is_empty() {
143            None
144        } else {
145            let mut parent = self.clone();
146            parent.segments.pop();
147            Some(parent)
148        }
149    }
150
151    /// Get the last segment (leaf field name)
152    pub fn last(&self) -> Option<&PathSegment> {
153        self.segments.last()
154    }
155
156    /// Get the field name of the last segment if it's a Field
157    pub fn last_field_name(&self) -> Option<&str> {
158        match self.last() {
159            Some(PathSegment::Field(name)) => Some(name),
160            _ => None,
161        }
162    }
163
164    /// Create a child path with a field segment
165    pub fn child_field(&self, name: impl Into<String>) -> Self {
166        self.clone().push_field(name)
167    }
168
169    /// Create a child path with an index segment
170    pub fn child_index(&self, idx: usize) -> Self {
171        self.clone().push_index(idx)
172    }
173
174    /// Create a child path with a key segment
175    pub fn child_key(&self, key: impl Into<String>) -> Self {
176        self.clone().push_key(key)
177    }
178
179    /// Convert to a dot-notation string
180    pub fn to_dot_notation(&self) -> String {
181        self.to_string()
182    }
183
184    /// Convert to a JSON pointer string (RFC 6901)
185    pub fn to_json_pointer(&self) -> String {
186        if self.segments.is_empty() {
187            return String::new();
188        }
189
190        let mut pointer = String::new();
191        for segment in &self.segments {
192            pointer.push('/');
193            match segment {
194                PathSegment::Field(name) => {
195                    // Escape ~ and / per RFC 6901
196                    let escaped = name.replace('~', "~0").replace('/', "~1");
197                    pointer.push_str(&escaped);
198                }
199                PathSegment::Index(idx) => {
200                    pointer.push_str(&idx.to_string());
201                }
202                PathSegment::Key(key) => {
203                    let escaped = key.replace('~', "~0").replace('/', "~1");
204                    pointer.push_str(&escaped);
205                }
206            }
207        }
208        pointer
209    }
210}
211
212impl fmt::Display for FieldPath {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        for (i, segment) in self.segments.iter().enumerate() {
215            match segment {
216                PathSegment::Field(name) => {
217                    if i > 0 {
218                        // Always add dot before a field if there's a previous segment
219                        write!(f, ".")?;
220                    }
221                    write!(f, "{}", name)?;
222                }
223                PathSegment::Index(idx) => {
224                    write!(f, "[{}]", idx)?;
225                }
226                PathSegment::Key(key) => {
227                    write!(f, "[\"{}\"]", key)?;
228                }
229            }
230        }
231        Ok(())
232    }
233}
234
235impl From<&str> for FieldPath {
236    fn from(s: &str) -> Self {
237        Self::from_field(s)
238    }
239}
240
241impl From<String> for FieldPath {
242    fn from(s: String) -> Self {
243        Self::from_field(s)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_simple_path() {
253        let path = FieldPath::from_field("email");
254        assert_eq!(path.to_string(), "email");
255    }
256
257    #[test]
258    fn test_nested_path() {
259        let path = FieldPath::new()
260            .push_field("user")
261            .push_field("address")
262            .push_field("city");
263        assert_eq!(path.to_string(), "user.address.city");
264    }
265
266    #[test]
267    fn test_array_path() {
268        let path = FieldPath::new()
269            .push_field("items")
270            .push_index(0)
271            .push_field("name");
272        assert_eq!(path.to_string(), "items[0].name");
273    }
274
275    #[test]
276    fn test_map_path() {
277        let path = FieldPath::new()
278            .push_field("metadata")
279            .push_key("custom")
280            .push_field("value");
281        assert_eq!(path.to_string(), "metadata[\"custom\"].value");
282    }
283
284    #[test]
285    fn test_json_pointer() {
286        let path = FieldPath::new()
287            .push_field("user")
288            .push_field("addresses")
289            .push_index(0)
290            .push_field("city");
291        assert_eq!(path.to_json_pointer(), "/user/addresses/0/city");
292    }
293
294    #[test]
295    fn test_parent() {
296        let path = FieldPath::new().push_field("user").push_field("email");
297        let parent = path.parent().unwrap();
298        assert_eq!(parent.to_string(), "user");
299    }
300}