1use super::constants::DEFS_KEY;
2use regex::Regex;
3use std::fmt::Display;
4use std::str::FromStr;
5
6lazy_static::lazy_static! {
7 static ref REF_PATTERN: Regex = Regex::new(r"^(?:((?:/\w+)*)/(\w+))?(?:#/\$defs/(\w+))?$").unwrap();
8}
9
10#[derive(Copy, Clone, Debug, Eq, PartialEq)]
11pub enum UnsupportedRefReason {
12 HasScheme,
13 HasQuery,
14 InvalidStructure,
15}
16
17#[derive(thiserror::Error, Debug)]
18pub enum Error {
19 #[error("Unsupported reference")]
20 UnsupportedRef {
21 reason: UnsupportedRefReason,
22 value: String,
23 },
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub enum Reference {
28 PathOnly {
29 path_prefix: Vec<String>,
30 path_name: String,
31 },
32 FragmentOnly {
33 fragment_name: String,
34 },
35 Both {
36 path_prefix: Vec<String>,
37 path_name: String,
38 fragment_name: String,
39 },
40}
41
42impl Reference {
43 pub fn new(path_prefix: Vec<String>, path_name: String, fragment_name: String) -> Self {
44 Reference::Both {
45 path_prefix,
46 path_name,
47 fragment_name,
48 }
49 }
50
51 pub fn from_path(path_prefix: Vec<String>, path_name: String) -> Self {
52 Reference::PathOnly {
53 path_prefix,
54 path_name,
55 }
56 }
57
58 pub fn from_fragment_name(fragment_name: String) -> Self {
59 Reference::FragmentOnly { fragment_name }
60 }
61
62 pub fn path(&self) -> Option<String> {
63 match self {
64 Self::PathOnly { .. } => Some(format!("{}", self)),
65 Self::Both {
66 path_prefix,
67 path_name,
68 ..
69 } => Self::from_path(path_prefix.clone(), path_name.clone()).path(),
70 Self::FragmentOnly { .. } => None,
71 }
72 }
73
74 pub fn name(&self) -> &str {
75 match self {
76 Self::PathOnly { path_name, .. } => path_name,
77 Self::Both { fragment_name, .. } => fragment_name,
78 Self::FragmentOnly { fragment_name } => fragment_name,
79 }
80 }
81}
82
83impl Display for Reference {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::PathOnly {
87 path_prefix,
88 path_name,
89 } => {
90 for part in path_prefix {
91 write!(f, "/{}", part)?;
92 }
93 write!(f, "/{}", path_name)
94 }
95 Self::FragmentOnly { fragment_name } => {
96 write!(f, "#/{}/{}", DEFS_KEY, fragment_name)
97 }
98 Self::Both {
99 path_prefix,
100 path_name,
101 fragment_name,
102 } => {
103 for part in path_prefix {
104 write!(f, "/{}", part)?;
105 }
106 write!(f, "/{}#/{}/{}", path_name, DEFS_KEY, fragment_name)
107 }
108 }
109 }
110}
111
112impl FromStr for Reference {
113 type Err = Error;
114
115 fn from_str(s: &str) -> Result<Self, Self::Err> {
116 if !s.starts_with('/') && !s.starts_with('#') {
117 Err(Error::UnsupportedRef {
118 reason: UnsupportedRefReason::HasScheme,
119 value: s.to_string(),
120 })
121 } else if s.contains('?') {
122 Err(Error::UnsupportedRef {
123 reason: UnsupportedRefReason::HasQuery,
124 value: s.to_string(),
125 })
126 } else {
127 let parts = REF_PATTERN
128 .captures(s)
129 .map(|captures| {
130 captures
131 .iter()
132 .skip(1)
133 .map(|value| value.map(|value| value.as_str()))
134 .collect::<Vec<_>>()
135 })
136 .ok_or_else(|| Error::UnsupportedRef {
137 reason: UnsupportedRefReason::InvalidStructure,
138 value: s.to_string(),
139 })?;
140
141 match parts.as_slice() {
142 [Some(path_prefix), Some(path_name), Some(fragment_name)] => Ok(Reference::new(
143 path_prefix
144 .split('/')
145 .skip(1)
146 .map(|value| value.to_string())
147 .collect(),
148 path_name.to_string(),
149 fragment_name.to_string(),
150 )),
151 [Some(path_prefix), Some(path_name), None] => Ok(Reference::from_path(
152 path_prefix
153 .split('/')
154 .skip(1)
155 .map(|value| value.to_string())
156 .collect(),
157 path_name.to_string(),
158 )),
159 [None, None, Some(fragment_name)] => {
160 Ok(Reference::from_fragment_name(fragment_name.to_string()))
161 }
162 _ => Err(Error::UnsupportedRef {
163 reason: UnsupportedRefReason::InvalidStructure,
164 value: s.to_string(),
165 }),
166 }
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn pairs() -> Vec<(&'static str, Reference)> {
176 vec![
177 (
178 "/foo/bar/baz#/$defs/qux",
179 Reference::new(
180 vec!["foo".to_string(), "bar".to_string()],
181 "baz".to_string(),
182 "qux".to_string(),
183 ),
184 ),
185 (
186 "/foo/bar/baz",
187 Reference::from_path(
188 vec!["foo".to_string(), "bar".to_string()],
189 "baz".to_string(),
190 ),
191 ),
192 (
193 "#/$defs/qux",
194 Reference::from_fragment_name("qux".to_string()),
195 ),
196 ]
197 }
198
199 #[test]
200 fn ref_parse() {
201 for (input, expected) in pairs() {
202 assert_eq!(input.parse::<Reference>().unwrap(), expected);
203 }
204 }
205
206 #[test]
207 fn ref_display() {
208 for (input, parsed) in pairs() {
209 assert_eq!(input, parsed.to_string());
210 }
211 }
212}