facet_path/lib.rs
1#![warn(missing_docs)]
2#![cfg_attr(not(feature = "std"), no_std)]
3//!
4//! [](https://coveralls.io/github/facet-rs/facet?branch=main)
5//! [](https://crates.io/crates/facet-path)
6//! [](https://docs.rs/facet-path)
7//! [](./LICENSE)
8//! [](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}