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)]
78pub enum PathStep {
79    /// Navigate to a struct field by index
80    Field(u32),
81    /// Navigate to a list/array element by index
82    Index(u32),
83    /// Navigate to an enum variant by index
84    Variant(u32),
85    /// Navigate into a map key at a specific entry index.
86    /// The entry index distinguishes paths for different map keys' inner frames.
87    MapKey(u32),
88    /// Navigate into a map value at a specific entry index.
89    /// The entry index distinguishes paths for different map values' inner frames.
90    MapValue(u32),
91    /// Navigate into `Some` of an Option
92    OptionSome,
93    /// Navigate through a pointer/reference
94    Deref,
95    /// Navigate into a transparent inner type (e.g., `NonZero<T>` -> T)
96    Inner,
97    /// Navigate into a proxy type (e.g., `Inner` with `#[facet(proxy = InnerProxy)]`)
98    ///
99    /// This step distinguishes a proxy frame from its parent in the deferred
100    /// processing path, so both can be stored without path collisions.
101    Proxy,
102}
103
104/// A path through a type structure, recorded as a series of steps.
105///
106/// This is a lightweight representation that only stores indices.
107/// The actual field names and type information can be reconstructed
108/// by replaying these steps against the original [`Shape`].
109#[derive(Debug, Clone)]
110pub struct Path {
111    /// The root [`Shape`] from which this path originates.
112    pub shape: &'static Shape,
113
114    /// The sequence of [`PathStep`]s representing navigation through the type structure.
115    pub steps: Vec<PathStep>,
116}
117
118impl PartialEq for Path {
119    fn eq(&self, other: &Self) -> bool {
120        // Compare shapes by pointer address (they're static references)
121        core::ptr::eq(self.shape, other.shape) && self.steps == other.steps
122    }
123}
124
125impl Eq for Path {}
126
127impl PartialOrd for Path {
128    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
129        Some(self.cmp(other))
130    }
131}
132
133impl Ord for Path {
134    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
135        // Compare shapes by pointer address first, then by steps
136        let shape_cmp =
137            (self.shape as *const Shape as usize).cmp(&(other.shape as *const Shape as usize));
138        if shape_cmp != core::cmp::Ordering::Equal {
139            return shape_cmp;
140        }
141        self.steps.cmp(&other.steps)
142    }
143}
144
145impl core::hash::Hash for Path {
146    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
147        // Hash the shape pointer address
148        (self.shape as *const Shape as usize).hash(state);
149        self.steps.hash(state);
150    }
151}
152
153impl Path {
154    /// Create a new empty path.
155    pub const fn new(shape: &'static Shape) -> Self {
156        Self {
157            shape,
158            steps: Vec::new(),
159        }
160    }
161
162    /// Create a path with pre-allocated capacity.
163    pub fn with_capacity(shape: &'static Shape, capacity: usize) -> Self {
164        Self {
165            shape,
166            steps: Vec::with_capacity(capacity),
167        }
168    }
169
170    /// Push a step onto the path.
171    pub fn push(&mut self, step: PathStep) {
172        self.steps.push(step);
173    }
174
175    /// Pop the last step from the path.
176    pub fn pop(&mut self) -> Option<PathStep> {
177        self.steps.pop()
178    }
179
180    /// Get the steps in this path.
181    pub fn steps(&self) -> &[PathStep] {
182        &self.steps
183    }
184
185    /// Get the length of this path.
186    pub const fn len(&self) -> usize {
187        self.steps.len()
188    }
189
190    /// Check if this path is empty.
191    pub const fn is_empty(&self) -> bool {
192        self.steps.is_empty()
193    }
194
195    /// Format this path as a human-readable string using the stored root shape.
196    ///
197    /// Returns a path like `outer.inner.items[3].name`.
198    pub fn format(&self) -> String {
199        self.format_with_shape(self.shape)
200    }
201
202    /// Format this path as a human-readable string by walking the given shape.
203    ///
204    /// Returns a path like `outer.inner.items[3].name`.
205    pub fn format_with_shape(&self, shape: &'static Shape) -> String {
206        let mut result = String::new();
207        let mut current_shape = shape;
208        let mut current_variant_idx: Option<usize> = None;
209
210        for step in &self.steps {
211            match step {
212                PathStep::Field(idx) => {
213                    let idx = *idx as usize;
214                    if let Some(field_name) =
215                        get_field_name_with_variant(current_shape, idx, current_variant_idx)
216                    {
217                        if !result.is_empty() {
218                            result.push('.');
219                        }
220                        result.push_str(field_name);
221                    }
222                    if let Some(field_shape) =
223                        get_field_shape_with_variant(current_shape, idx, current_variant_idx)
224                    {
225                        current_shape = field_shape;
226                    }
227                    current_variant_idx = None;
228                }
229                PathStep::Index(idx) => {
230                    write!(result, "[{}]", idx).unwrap();
231                    if let Some(elem_shape) = get_element_shape(current_shape) {
232                        current_shape = elem_shape;
233                    }
234                    current_variant_idx = None;
235                }
236                PathStep::Variant(idx) => {
237                    let idx = *idx as usize;
238                    if let Some(variant_name) = get_variant_name(current_shape, idx) {
239                        result.push_str("::");
240                        result.push_str(variant_name);
241                    }
242                    // Don't advance current_shape — the next Field step will
243                    // use the variant index to look up fields within the enum.
244                    current_variant_idx = Some(idx);
245                }
246                PathStep::MapKey(idx) => {
247                    write!(result, "[key#{}]", idx).unwrap();
248                    if let Some(key_shape) = get_map_key_shape(current_shape) {
249                        current_shape = key_shape;
250                    }
251                    current_variant_idx = None;
252                }
253                PathStep::MapValue(idx) => {
254                    write!(result, "[value#{}]", idx).unwrap();
255                    if let Some(value_shape) = get_map_value_shape(current_shape) {
256                        current_shape = value_shape;
257                    }
258                    current_variant_idx = None;
259                }
260                PathStep::OptionSome => {
261                    result.push_str("::Some");
262                    if let Some(inner_shape) = get_option_inner_shape(current_shape) {
263                        current_shape = inner_shape;
264                    }
265                    current_variant_idx = None;
266                }
267                PathStep::Deref => {
268                    if let Some(inner_shape) = get_pointer_inner_shape(current_shape) {
269                        current_shape = inner_shape;
270                    }
271                    current_variant_idx = None;
272                }
273                PathStep::Inner => {
274                    if let Some(inner_shape) = get_inner_shape(current_shape) {
275                        current_shape = inner_shape;
276                    }
277                    current_variant_idx = None;
278                }
279                PathStep::Proxy => {
280                    if let Some(proxy_def) = current_shape.effective_proxy(None) {
281                        current_shape = proxy_def.shape;
282                    }
283                    current_variant_idx = None;
284                }
285            }
286        }
287
288        if result.is_empty() {
289            result.push_str("<root>");
290        }
291
292        result
293    }
294
295    /// Resolve the field at the end of this path, if the path ends at a struct field.
296    ///
297    /// This navigates through the given shape following each step in the path,
298    /// and returns the [`Field`] if the final step is a `PathStep::Field`.
299    ///
300    /// This is useful for accessing field metadata like attributes when handling
301    /// errors that occur at a specific field location.
302    ///
303    /// # Returns
304    ///
305    /// - `Some(&Field)` if the path ends at a struct field
306    /// - `None` if the path is empty, doesn't end at a field, or navigation fails
307    pub fn resolve_leaf_field(&self, shape: &'static Shape) -> Option<&'static Field> {
308        if self.steps.is_empty() {
309            return None;
310        }
311
312        let mut current_shape = shape;
313        let mut current_variant_idx: Option<usize> = None;
314
315        // Navigate through all steps except the last one
316        for step in &self.steps[..self.steps.len() - 1] {
317            match step {
318                PathStep::Field(idx) => {
319                    let idx = *idx as usize;
320                    current_shape =
321                        get_field_shape_with_variant(current_shape, idx, current_variant_idx)?;
322                    current_variant_idx = None;
323                }
324                PathStep::Index(_) => {
325                    current_shape = get_element_shape(current_shape)?;
326                    current_variant_idx = None;
327                }
328                PathStep::Variant(idx) => {
329                    // Remember the variant for the next field lookup
330                    current_variant_idx = Some(*idx as usize);
331                }
332                PathStep::MapKey(_) => {
333                    current_shape = get_map_key_shape(current_shape)?;
334                    current_variant_idx = None;
335                }
336                PathStep::MapValue(_) => {
337                    current_shape = get_map_value_shape(current_shape)?;
338                    current_variant_idx = None;
339                }
340                PathStep::OptionSome => {
341                    current_shape = get_option_inner_shape(current_shape)?;
342                    current_variant_idx = None;
343                }
344                PathStep::Deref => {
345                    current_shape = get_pointer_inner_shape(current_shape)?;
346                    current_variant_idx = None;
347                }
348                PathStep::Inner => {
349                    current_shape = get_inner_shape(current_shape)?;
350                    current_variant_idx = None;
351                }
352                PathStep::Proxy => {
353                    let proxy_def = current_shape.effective_proxy(None)?;
354                    current_shape = proxy_def.shape;
355                    current_variant_idx = None;
356                }
357            }
358        }
359
360        // Check if the last step is a field
361        if let Some(PathStep::Field(idx)) = self.steps.last() {
362            let idx = *idx as usize;
363            return get_field_with_variant(current_shape, idx, current_variant_idx);
364        }
365
366        None
367    }
368}
369
370impl core::fmt::Display for Path {
371    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
372        f.write_str(&self.format())
373    }
374}
375
376/// Get the field at the given index, handling both structs and enum variants.
377fn get_field_with_variant(
378    shape: &Shape,
379    idx: usize,
380    variant_idx: Option<usize>,
381) -> Option<&'static Field> {
382    match shape.ty {
383        Type::User(UserType::Struct(sd)) => sd.fields.get(idx),
384        Type::User(UserType::Enum(ed)) => {
385            let variant_idx = variant_idx?;
386            let variant = ed.variants.get(variant_idx)?;
387            variant.data.fields.get(idx)
388        }
389        _ => None,
390    }
391}
392
393/// Get the shape of a field at the given index, handling both structs and enum variants.
394fn get_field_shape_with_variant(
395    shape: &Shape,
396    idx: usize,
397    variant_idx: Option<usize>,
398) -> Option<&'static Shape> {
399    get_field_with_variant(shape, idx, variant_idx).map(|f| f.shape())
400}
401
402/// Get the name of a field at the given index, handling both structs and enum variants.
403fn get_field_name_with_variant(
404    shape: &Shape,
405    idx: usize,
406    variant_idx: Option<usize>,
407) -> Option<&'static str> {
408    get_field_with_variant(shape, idx, variant_idx).map(|f| f.name)
409}
410
411/// Get the element shape for a list/array.
412const fn get_element_shape(shape: &Shape) -> Option<&'static Shape> {
413    match shape.def {
414        Def::List(ld) => Some(ld.t()),
415        Def::Array(ad) => Some(ad.t()),
416        Def::Slice(sd) => Some(sd.t()),
417        _ => None,
418    }
419}
420
421/// Get the name of a variant at the given index.
422fn get_variant_name(shape: &Shape, idx: usize) -> Option<&'static str> {
423    match shape.ty {
424        Type::User(UserType::Enum(ed)) => ed.variants.get(idx).map(|v| v.name),
425        _ => None,
426    }
427}
428
429/// Get the key shape for a map.
430const fn get_map_key_shape(shape: &Shape) -> Option<&'static Shape> {
431    match shape.def {
432        Def::Map(md) => Some(md.k()),
433        _ => None,
434    }
435}
436
437/// Get the value shape for a map.
438const fn get_map_value_shape(shape: &Shape) -> Option<&'static Shape> {
439    match shape.def {
440        Def::Map(md) => Some(md.v()),
441        _ => None,
442    }
443}
444
445/// Get the inner shape for an Option.
446const fn get_option_inner_shape(shape: &Shape) -> Option<&'static Shape> {
447    match shape.def {
448        Def::Option(od) => Some(od.t()),
449        _ => None,
450    }
451}
452
453/// Get the inner shape for a pointer.
454const fn get_pointer_inner_shape(shape: &Shape) -> Option<&'static Shape> {
455    match shape.def {
456        Def::Pointer(pd) => pd.pointee(),
457        _ => None,
458    }
459}
460
461/// Get the inner shape for a transparent type (e.g., `NonZero<T>`).
462const fn get_inner_shape(shape: &Shape) -> Option<&'static Shape> {
463    shape.inner
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_path_step_size() {
472        // PathStep should be 8 bytes (discriminant + u32, aligned)
473        assert_eq!(core::mem::size_of::<PathStep>(), 8);
474    }
475}