Skip to main content

facet_path/
lib.rs

1#![warn(missing_docs)]
2#![cfg_attr(not(feature = "std"), no_std)]
3//!
4//! [![Coverage Status](https://coveralls.io/repos/github/facet-rs/facet-path/badge.svg?branch=main)](https://coveralls.io/github/facet-rs/facet?branch=main)
5//! [![crates.io](https://img.shields.io/crates/v/facet-path.svg)](https://crates.io/crates/facet-path)
6//! [![documentation](https://docs.rs/facet-path/badge.svg)](https://docs.rs/facet-path)
7//! [![MIT/Apache-2.0 licensed](https://img.shields.io/crates/l/facet-path.svg)](./LICENSE)
8//! [![Discord](https://img.shields.io/discord/1379550208551026748?logo=discord&label=discord)](https://discord.gg/JhD7CwCJ8F)
9//!
10//!
11//! Path tracking for navigating Facet type structures.
12//!
13//! This crate provides lightweight path tracking that records navigation steps through a Facet type hierarchy. When an error occurs during serialization or deserialization, the path can be used to produce helpful error messages showing exactly where in the data structure the problem occurred.
14//!
15//! ## Features
16//!
17//! - Lightweight `PathStep` enum that stores indices, not strings
18//! - Reconstruct human-readable paths by replaying steps against a `Shape`
19//! - Optional `pretty` feature for rich error rendering with `facet-pretty`
20//!
21//! ## Usage
22//!
23//! ```rust
24//! use facet::Facet;
25//! use facet_path::{Path, PathStep};
26//!
27//! #[derive(Facet)]
28//! struct Outer {
29//!     items: Vec<Inner>,
30//! }
31//!
32//! #[derive(Facet)]
33//! struct Inner {
34//!     name: String,
35//!     value: u32,
36//! }
37//!
38//! // Build a path during traversal
39//! let mut path = Path::new(<Outer as Facet>::SHAPE);
40//! path.push(PathStep::Field(0));      // "items"
41//! path.push(PathStep::Index(2));       // [2]
42//! path.push(PathStep::Field(0));      // "name"
43//!
44//! // Format the path as a human-readable string
45//! let formatted = path.format();
46//! assert_eq!(formatted, "items[2].name");
47//! ```
48//!
49//! ## Feature Flags
50//!
51//! - `std` (default): Enables standard library support
52//! - `alloc`: Enables heap allocation without full std
53//! - `pretty`: Enables rich error rendering with `facet-pretty`
54//!
55//!
56//!
57#![doc = include_str!("../readme-footer.md")]
58
59extern crate alloc;
60
61use alloc::string::String;
62use alloc::vec::Vec;
63use core::fmt::Write;
64
65use facet_core::{Def, Field, Shape, Type, UserType};
66
67pub mod access;
68pub use access::PathAccessError;
69
70pub mod walk;
71pub use walk::{ShapeVisitor, VisitDecision, WalkStatus, walk_shape};
72
73/// A single step in a path through a type structure.
74///
75/// Each step records an index that can be used to navigate
76/// back through a [`Shape`] to reconstruct field names and types.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78#[non_exhaustive]
79pub enum PathStep {
80    /// Navigate to a struct field by index
81    Field(u32),
82    /// Navigate to a list/array element by index
83    Index(u32),
84    /// Navigate to an enum variant by index
85    Variant(u32),
86    /// Navigate into a map key at a specific entry index.
87    /// The entry index distinguishes paths for different map keys' inner frames.
88    MapKey(u32),
89    /// Navigate into a map value at a specific entry index.
90    /// The entry index distinguishes paths for different map values' inner frames.
91    MapValue(u32),
92    /// Navigate into `Some` of an Option
93    OptionSome,
94    /// Navigate through a pointer/reference
95    Deref,
96    /// Navigate into a transparent inner type (e.g., `NonZero<T>` -> T)
97    Inner,
98    /// Navigate into a proxy type (e.g., `Inner` with `#[facet(proxy = InnerProxy)]`)
99    ///
100    /// This step distinguishes a proxy frame from its parent in the deferred
101    /// processing path, so both can be stored without path collisions.
102    Proxy,
103}
104
105/// A path through a type structure, recorded as a series of steps.
106///
107/// This is a lightweight representation that only stores indices.
108/// The actual field names and type information can be reconstructed
109/// by replaying these steps against the original [`Shape`].
110#[derive(Debug, Clone)]
111pub struct Path {
112    /// The root [`Shape`] from which this path originates.
113    pub shape: &'static Shape,
114
115    /// The sequence of [`PathStep`]s representing navigation through the type structure.
116    pub steps: Vec<PathStep>,
117}
118
119impl PartialEq for Path {
120    fn eq(&self, other: &Self) -> bool {
121        // Compare shapes by pointer address (they're static references)
122        core::ptr::eq(self.shape, other.shape) && self.steps == other.steps
123    }
124}
125
126impl Eq for Path {}
127
128impl PartialOrd for Path {
129    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
130        Some(self.cmp(other))
131    }
132}
133
134impl Ord for Path {
135    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
136        // Compare shapes by pointer address first, then by steps
137        let shape_cmp =
138            (self.shape as *const Shape as usize).cmp(&(other.shape as *const Shape as usize));
139        if shape_cmp != core::cmp::Ordering::Equal {
140            return shape_cmp;
141        }
142        self.steps.cmp(&other.steps)
143    }
144}
145
146impl core::hash::Hash for Path {
147    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
148        // Hash the shape pointer address
149        (self.shape as *const Shape as usize).hash(state);
150        self.steps.hash(state);
151    }
152}
153
154impl Path {
155    /// Create a new empty path.
156    pub const fn new(shape: &'static Shape) -> Self {
157        Self {
158            shape,
159            steps: Vec::new(),
160        }
161    }
162
163    /// Create a path with pre-allocated capacity.
164    pub fn with_capacity(shape: &'static Shape, capacity: usize) -> Self {
165        Self {
166            shape,
167            steps: Vec::with_capacity(capacity),
168        }
169    }
170
171    /// Push a step onto the path.
172    pub fn push(&mut self, step: PathStep) {
173        self.steps.push(step);
174    }
175
176    /// Pop the last step from the path.
177    pub fn pop(&mut self) -> Option<PathStep> {
178        self.steps.pop()
179    }
180
181    /// Get the steps in this path.
182    pub fn steps(&self) -> &[PathStep] {
183        &self.steps
184    }
185
186    /// Get the length of this path.
187    pub const fn len(&self) -> usize {
188        self.steps.len()
189    }
190
191    /// Check if this path is empty.
192    pub const fn is_empty(&self) -> bool {
193        self.steps.is_empty()
194    }
195
196    /// Format this path as a human-readable string using the stored root shape.
197    ///
198    /// Returns a path like `outer.inner.items[3].name`.
199    pub fn format(&self) -> String {
200        self.format_with_shape(self.shape)
201    }
202
203    /// Format this path as a human-readable string by walking the given shape.
204    ///
205    /// Returns a path like `outer.inner.items[3].name`.
206    pub fn format_with_shape(&self, shape: &'static Shape) -> String {
207        let mut result = String::new();
208        let mut current_shape = shape;
209        let mut current_variant_idx: Option<usize> = None;
210
211        for step in &self.steps {
212            match step {
213                PathStep::Field(idx) => {
214                    let idx = *idx as usize;
215                    if let Some(field_name) =
216                        get_field_name_with_variant(current_shape, idx, current_variant_idx)
217                    {
218                        if !result.is_empty() {
219                            result.push('.');
220                        }
221                        result.push_str(field_name);
222                    }
223                    if let Some(field_shape) =
224                        get_field_shape_with_variant(current_shape, idx, current_variant_idx)
225                    {
226                        current_shape = field_shape;
227                    }
228                    current_variant_idx = None;
229                }
230                PathStep::Index(idx) => {
231                    write!(result, "[{}]", idx).unwrap();
232                    if let Some(elem_shape) = get_element_shape(current_shape) {
233                        current_shape = elem_shape;
234                    }
235                    current_variant_idx = None;
236                }
237                PathStep::Variant(idx) => {
238                    let idx = *idx as usize;
239                    if let Some(variant_name) = get_variant_name(current_shape, idx) {
240                        result.push_str("::");
241                        result.push_str(variant_name);
242                    }
243                    // Don't advance current_shape — the next Field step will
244                    // use the variant index to look up fields within the enum.
245                    current_variant_idx = Some(idx);
246                }
247                PathStep::MapKey(idx) => {
248                    write!(result, "[key#{}]", idx).unwrap();
249                    if let Some(key_shape) = get_map_key_shape(current_shape) {
250                        current_shape = key_shape;
251                    }
252                    current_variant_idx = None;
253                }
254                PathStep::MapValue(idx) => {
255                    write!(result, "[value#{}]", idx).unwrap();
256                    if let Some(value_shape) = get_map_value_shape(current_shape) {
257                        current_shape = value_shape;
258                    }
259                    current_variant_idx = None;
260                }
261                PathStep::OptionSome => {
262                    result.push_str("::Some");
263                    if let Some(inner_shape) = get_option_inner_shape(current_shape) {
264                        current_shape = inner_shape;
265                    }
266                    current_variant_idx = None;
267                }
268                PathStep::Deref => {
269                    if let Some(inner_shape) = get_pointer_inner_shape(current_shape) {
270                        current_shape = inner_shape;
271                    }
272                    current_variant_idx = None;
273                }
274                PathStep::Inner => {
275                    if let Some(inner_shape) = get_inner_shape(current_shape) {
276                        current_shape = inner_shape;
277                    }
278                    current_variant_idx = None;
279                }
280                PathStep::Proxy => {
281                    if let Some(proxy_def) = current_shape.effective_proxy(None) {
282                        current_shape = proxy_def.shape;
283                    }
284                    current_variant_idx = None;
285                }
286            }
287        }
288
289        if result.is_empty() {
290            result.push_str("<root>");
291        }
292
293        result
294    }
295
296    /// Resolve the field at the end of this path, if the path ends at a struct field.
297    ///
298    /// This navigates through the given shape following each step in the path,
299    /// and returns the [`Field`] if the final step is a `PathStep::Field`.
300    ///
301    /// This is useful for accessing field metadata like attributes when handling
302    /// errors that occur at a specific field location.
303    ///
304    /// # Returns
305    ///
306    /// - `Some(&Field)` if the path ends at a struct field
307    /// - `None` if the path is empty, doesn't end at a field, or navigation fails
308    pub fn resolve_leaf_field(&self, shape: &'static Shape) -> Option<&'static Field> {
309        if self.steps.is_empty() {
310            return None;
311        }
312
313        let mut current_shape = shape;
314        let mut current_variant_idx: Option<usize> = None;
315
316        // Navigate through all steps except the last one
317        for step in &self.steps[..self.steps.len() - 1] {
318            match step {
319                PathStep::Field(idx) => {
320                    let idx = *idx as usize;
321                    current_shape =
322                        get_field_shape_with_variant(current_shape, idx, current_variant_idx)?;
323                    current_variant_idx = None;
324                }
325                PathStep::Index(_) => {
326                    current_shape = get_element_shape(current_shape)?;
327                    current_variant_idx = None;
328                }
329                PathStep::Variant(idx) => {
330                    // Remember the variant for the next field lookup
331                    current_variant_idx = Some(*idx as usize);
332                }
333                PathStep::MapKey(_) => {
334                    current_shape = get_map_key_shape(current_shape)?;
335                    current_variant_idx = None;
336                }
337                PathStep::MapValue(_) => {
338                    current_shape = get_map_value_shape(current_shape)?;
339                    current_variant_idx = None;
340                }
341                PathStep::OptionSome => {
342                    current_shape = get_option_inner_shape(current_shape)?;
343                    current_variant_idx = None;
344                }
345                PathStep::Deref => {
346                    current_shape = get_pointer_inner_shape(current_shape)?;
347                    current_variant_idx = None;
348                }
349                PathStep::Inner => {
350                    current_shape = get_inner_shape(current_shape)?;
351                    current_variant_idx = None;
352                }
353                PathStep::Proxy => {
354                    let proxy_def = current_shape.effective_proxy(None)?;
355                    current_shape = proxy_def.shape;
356                    current_variant_idx = None;
357                }
358            }
359        }
360
361        // Check if the last step is a field
362        if let Some(PathStep::Field(idx)) = self.steps.last() {
363            let idx = *idx as usize;
364            return get_field_with_variant(current_shape, idx, current_variant_idx);
365        }
366
367        None
368    }
369}
370
371impl core::fmt::Display for Path {
372    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
373        f.write_str(&self.format())
374    }
375}
376
377/// Get the field at the given index, handling both structs and enum variants.
378fn get_field_with_variant(
379    shape: &Shape,
380    idx: usize,
381    variant_idx: Option<usize>,
382) -> Option<&'static Field> {
383    match shape.ty {
384        Type::User(UserType::Struct(sd)) => sd.fields.get(idx),
385        Type::User(UserType::Enum(ed)) => {
386            let variant_idx = variant_idx?;
387            let variant = ed.variants.get(variant_idx)?;
388            variant.data.fields.get(idx)
389        }
390        _ => None,
391    }
392}
393
394/// Get the shape of a field at the given index, handling both structs and enum variants.
395fn get_field_shape_with_variant(
396    shape: &Shape,
397    idx: usize,
398    variant_idx: Option<usize>,
399) -> Option<&'static Shape> {
400    get_field_with_variant(shape, idx, variant_idx).map(|f| f.shape())
401}
402
403/// Get the name of a field at the given index, handling both structs and enum variants.
404fn get_field_name_with_variant(
405    shape: &Shape,
406    idx: usize,
407    variant_idx: Option<usize>,
408) -> Option<&'static str> {
409    get_field_with_variant(shape, idx, variant_idx).map(|f| f.name)
410}
411
412/// Get the element shape for a list/array.
413const fn get_element_shape(shape: &Shape) -> Option<&'static Shape> {
414    match shape.def {
415        Def::List(ld) => Some(ld.t()),
416        Def::Array(ad) => Some(ad.t()),
417        Def::Slice(sd) => Some(sd.t()),
418        _ => None,
419    }
420}
421
422/// Get the name of a variant at the given index.
423fn get_variant_name(shape: &Shape, idx: usize) -> Option<&'static str> {
424    match shape.ty {
425        Type::User(UserType::Enum(ed)) => ed.variants.get(idx).map(|v| v.name),
426        _ => None,
427    }
428}
429
430/// Get the key shape for a map.
431const fn get_map_key_shape(shape: &Shape) -> Option<&'static Shape> {
432    match shape.def {
433        Def::Map(md) => Some(md.k()),
434        _ => None,
435    }
436}
437
438/// Get the value shape for a map.
439const fn get_map_value_shape(shape: &Shape) -> Option<&'static Shape> {
440    match shape.def {
441        Def::Map(md) => Some(md.v()),
442        _ => None,
443    }
444}
445
446/// Get the inner shape for an Option.
447const fn get_option_inner_shape(shape: &Shape) -> Option<&'static Shape> {
448    match shape.def {
449        Def::Option(od) => Some(od.t()),
450        _ => None,
451    }
452}
453
454/// Get the inner shape for a pointer.
455const fn get_pointer_inner_shape(shape: &Shape) -> Option<&'static Shape> {
456    match shape.def {
457        Def::Pointer(pd) => pd.pointee(),
458        _ => None,
459    }
460}
461
462/// Get the inner shape for a transparent type (e.g., `NonZero<T>`).
463const fn get_inner_shape(shape: &Shape) -> Option<&'static Shape> {
464    shape.inner
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_path_step_size() {
473        // PathStep should be 8 bytes (discriminant + u32, aligned)
474        assert_eq!(core::mem::size_of::<PathStep>(), 8);
475    }
476}