Skip to content

Commit d924e69

Browse files
authored
feat: schema coordinate parsing (#1185)
1 parent d38f9c6 commit d924e69

File tree

15 files changed

+1829
-4
lines changed

15 files changed

+1829
-4
lines changed

cynic-parser/parser-generator/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ fn main() {
55
.process()
66
.unwrap();
77

8-
for input in ["../src/parser/executable.rs", "../src/parser/schema.rs"] {
8+
lalrpop::Configuration::new()
9+
.set_in_dir("../src/schema_coordinates")
10+
.set_out_dir("../src/schema_coordinates")
11+
.process()
12+
.unwrap();
13+
14+
for input in [
15+
"../src/parser/executable.rs",
16+
"../src/parser/schema.rs",
17+
"../src/schema_coordinates/parser.rs",
18+
] {
919
std::process::Command::new("cargo")
1020
.args(["fmt", "--", input])
1121
.spawn()

cynic-parser/src/errors.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ pub enum Error {
6060

6161
/// The GraphQl document was empty
6262
EmptyExecutableDocument,
63+
64+
/// The schema coordinate was empty
65+
EmptySchemaCoordinate,
6366
}
6467

6568
impl Error {
@@ -79,7 +82,9 @@ impl Error {
7982
Error::MalformedStringLiteral(error) => error.span().into(),
8083
Error::MalformedDirectiveLocation(lhs, _, rhs) => Span::new(*lhs, *rhs).into(),
8184
Error::VariableInConstPosition(lhs, _, rhs) => Span::new(*lhs, *rhs).into(),
82-
Error::EmptyExecutableDocument | Error::EmptyTypeSystemDocument => None,
85+
Error::EmptyExecutableDocument
86+
| Error::EmptyTypeSystemDocument
87+
| Error::EmptySchemaCoordinate => None,
8388
}
8489
}
8590
}
@@ -97,6 +102,7 @@ impl std::error::Error for Error {
97102
| Error::EmptyTypeSystemDocument
98103
| Error::EmptyExecutableDocument => None,
99104
Error::Lexical(error) => Some(error),
105+
Error::EmptySchemaCoordinate => todo!(),
100106
}
101107
}
102108
}
@@ -177,6 +183,9 @@ impl fmt::Display for Error {
177183
"the graphql document was empty, please provide at least one definition"
178184
)
179185
}
186+
Error::EmptySchemaCoordinate => {
187+
write!(f, "the schema coordinate was empty")
188+
}
180189
}
181190
}
182191
}

cynic-parser/src/errors/report.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ impl Error {
108108
}
109109
Error::EmptyExecutableDocument => (self.to_string(), None, Some("graphql documents should contain at least one query, mutation or subscription".into())),
110110
Error::EmptyTypeSystemDocument => (self.to_string(), None, Some("graphql documents should contain at least one type, schema or directive definition".into())),
111+
Error::EmptySchemaCoordinate => (self.to_string(), None, Some("schema coordinates should not be empty".into())),
111112
}
112113
}
113114
}

cynic-parser/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod common;
22
pub mod executable;
3+
pub mod schema_coordinates;
34
pub mod type_system;
45
pub mod values;
56

@@ -19,6 +20,7 @@ pub use errors::Report;
1920
pub use self::{
2021
errors::Error,
2122
executable::ExecutableDocument,
23+
schema_coordinates::{SchemaCoordinate, parse_schema_coordinate},
2224
span::Span,
2325
type_system::TypeSystemDocument,
2426
values::{ConstValue, Value},

cynic-parser/src/parser/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// These are generated by LARLPOP
2-
#[expect(unused_braces)]
2+
#[allow(unused_braces)]
33
mod executable;
4-
#[expect(unused_braces)]
4+
#[allow(unused_braces)]
55
mod schema;
66

77
pub use executable::*;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::fmt;
2+
3+
use crate::Span;
4+
5+
use super::{MemberCoordinate, Name};
6+
7+
#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
8+
pub struct ArgumentCoordinate {
9+
pub(super) member: MemberCoordinate,
10+
pub(super) name: Name,
11+
}
12+
13+
impl ArgumentCoordinate {
14+
pub fn new(
15+
ty: impl Into<String>,
16+
field: impl Into<String>,
17+
argument: impl Into<String>,
18+
) -> Self {
19+
Self {
20+
member: MemberCoordinate::new(ty, field),
21+
name: Name::new(argument.into()),
22+
}
23+
}
24+
25+
pub fn member(&self) -> &MemberCoordinate {
26+
&self.member
27+
}
28+
29+
pub fn span(&self) -> Span {
30+
self.name.span
31+
}
32+
33+
pub fn name(&self) -> &str {
34+
&self.name.value
35+
}
36+
}
37+
38+
impl fmt::Display for ArgumentCoordinate {
39+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40+
write!(f, "{}({}:)", self.member, self.name.value)
41+
}
42+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use core::fmt;
2+
3+
use crate::Span;
4+
5+
use super::Name;
6+
7+
#[derive(Clone, Debug, Eq)]
8+
pub struct DirectiveCoordinate {
9+
pub(super) name: Name,
10+
pub(super) span: Span,
11+
}
12+
13+
impl PartialEq for DirectiveCoordinate {
14+
fn eq(&self, other: &Self) -> bool {
15+
self.name == other.name
16+
}
17+
}
18+
19+
impl std::hash::Hash for DirectiveCoordinate {
20+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
21+
self.name.hash(state);
22+
}
23+
}
24+
25+
impl PartialOrd for DirectiveCoordinate {
26+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
27+
Some(self.cmp(other))
28+
}
29+
}
30+
31+
impl Ord for DirectiveCoordinate {
32+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
33+
self.name.cmp(&other.name)
34+
}
35+
}
36+
37+
impl DirectiveCoordinate {
38+
pub fn new(name: impl Into<String>) -> Self {
39+
Self {
40+
name: Name::new(name.into()),
41+
span: Span::default(),
42+
}
43+
}
44+
45+
pub fn span(&self) -> Span {
46+
self.span
47+
}
48+
49+
pub fn name_span(&self) -> Span {
50+
self.name.span
51+
}
52+
53+
pub fn name(&self) -> &str {
54+
&self.name.value
55+
}
56+
}
57+
58+
impl fmt::Display for DirectiveCoordinate {
59+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60+
write!(f, "@{}", self.name.value)
61+
}
62+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use core::fmt;
2+
3+
use crate::Span;
4+
5+
use super::{DirectiveCoordinate, Name};
6+
7+
#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
8+
pub struct DirectiveArgumentCoordinate {
9+
pub(super) directive: DirectiveCoordinate,
10+
pub(super) name: Name,
11+
}
12+
13+
impl DirectiveArgumentCoordinate {
14+
pub(crate) fn new(name: impl Into<String>, argument: impl Into<String>) -> Self {
15+
Self {
16+
directive: DirectiveCoordinate::new(name),
17+
name: Name::new(argument.into()),
18+
}
19+
}
20+
21+
pub fn directive(&self) -> &DirectiveCoordinate {
22+
&self.directive
23+
}
24+
25+
pub fn span(&self) -> Span {
26+
self.name.span
27+
}
28+
29+
pub fn name(&self) -> &str {
30+
&self.name.value
31+
}
32+
}
33+
34+
impl fmt::Display for DirectiveArgumentCoordinate {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
write!(f, "{}({}:)", self.directive, self.name.value)
37+
}
38+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use std::fmt;
2+
3+
use logos::{Logos, SpannedIter};
4+
5+
use crate::{
6+
Span,
7+
lexer::{LexicalError, Spanned, TokenExtras},
8+
parser::AdditionalErrors,
9+
};
10+
11+
pub struct Lexer<'input> {
12+
// instead of an iterator over characters, we have a token iterator
13+
token_stream: SpannedIter<'input, Token<'input>>,
14+
input: &'input str,
15+
}
16+
17+
impl<'input> Lexer<'input> {
18+
pub fn new(input: &'input str) -> Self {
19+
Self {
20+
token_stream: Token::lexer(input).spanned(),
21+
input,
22+
}
23+
}
24+
}
25+
26+
impl<'input> Iterator for Lexer<'input> {
27+
type Item = Spanned<Token<'input>, usize, AdditionalErrors>;
28+
29+
fn next(&mut self) -> Option<Self::Item> {
30+
match self.token_stream.next() {
31+
None => None,
32+
Some((Ok(token), span)) => Some(Ok((span.start, token, span.end))),
33+
Some((Err(_), span)) => {
34+
Some(Err(AdditionalErrors::Lexical(LexicalError::InvalidToken(
35+
self.input[span.start..span.end].to_string(),
36+
Span::new(span.start, span.end),
37+
))))
38+
}
39+
}
40+
}
41+
}
42+
43+
/// Lexer for GraphQL schema coordinates: https://spec.graphql.org/September2025/#sec-Schema-Coordinates.Parsing-a-Schema-Coordinate
44+
#[derive(Logos, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
45+
#[logos(extras = TokenExtras, skip r"[ \t\r\n\f,\ufeff]+|#[^\n\r]*")]
46+
pub enum Token<'a> {
47+
// Valid tokens
48+
#[token("@")]
49+
At,
50+
51+
#[token(")")]
52+
CloseParen,
53+
54+
#[token(":")]
55+
Colon,
56+
57+
#[regex("[a-zA-Z_][a-zA-Z0-9_]*", |lex| lex.slice())]
58+
Identifier(&'a str),
59+
60+
#[token("(")]
61+
OpenParen,
62+
63+
#[token(".")]
64+
Dot,
65+
}
66+
67+
impl fmt::Display for Token<'_> {
68+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69+
f.write_str(match self {
70+
Token::At => "at ('@')",
71+
Token::CloseParen => "closing paren (')')",
72+
Token::Colon => "colon (':')",
73+
Token::Identifier(_) => "identifier",
74+
Token::OpenParen => "opening paren ('(')",
75+
Token::Dot => "dot ('.')",
76+
})
77+
}
78+
}
79+
80+
impl From<lalrpop_util::ParseError<usize, Token<'_>, AdditionalErrors>> for crate::Error {
81+
fn from(value: lalrpop_util::ParseError<usize, Token<'_>, AdditionalErrors>) -> Self {
82+
use crate::Error;
83+
use lalrpop_util::ParseError;
84+
85+
match value {
86+
ParseError::InvalidToken { location } => Error::InvalidToken { location },
87+
ParseError::UnrecognizedEof { location, expected } => {
88+
Error::UnrecognizedEof { location, expected }
89+
}
90+
ParseError::UnrecognizedToken {
91+
token: (lspan, token, rspan),
92+
expected,
93+
} => Error::UnrecognizedToken {
94+
token: (lspan, token.to_string(), rspan),
95+
expected,
96+
},
97+
ParseError::ExtraToken {
98+
token: (lspan, token, rspan),
99+
} => Error::ExtraToken {
100+
token: (lspan, token.to_string(), rspan),
101+
},
102+
ParseError::User {
103+
error: AdditionalErrors::Lexical(error),
104+
} => Error::Lexical(error),
105+
ParseError::User {
106+
error: AdditionalErrors::MalformedString(error),
107+
} => Error::MalformedStringLiteral(error),
108+
ParseError::User {
109+
error: AdditionalErrors::MalformedDirectiveLocation(lhs, location, rhs),
110+
} => Error::MalformedDirectiveLocation(lhs, location, rhs),
111+
ParseError::User {
112+
error: AdditionalErrors::VariableInConstPosition(lhs, name, rhs),
113+
} => Error::MalformedDirectiveLocation(lhs, name, rhs),
114+
}
115+
}
116+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::fmt;
2+
3+
use crate::Span;
4+
5+
use super::{Name, TypeCoordinate};
6+
7+
#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
8+
pub struct MemberCoordinate {
9+
pub(super) ty: TypeCoordinate,
10+
pub(super) name: Name,
11+
}
12+
13+
impl MemberCoordinate {
14+
pub fn new(ty: impl Into<String>, field: impl Into<String>) -> Self {
15+
MemberCoordinate {
16+
ty: TypeCoordinate::new(ty),
17+
name: Name::new(field.into()),
18+
}
19+
}
20+
21+
pub fn ty(&self) -> &TypeCoordinate {
22+
&self.ty
23+
}
24+
25+
pub fn span(&self) -> Span {
26+
self.name.span
27+
}
28+
29+
pub fn name(&self) -> &str {
30+
&self.name.value
31+
}
32+
}
33+
34+
impl fmt::Display for MemberCoordinate {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
write!(f, "{}.{}", self.ty.name.value, self.name.value)
37+
}
38+
}

0 commit comments

Comments
 (0)