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)]
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}