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),
55    InDirective(&'a str, usize),
56}
57
58fn is_valid_graphql_name(s: &str) -> bool {
59    let mut chars = s.chars();
60
61    let Some(first_char) = chars.next() else {
62        return false;
63    };
64
65    if !first_char.is_ascii_alphabetic() && first_char != '_' {
66        return false;
67    }
68
69    for c in chars {
70        if !c.is_ascii_alphanumeric() && c != '_' {
71            return false;
72        }
73    }
74
75    true
76}
77
78#[cfg(test)]
79mod tests {
80    #![allow(clippy::panic)]
81
82    use super::*;
83
84    #[test]
85    fn error_tests() {
86        fn expect_error(path: &str) {
87            match Path::parse(path) {
88                Err(_) => (),
89                Ok(found) => panic!("Expected error for path: {path}, got: {found:?}"),
90            }
91        }
92
93        for case in [
94            "",
95            "s:",
96            "s:meow",
97            ":schema[-1]",
98            ":schema.something",
99            ":s",
100            ":s[1]",
101            ":something",
102            "t:something",
103            "something.:s",
104            "10",
105            "test.",
106            "test[10].",
107            "test.@siblings.",
108            // Directive applications without index.
109            "myObject._abc1.@requires",
110            "_my_object.@key",
111            // Empty directive name
112            "@",
113            "_my_object.@",
114            // Index on a directive definition
115            "@test[0]",
116            "myObject.&MyInterface.a",
117            "myObject.&MyInterface.",
118        ] {
119            expect_error(case);
120        }
121    }
122
123    #[test]
124    fn roundtrip_tests() {
125        fn test(path: &str) {
126            let Ok(parsed) = Path::parse(path) else {
127                panic!("Failed to parse path: {path}")
128            };
129            let formatted = parsed.to_string();
130            assert_eq!(path, formatted);
131        }
132
133        for case in [
134            ":schema",
135            ":schema[0]",
136            ":schema[1]",
137            ":schema[100]",
138            "@meow",
139            "@deprecated",
140            "@something__else",
141            "@join__type",
142            "my_union",
143            "__my_input_object[32]",
144            "_my_object.id",
145            "_my_object.@key[0]",
146            "myObject[10].id",
147            "myObject.@join__type[0]",
148            "myObject._abc1",
149            "myObject._abc1.@requires[0]",
150            "myObject._abc1.@requires[100]",
151            "myObject._abc1.arg1",
152            "myObject.&MyInterface",
153        ] {
154            test(case);
155        }
156    }
157}