1use std::fmt;
3use std::str;
4
5use apollo_compiler::Name;
6use apollo_compiler::name;
7use thiserror::Error;
8
9use crate::error::FederationError;
10use crate::error::SingleFederationError;
11
12pub const APOLLO_SPEC_DOMAIN: &str = "https://specs.apollo.dev";
13
14#[derive(Error, Debug, PartialEq)]
15pub enum SpecError {
16 #[error("Parse error: {0}")]
17 ParseError(String),
18}
19
20impl From<SpecError> for FederationError {
22 fn from(value: SpecError) -> Self {
23 SingleFederationError::InvalidLinkIdentifier {
24 message: value.to_string(),
25 }
26 .into()
27 }
28}
29
30#[derive(Clone, PartialEq, Eq, Hash, Debug)]
32pub struct Identity {
33 pub domain: String,
36
37 pub name: Name,
40}
41
42impl fmt::Display for Identity {
43 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52 write!(f, "{}/{}", self.domain, self.name)
53 }
54}
55
56impl Identity {
57 pub fn core_identity() -> Identity {
58 Identity {
59 domain: APOLLO_SPEC_DOMAIN.to_string(),
60 name: name!("core"),
61 }
62 }
63
64 pub fn link_identity() -> Identity {
65 Identity {
66 domain: APOLLO_SPEC_DOMAIN.to_string(),
67 name: name!("link"),
68 }
69 }
70
71 pub fn federation_identity() -> Identity {
72 Identity {
73 domain: APOLLO_SPEC_DOMAIN.to_string(),
74 name: name!("federation"),
75 }
76 }
77
78 pub fn join_identity() -> Identity {
79 Identity {
80 domain: APOLLO_SPEC_DOMAIN.to_string(),
81 name: name!("join"),
82 }
83 }
84
85 pub fn inaccessible_identity() -> Identity {
86 Identity {
87 domain: APOLLO_SPEC_DOMAIN.to_string(),
88 name: name!("inaccessible"),
89 }
90 }
91
92 pub fn cost_identity() -> Identity {
93 Identity {
94 domain: APOLLO_SPEC_DOMAIN.to_string(),
95 name: name!("cost"),
96 }
97 }
98
99 pub fn context_identity() -> Identity {
100 Identity {
101 domain: APOLLO_SPEC_DOMAIN.to_string(),
102 name: name!("context"),
103 }
104 }
105
106 pub fn requires_scopes_identity() -> Identity {
107 Identity {
108 domain: APOLLO_SPEC_DOMAIN.to_string(),
109 name: name!("requiresScopes"),
110 }
111 }
112
113 pub fn authenticated_identity() -> Identity {
114 Identity {
115 domain: APOLLO_SPEC_DOMAIN.to_string(),
116 name: name!("authenticated"),
117 }
118 }
119
120 pub fn policy_identity() -> Identity {
121 Identity {
122 domain: APOLLO_SPEC_DOMAIN.to_string(),
123 name: name!("policy"),
124 }
125 }
126}
127
128#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
130pub struct Version {
131 pub major: u32,
133
134 pub minor: u32,
136}
137
138impl fmt::Display for Version {
139 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144 write!(f, "{}.{}", self.major, self.minor)
145 }
146}
147
148impl str::FromStr for Version {
149 type Err = SpecError;
150
151 fn from_str(s: &str) -> Result<Self, Self::Err> {
152 let (major, minor) = s.split_once('.').ok_or(SpecError::ParseError(
153 "version number is missing a dot (.)".to_string(),
154 ))?;
155
156 let major = major.parse::<u32>().map_err(|_| {
157 SpecError::ParseError(format!("invalid major version number '{}'", major))
158 })?;
159 let minor = minor.parse::<u32>().map_err(|_| {
160 SpecError::ParseError(format!("invalid minor version number '{}'", minor))
161 })?;
162
163 Ok(Version { major, minor })
164 }
165}
166
167impl Version {
168 pub fn satisfies(&self, required: &Version) -> bool {
177 if self.major == 0 {
178 self == required
179 } else {
180 self.major == required.major && self.minor >= required.minor
181 }
182 }
183
184 pub fn satisfies_range(&self, min: &Version, max: &Version) -> bool {
197 assert_eq!(min.major, max.major);
198 assert!(min.minor < max.minor);
199
200 self.major == min.major && self.minor >= min.minor && self.minor <= max.minor
201 }
202}
203
204#[derive(Clone, PartialEq, Eq, Debug)]
206pub struct Url {
207 pub identity: Identity,
209
210 pub version: Version,
212}
213
214impl fmt::Display for Url {
215 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
227 write!(f, "{}/v{}", self.identity, self.version)
228 }
229}
230
231impl str::FromStr for Url {
232 type Err = SpecError;
233
234 fn from_str(s: &str) -> Result<Self, Self::Err> {
235 match url::Url::parse(s) {
236 Ok(url) => {
237 let mut segments = url.path_segments().ok_or(SpecError::ParseError(
238 "invalid `@link` specification url".to_string(),
239 ))?;
240 let version = segments.next_back().ok_or(SpecError::ParseError(
241 "invalid `@link` specification url: missing specification version".to_string(),
242 ))?;
243 if !version.starts_with('v') {
244 return Err(SpecError::ParseError("invalid `@link` specification url: the last element of the path should be the version starting with a 'v'".to_string()));
245 }
246 let version = version.strip_prefix('v').unwrap().parse::<Version>()?;
247 let name = segments
248 .next_back()
249 .ok_or(SpecError::ParseError(
250 "invalid `@link` specification url: missing specification name".to_string(),
251 ))
252 .map(Name::new_unchecked)?;
258 let scheme = url.scheme();
259 if !scheme.starts_with("http") {
260 return Err(SpecError::ParseError("invalid `@link` specification url: only http(s) urls are supported currently".to_string()));
261 }
262 let url_domain = url.domain().ok_or(SpecError::ParseError(
263 "invalid `@link` specification url".to_string(),
264 ))?;
265 let path_remainder = segments.collect::<Vec<&str>>();
266 let domain = if path_remainder.is_empty() {
267 format!("{}://{}", scheme, url_domain)
268 } else {
269 format!("{}://{}/{}", scheme, url_domain, path_remainder.join("/"))
270 };
271 Ok(Url {
272 identity: Identity { domain, name },
273 version,
274 })
275 }
276 Err(e) => Err(SpecError::ParseError(format!(
277 "invalid specification url: {}",
278 e
279 ))),
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use apollo_compiler::name;
287
288 use super::*;
289
290 #[test]
291 fn versions_compares_correctly() {
292 assert!(Version { major: 0, minor: 0 } < Version { major: 0, minor: 1 });
293 assert!(Version { major: 1, minor: 1 } < Version { major: 1, minor: 4 });
294 assert!(Version { major: 1, minor: 4 } < Version { major: 2, minor: 0 });
295
296 assert_eq!(
297 Version { major: 0, minor: 0 },
298 Version { major: 0, minor: 0 }
299 );
300 assert_eq!(
301 Version { major: 2, minor: 3 },
302 Version { major: 2, minor: 3 }
303 );
304 }
305
306 #[test]
307 fn valid_versions_can_be_parsed() {
308 assert_eq!(
309 "0.0".parse::<Version>().unwrap(),
310 Version { major: 0, minor: 0 }
311 );
312 assert_eq!(
313 "0.5".parse::<Version>().unwrap(),
314 Version { major: 0, minor: 5 }
315 );
316 assert_eq!(
317 "2.49".parse::<Version>().unwrap(),
318 Version {
319 major: 2,
320 minor: 49
321 }
322 );
323 }
324
325 #[test]
326 fn invalid_versions_strings_return_menaingful_errors() {
327 assert_eq!(
328 "foo".parse::<Version>(),
329 Err(SpecError::ParseError(
330 "version number is missing a dot (.)".to_string()
331 ))
332 );
333 assert_eq!(
334 "foo.bar".parse::<Version>(),
335 Err(SpecError::ParseError(
336 "invalid major version number 'foo'".to_string()
337 ))
338 );
339 assert_eq!(
340 "0.bar".parse::<Version>(),
341 Err(SpecError::ParseError(
342 "invalid minor version number 'bar'".to_string()
343 ))
344 );
345 assert_eq!(
346 "0.12-foo".parse::<Version>(),
347 Err(SpecError::ParseError(
348 "invalid minor version number '12-foo'".to_string()
349 ))
350 );
351 assert_eq!(
352 "0.12.2".parse::<Version>(),
353 Err(SpecError::ParseError(
354 "invalid minor version number '12.2'".to_string()
355 ))
356 );
357 }
358
359 #[test]
360 fn valid_urls_can_be_parsed() {
361 assert_eq!(
362 "https://specs.apollo.dev/federation/v2.3"
363 .parse::<Url>()
364 .unwrap(),
365 Url {
366 identity: Identity {
367 domain: "https://specs.apollo.dev".to_string(),
368 name: name!("federation")
369 },
370 version: Version { major: 2, minor: 3 }
371 }
372 );
373
374 assert_eq!(
375 "http://something.com/more/path/my_spec_name/v0.1?k=2"
376 .parse::<Url>()
377 .unwrap(),
378 Url {
379 identity: Identity {
380 domain: "http://something.com/more/path".to_string(),
381 name: name!("my_spec_name")
382 },
383 version: Version { major: 0, minor: 1 }
384 }
385 );
386 }
387}