1use crate::ResolveError;
2use std::borrow::Cow;
3use std::fmt;
4
5macro_rules! sections {
8 ($( $(#[$meta:meta])* $variant:ident => $pointer:literal ),+ $(,)?) => {
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15 #[non_exhaustive]
16 pub enum Section {
17 $( $(#[$meta])* $variant, )+
18 }
19
20 impl Section {
21 pub const ALL: &'static [Section] = &[ $( Section::$variant, )+ ];
23
24 pub const fn as_pointer(self) -> &'static str {
26 match self { $( Self::$variant => $pointer, )+ }
27 }
28 }
29 };
30}
31
32sections! {
33 Callbacks => "components/callbacks",
35 Examples => "components/examples",
37 Headers => "components/headers",
39 Links => "components/links",
41 Parameters => "components/parameters",
43 RequestBodies => "components/requestBodies",
45 Responses => "components/responses",
47 Schemas => "components/schemas",
49 SecuritySchemes => "components/securitySchemes",
51 Paths => "paths",
53}
54
55impl Section {
56 fn from_components_segment(segment: &str) -> Option<Self> {
57 Self::ALL
58 .iter()
59 .copied()
60 .find(|section| section.as_pointer().strip_prefix("components/") == Some(segment))
61 }
62}
63
64impl fmt::Display for Section {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.write_str(self.as_pointer())
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub(crate) struct ComponentRef<'a> {
73 pub section: Section,
74 pub name: Cow<'a, str>,
76}
77
78impl<'a> ComponentRef<'a> {
79 pub(crate) fn parse(reference: &'a str) -> Result<Self, ResolveError> {
86 match percent_decode(reference) {
87 Cow::Borrowed(decoded) => Self::parse_decoded(decoded, reference),
88 Cow::Owned(decoded) => {
89 let parsed = Self::parse_decoded(&decoded, reference)?;
90 Ok(ComponentRef {
91 section: parsed.section,
92 name: Cow::Owned(parsed.name.into_owned()),
93 })
94 }
95 }
96 }
97
98 fn parse_decoded<'s>(
99 decoded: &'s str,
100 reference: &str,
101 ) -> Result<ComponentRef<'s>, ResolveError> {
102 let Some(pointer) = decoded.strip_prefix("#/") else {
103 return Err(match decoded.split_once('#') {
104 Some((document, _)) if !document.is_empty() => ResolveError::ExternalDocument {
105 document: document.to_owned(),
106 },
107 _ => ResolveError::NotALocalReference {
108 reference: reference.to_owned(),
109 },
110 });
111 };
112
113 let malformed = || ResolveError::MalformedPointer {
114 reference: reference.to_owned(),
115 };
116 let (root, rest) = pointer.split_once('/').ok_or_else(malformed)?;
117
118 let (section, name) = match root {
119 "components" => {
120 let (segment, name) = rest.split_once('/').ok_or_else(malformed)?;
121 let section = Section::from_components_segment(segment).ok_or_else(|| {
122 ResolveError::UnknownSection {
123 section: segment.to_owned(),
124 }
125 })?;
126 (section, name)
127 }
128 root if root == Section::Paths.as_pointer() => (Section::Paths, rest),
129 other => {
130 return Err(ResolveError::UnsupportedRootSection {
131 section: other.to_owned(),
132 })
133 }
134 };
135
136 if name.contains('/') {
137 return Err(ResolveError::PointerTooDeep {
138 reference: reference.to_owned(),
139 });
140 }
141
142 Ok(ComponentRef {
143 section,
144 name: unescape(name),
145 })
146 }
147}
148
149fn percent_decode(input: &str) -> Cow<'_, str> {
152 if !input.contains('%') {
153 return Cow::Borrowed(input);
154 }
155
156 let mut decoded = Vec::with_capacity(input.len());
157 let mut bytes = input.bytes();
158 while let Some(byte) = bytes.next() {
159 if byte != b'%' {
160 decoded.push(byte);
161 continue;
162 }
163 let mut probe = bytes.clone();
164 match (
165 probe.next().and_then(hex_digit),
166 probe.next().and_then(hex_digit),
167 ) {
168 (Some(high), Some(low)) => {
169 decoded.push(high * 16 + low);
170 bytes = probe;
171 }
172 _ => decoded.push(b'%'),
173 }
174 }
175
176 match String::from_utf8(decoded) {
177 Ok(decoded) => Cow::Owned(decoded),
178 Err(_) => Cow::Borrowed(input),
179 }
180}
181
182fn hex_digit(byte: u8) -> Option<u8> {
183 char::from(byte).to_digit(16).map(|digit| digit as u8)
184}
185
186fn unescape(segment: &str) -> Cow<'_, str> {
189 if !segment.contains('~') {
190 return Cow::Borrowed(segment);
191 }
192
193 let mut unescaped = String::with_capacity(segment.len());
194 let mut characters = segment.chars();
195 while let Some(character) = characters.next() {
196 if character != '~' {
197 unescaped.push(character);
198 continue;
199 }
200 match characters.next() {
201 Some('0') => unescaped.push('~'),
202 Some('1') => unescaped.push('/'),
203 Some(other) => {
204 unescaped.push('~');
205 unescaped.push(other);
206 }
207 None => unescaped.push('~'),
208 }
209 }
210
211 Cow::Owned(unescaped)
212}
213
214pub(crate) fn escape(name: &str) -> Cow<'_, str> {
216 if !name.contains(['~', '/']) {
217 return Cow::Borrowed(name);
218 }
219 Cow::Owned(name.replace('~', "~0").replace('/', "~1"))
220}
221
222#[cfg(test)]
223mod tests {
224 #![allow(clippy::unwrap_used)]
225
226 use super::*;
227
228 #[test]
229 fn every_section_parses_back_from_its_own_pointer() {
230 for section in Section::ALL.iter().copied() {
233 let reference = format!("#/{}/Thing", section.as_pointer());
234 let parsed = ComponentRef::parse(&reference);
235 assert_eq!(
236 parsed,
237 Ok(ComponentRef {
238 section,
239 name: Cow::Borrowed("Thing")
240 })
241 );
242 }
243 }
244
245 #[test]
246 fn section_pointers_use_wire_spelling_not_rust_field_names() {
247 assert_eq!(
248 Section::RequestBodies.as_pointer(),
249 "components/requestBodies"
250 );
251 assert_eq!(
252 Section::SecuritySchemes.as_pointer(),
253 "components/securitySchemes"
254 );
255 }
256
257 #[test]
258 fn borrows_the_name_when_there_is_nothing_to_decode() {
259 let parsed = ComponentRef::parse("#/components/schemas/Pet").unwrap();
260 assert!(matches!(parsed.name, Cow::Borrowed("Pet")));
261 }
262
263 #[test]
264 fn unescapes_a_slash_in_a_path_name() {
265 let parsed = ComponentRef::parse("#/paths/~1pets").unwrap();
266 assert_eq!(parsed.name, "/pets");
267 }
268
269 #[test]
270 fn unescapes_tilde_after_slash_so_escaped_escapes_survive() {
271 let parsed = ComponentRef::parse("#/components/schemas/a~01b").unwrap();
273 assert_eq!(parsed.name, "a~1b");
274 }
275
276 #[test]
277 fn keeps_a_trailing_tilde_that_escapes_nothing() {
278 let parsed = ComponentRef::parse("#/components/schemas/a~").unwrap();
279 assert_eq!(parsed.name, "a~");
280 }
281
282 #[test]
283 fn percent_decodes_the_fragment_before_reading_it_as_a_pointer() {
284 let parsed = ComponentRef::parse("#/paths/~1pets~1%7Bid%7D").unwrap();
287 assert_eq!(parsed.name, "/pets/{id}");
288 }
289
290 #[test]
291 fn percent_decodes_multi_byte_utf8() {
292 let parsed = ComponentRef::parse("#/components/schemas/caf%C3%A9").unwrap();
293 assert_eq!(parsed.name, "café");
294 }
295
296 #[test]
297 fn leaves_an_incomplete_percent_escape_alone() {
298 let parsed = ComponentRef::parse("#/components/schemas/100%25%zz").unwrap();
299 assert_eq!(parsed.name, "100%%zz");
300 }
301
302 #[test]
303 fn escape_round_trips_through_unescape() {
304 for name in ["plain", "/pets/{id}", "a~1b", "~/", "~0"] {
305 assert_eq!(unescape(&escape(name)), name);
306 }
307 }
308
309 #[test]
310 fn escape_borrows_a_name_with_nothing_to_escape() {
311 assert!(matches!(escape("Pet"), Cow::Borrowed("Pet")));
312 }
313
314 #[test]
315 fn rejects_a_reference_without_the_local_prefix() {
316 assert_eq!(
317 ComponentRef::parse("Pet"),
318 Err(ResolveError::NotALocalReference {
319 reference: "Pet".to_owned()
320 })
321 );
322 }
323
324 #[test]
325 fn rejects_a_reference_into_another_document() {
326 assert_eq!(
327 ComponentRef::parse("common.yaml#/components/schemas/Pet"),
328 Err(ResolveError::ExternalDocument {
329 document: "common.yaml".to_owned()
330 })
331 );
332 }
333
334 #[test]
335 fn rejects_a_pointer_with_too_few_segments() {
336 assert_eq!(
337 ComponentRef::parse("#/components/schemas"),
338 Err(ResolveError::MalformedPointer {
339 reference: "#/components/schemas".to_owned()
340 })
341 );
342 }
343
344 #[test]
345 fn rejects_a_root_section_that_cannot_hold_targets() {
346 assert_eq!(
347 ComponentRef::parse("#/info/title"),
348 Err(ResolveError::UnsupportedRootSection {
349 section: "info".to_owned()
350 })
351 );
352 }
353
354 #[test]
355 fn rejects_a_rust_field_name_used_as_a_section() {
356 assert_eq!(
357 ComponentRef::parse("#/components/request_bodies/CreatePet"),
358 Err(ResolveError::UnknownSection {
359 section: "request_bodies".to_owned()
360 })
361 );
362 }
363
364 #[test]
365 fn rejects_a_pointer_into_the_middle_of_a_component() {
366 let reference = "#/components/schemas/Pet/properties/name";
367 assert_eq!(
368 ComponentRef::parse(reference),
369 Err(ResolveError::PointerTooDeep {
370 reference: reference.to_owned()
371 })
372 );
373 }
374}