Skip to main content

graphql_schema_diff/path/
parse.rs

1use super::*;
2
3impl<'a> Path<'a> {
4    pub fn parse(s: &'a str) -> ParseResult<Path<'a>> {
5        let mut segments = s.split('.');
6
7        let first_segment = segments.next().filter(|s| !s.is_empty()).ok_or(ParseError)?;
8
9        // Is it a schema definition or extension?
10        if let Some(suffix) = first_segment.strip_prefix(":schema") {
11            if segments.next().is_some() {
12                return Err(ParseError);
13            }
14
15            if suffix.is_empty() {
16                return Ok(Path::SchemaDefinition);
17            }
18
19            let index = parse_index(suffix)?;
20            return Ok(Path::SchemaExtension(index));
21        }
22
23        // Is it a directive definition?
24        if let Some(name) = first_segment.strip_prefix('@') {
25            if !is_valid_graphql_name(name) {
26                return Err(ParseError);
27            }
28
29            if segments.next().is_some() {
30                return Err(ParseError);
31            }
32
33            return Ok(Path::DirectiveDefinition(name));
34        }
35
36        // The only remaining possibility is a type definition
37        let (name, idx) = parse_name_and_optional_index(first_segment)?;
38
39        if let Some(idx) = idx {
40            Ok(Path::TypeExtension(name, idx, Self::parse_path_in_type(segments)?))
41        } else {
42            Ok(Path::TypeDefinition(name, Self::parse_path_in_type(segments)?))
43        }
44    }
45
46    fn parse_path_in_type(mut segments: impl Iterator<Item = &'a str>) -> ParseResult<Option<PathInType<'a>>> {
47        let segment = match segments.next() {
48            Some("") => return Err(ParseError),
49            Some(segment) => segment,
50            None => return Ok(None),
51        };
52
53        // Is it a directive path?
54        if let Some(suffix) = segment.strip_prefix('@') {
55            let (name, idx) = parse_name_and_optional_index(suffix)?;
56
57            if segments.next().is_some() {
58                return Err(ParseError);
59            }
60
61            let Some(idx) = idx else {
62                return Err(ParseError);
63            };
64
65            return Ok(Some(PathInType::InDirective(name, idx)));
66        }
67
68        // Is it an interface implementation?
69        if let Some(suffix) = segment.strip_prefix('&') {
70            if !is_valid_graphql_name(suffix) {
71                return Err(ParseError);
72            }
73
74            if segments.next().is_some() {
75                return Err(ParseError);
76            }
77
78            return Ok(Some(PathInType::InterfaceImplementation(suffix)));
79        }
80
81        // The only other possibility is that we have a field / union member / enum value name.
82        if !is_valid_graphql_name(segment) {
83            return Err(ParseError);
84        }
85
86        Ok(Some(PathInType::InField(segment, Self::parse_path_in_field(segments)?)))
87    }
88
89    fn parse_path_in_field(mut segments: impl Iterator<Item = &'a str>) -> ParseResult<Option<PathInField<'a>>> {
90        let segment = match segments.next() {
91            Some("") => return Err(ParseError),
92            Some(segment) => segment,
93            None => return Ok(None),
94        };
95
96        // Is it a directive path?
97        if let Some(suffix) = segment.strip_prefix('@') {
98            let (name, Some(idx)) = parse_name_and_optional_index(suffix)? else {
99                return Err(ParseError);
100            };
101
102            if segments.next().is_some() {
103                return Err(ParseError);
104            }
105
106            return Ok(Some(PathInField::InDirective(name, idx)));
107        }
108
109        // The only other possibility is that we have an argument name.
110        if !is_valid_graphql_name(segment) {
111            return Err(ParseError);
112        }
113
114        Ok(Some(PathInField::InArgument(
115            segment,
116            Self::parse_path_in_argument(segments)?,
117        )))
118    }
119
120    fn parse_path_in_argument(mut segments: impl Iterator<Item = &'a str>) -> ParseResult<Option<PathInArgument<'a>>> {
121        let segment = match segments.next() {
122            Some("") => return Err(ParseError),
123            Some(segment) => segment,
124            None => return Ok(None),
125        };
126
127        if segments.next().is_some() {
128            return Err(ParseError);
129        }
130
131        let suffix = segment.strip_prefix('@').ok_or(ParseError)?;
132        let (name, Some(idx)) = parse_name_and_optional_index(suffix)? else {
133            return Err(ParseError);
134        };
135
136        Ok(Some(PathInArgument::InDirective(name, idx)))
137    }
138}
139
140fn parse_name_and_optional_index(s: &str) -> ParseResult<(&str, Option<usize>)> {
141    let bracket_idx = s.chars().position(|c| c == '[');
142
143    let name = bracket_idx.map(|idx| &s[..idx]).unwrap_or(s);
144
145    if !is_valid_graphql_name(name) {
146        return Err(ParseError);
147    }
148
149    let idx = if let Some(bracket_idx) = bracket_idx {
150        Some(parse_index(&s[bracket_idx..])?)
151    } else {
152        None
153    };
154
155    Ok((name, idx))
156}
157
158fn parse_index(s: &str) -> ParseResult<usize> {
159    if !s.starts_with('[') || !s.ends_with(']') {
160        return Err(ParseError);
161    }
162
163    let idx = &s[1..s.len() - 1];
164
165    if !idx.chars().any(|char| char.is_ascii_digit()) {
166        return Err(ParseError);
167    }
168
169    idx.parse::<usize>().map_err(|_| ParseError)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn parse_index_tests() {
178        assert_eq!(parse_index("[0]").unwrap(), 0);
179        assert_eq!(parse_index("[10]").unwrap(), 10);
180        assert_eq!(parse_index("[1243247]").unwrap(), 1243247);
181        assert_eq!(parse_index("[03]").unwrap(), 3);
182
183        assert!(parse_index("3").is_err());
184        assert!(parse_index("[3").is_err());
185        assert!(parse_index("3]").is_err());
186        assert!(parse_index("[[3]]").is_err());
187        assert!(parse_index("[0x3]").is_err());
188        assert!(parse_index("[-3]").is_err());
189    }
190}