json_schema_tools/
reference.rs

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}