1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! string_newtype {
8 ($(#[$meta:meta])* $name:ident) => {
9 $(#[$meta])*
10 #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
11 pub struct $name(String);
12
13 impl $name {
14 pub fn new(value: impl Into<String>) -> Self {
16 Self(value.into())
17 }
18
19 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23 }
24
25 impl AsRef<str> for $name {
26 fn as_ref(&self) -> &str {
27 self.as_str()
28 }
29 }
30
31 impl From<String> for $name {
32 fn from(value: String) -> Self {
33 Self::new(value)
34 }
35 }
36
37 impl From<&str> for $name {
38 fn from(value: &str) -> Self {
39 Self::new(value)
40 }
41 }
42
43 impl fmt::Display for $name {
44 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45 formatter.write_str(self.as_str())
46 }
47 }
48 };
49}
50
51string_newtype! {
52 DocumentPath
54}
55string_newtype! {
56 PathSegment
58}
59string_newtype! {
60 FieldSelector
62}
63
64impl DocumentPath {
65 pub fn try_new(value: impl AsRef<str>) -> Result<Self, PathParseError> {
67 validate_path(value.as_ref())?;
68 Ok(Self::new(value.as_ref().trim()))
69 }
70
71 pub fn segments(&self) -> Result<Vec<PathSegment>, PathParseError> {
73 parse_segments(self.as_str())
74 }
75}
76
77impl FromStr for DocumentPath {
78 type Err = PathParseError;
79
80 fn from_str(input: &str) -> Result<Self, Self::Err> {
81 Self::try_new(input)
82 }
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
87pub enum PathParseError {
88 EmptyPath,
90 EmptySegment { index: usize },
92}
93
94impl fmt::Display for PathParseError {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 match self {
97 Self::EmptyPath => formatter.write_str("document path must not be empty"),
98 Self::EmptySegment { index } => {
99 write!(
100 formatter,
101 "document path segment at index {index} must not be empty"
102 )
103 },
104 }
105 }
106}
107
108impl Error for PathParseError {}
109
110fn validate_path(input: &str) -> Result<(), PathParseError> {
111 parse_segments(input).map(|_| ())
112}
113
114fn parse_segments(input: &str) -> Result<Vec<PathSegment>, PathParseError> {
115 let trimmed = input.trim();
116 if trimmed.is_empty() {
117 return Err(PathParseError::EmptyPath);
118 }
119
120 let mut segments = Vec::new();
121 for (index, segment) in trimmed.split('.').enumerate() {
122 if segment.is_empty() {
123 return Err(PathParseError::EmptySegment { index });
124 }
125 segments.push(PathSegment::new(segment));
126 }
127
128 Ok(segments)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::{DocumentPath, FieldSelector, PathParseError, PathSegment};
134
135 #[test]
136 fn constructs_and_displays_paths() {
137 let path = DocumentPath::new("profile.display_name");
138 assert_eq!(path.as_str(), "profile.display_name");
139 assert_eq!(path.to_string(), "profile.display_name");
140 assert_eq!(path.as_ref(), "profile.display_name");
141 assert_eq!(DocumentPath::from("profile"), DocumentPath::new("profile"));
142 }
143
144 #[test]
145 fn parses_dot_path_segments() -> Result<(), PathParseError> {
146 let path = DocumentPath::try_new("settings.notifications.email")?;
147 let segments = path.segments()?;
148
149 assert_eq!(
150 segments,
151 vec![
152 PathSegment::new("settings"),
153 PathSegment::new("notifications"),
154 PathSegment::new("email"),
155 ]
156 );
157 assert_eq!(
158 FieldSelector::new("profile.display_name").to_string(),
159 "profile.display_name"
160 );
161
162 Ok(())
163 }
164
165 #[test]
166 fn rejects_invalid_paths() {
167 assert_eq!(DocumentPath::try_new(""), Err(PathParseError::EmptyPath));
168 assert_eq!(
169 DocumentPath::try_new("profile..name"),
170 Err(PathParseError::EmptySegment { index: 1 })
171 );
172 }
173}