Skip to main content

graphql_schema_diff/
path.rs

1//! A structured path to a specific location in a schema. See the docs on [Path].
2
3mod display;
4mod parse;
5
6use std::fmt;
7
8type ParseResult<T> = Result<T, ParseError>;
9
10#[derive(Debug)]
11pub struct ParseError;
12
13/// A structured path to a specific location in a schema.
14///
15/// Paths have a structured string representation.
16///
17/// Each level in a path is separated by a '.' character. Directive uses, type definition extensions and schema definition extensions have an index to distinguish between them. The index is optional.
18///
19/// First level:
20///
21/// - Type definitions are unprefixed.
22/// - Type extensions have an index, for example `Query[3]`.
23/// - Directive definitions are prefixed with an `@`: `@authorized`.
24/// - `:schema` for schema definitions, `:schema[1]` (index) for extensions.
25///
26/// Second level:
27///
28/// - Fields, union members, enum values and input object fields are unprefixed.
29/// - Directives on types and schema definitions are prefixed with an `@` and followed by an index: `@key[0]`.
30/// - Interface implementations are prefixed with an `&`: `&SomeInterface`.
31///
32/// Third level:
33///
34/// - Field arguments are unprefixed.
35/// - Directives on fields and enum values are prefixed with an `@` and followed by an index: `@include[0]`.
36#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
37pub enum Path<'a> {
38    SchemaDefinition,
39    SchemaExtension(usize),
40    TypeDefinition(&'a str, Option<PathInType<'a>>),
41    TypeExtension(&'a str, usize, Option<PathInType<'a>>),
42    DirectiveDefinition(&'a str),
43}
44
45#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
46pub enum PathInType<'a> {
47    InField(&'a str, Option<PathInField<'a>>),
48    InDirective(&'a str, usize),
49    InterfaceImplementation(&'a str),
50}
51
52#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
53pub enum PathInField<'a> {
54    InArgument(&'a str, Option<PathInArgument<'a>>),
55    InDirective(&'a str, usize),
56}
57
58#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
59pub enum PathInArgument<'a> {
60    InDirective(&'a str, usize),
61}
62
63fn is_valid_graphql_name(s: &str) -> bool {
64    let mut chars = s.chars();
65
66    let Some(first_char) = chars.next() else {
67        return false;
68    };
69
70    if !first_char.is_ascii_alphabetic() && first_char != '_' {
71        return false;
72    }
73
74    for c in chars {
75        if !c.is_ascii_alphanumeric() && c != '_' {
76            return false;
77        }
78    }
79
80    true
81}
82
83#[cfg(test)]
84mod tests {
85    #![allow(clippy::panic)]
86
87    use super::*;
88
89    #[test]
90    fn error_tests() {
91        fn expect_error(path: &str) {
92            match Path::parse(path) {
93                Err(_) => (),
94                Ok(found) => panic!("Expected error for path: {path}, got: {found:?}"),
95            }
96        }
97
98        for case in [
99            "",
100            "s:",
101            "s:meow",
102            ":schema[-1]",
103            ":schema.something",
104            ":s",
105            ":s[1]",
106            ":something",
107            "t:something",
108            "something.:s",
109            "10",
110            "test.",
111            "test[10].",
112            "test.@siblings.",
113            // Directive applications without index.
114            "myObject._abc1.@requires",
115            "_my_object.@key",
116            // Empty directive name
117            "@",
118            "_my_object.@",
119            // Index on a directive definition
120            "@test[0]",
121            "myObject.&MyInterface.a",
122            "myObject.&MyInterface.",
123            // Argument path errors
124            "myObject._abc1.arg1.",
125            "myObject._abc1.arg1.something",
126            "myObject._abc1.arg1.@requires",
127        ] {
128            expect_error(case);
129        }
130    }
131
132    #[test]
133    fn roundtrip_tests() {
134        fn test(path: &str) {
135            let Ok(parsed) = Path::parse(path) else {
136                panic!("Failed to parse path: {path}")
137            };
138            let formatted = parsed.to_string();
139            assert_eq!(path, formatted);
140        }
141
142        for case in [
143            ":schema",
144            ":schema[0]",
145            ":schema[1]",
146            ":schema[100]",
147            "@meow",
148            "@deprecated",
149            "@something__else",
150            "@join__type",
151            "my_union",
152            "__my_input_object[32]",
153            "_my_object.id",
154            "_my_object.@key[0]",
155            "myObject[10].id",
156            "myObject.@join__type[0]",
157            "myObject._abc1",
158            "myObject._abc1.@requires[0]",
159            "myObject._abc1.@requires[100]",
160            "myObject._abc1.arg1",
161            "myObject._abc1.arg1.@deprecated[0]",
162            "myObject.&MyInterface",
163        ] {
164            test(case);
165        }
166    }
167}