Skip to content

Commit

Permalink
First pass of cynic-parser (#803)
Browse files Browse the repository at this point in the history
#### Why are we making this change?

There are parts of the latest spec that `graphql-parser` doesn't
support, so I want to replace it with an alternative.

Other options out there that I'm aware of:

- `async-graphql-parser` would work but its AST is quite annoying to
work with, and last time I tested it was slower than `graphql-parser`.
- `apollo-parser` continues on failure to parse, which is nice but means
there are a lot of `Option`s in its AST. I don't particularly want that
behaviour so that's out.
- `apollo-compiler` would probably make `apollo-parser` workable, but
feels like too heavy a dependency for my needs.

So it looks like I might write my own. Also, it'll be fun.

#### What effects does this change have?

Adds the first pass of a parser for GraphQL type system documents.
  • Loading branch information
obmarg authored Jan 7, 2024
1 parent edaf5ef commit dab97cb
Show file tree
Hide file tree
Showing 13 changed files with 2,154 additions and 14 deletions.
338 changes: 326 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ members = [
"examples",
"schemas/github",
"tests/querygen-compile-run",
"tests/ui-tests"
"tests/ui-tests",
"cynic-parser"
]
resolver = "2"

Expand All @@ -20,7 +21,8 @@ default-members = [
"cynic-codegen",
"cynic-introspection",
"cynic-proc-macros",
"cynic-querygen"
"cynic-querygen",
"cynic-parser"
]

[workspace.package]
Expand Down
31 changes: 31 additions & 0 deletions cynic-parser/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "cynic-parser"
version = "0.1.0"
edition = "2021"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
# TODO: Disable this at some point
default = ["sdl"]
sdl = ["pretty"]

[dependencies]
indexmap = "2"
lalrpop-util = "0.20.0"
logos = "0.13"
pretty = { version = "0.12", optional = true }

[dev-dependencies]
criterion = "0.4"
graphql-parser = "0.4"
insta = "1.29"

[build-dependencies]
lalrpop = "0.20.0"


[[bench]]
name = "parsing-benchmark"
harness = false
25 changes: 25 additions & 0 deletions cynic-parser/benches/parsing-benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use cynic_parser::Ast;

pub fn criterion_benchmark(c: &mut Criterion) {
let input = "type MyType { field: Whatever, field: Whatever }";
c.bench_function("cynic-parser parse object", |b| {
b.iter(|| {
let lexer = cynic_parser::Lexer::new(input);
let object = cynic_parser::ObjectDefinitionParser::new()
.parse(input, &mut Ast::new(), lexer)
.unwrap();
black_box(object)
})
});

c.bench_function("graphql_parser parse object", |b| {
b.iter(|| {
let parsed = graphql_parser::parse_schema::<String>(input).unwrap();
black_box(parsed)
})
});
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
3 changes: 3 additions & 0 deletions cynic-parser/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
lalrpop::process_root().unwrap();
}
262 changes: 262 additions & 0 deletions cynic-parser/src/ast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
use std::collections::HashMap;

use ids::*;
use indexmap::IndexSet;

pub(crate) mod ids;
mod reader;

pub use reader::{AstReader, Definition, ValueReader};

#[derive(Default)]
pub struct Ast {
strings: IndexSet<Box<str>>,

nodes: Vec<Node>,

definition_nodes: Vec<NodeId>,

schema_definitions: Vec<SchemaDefinition>,
object_definitions: Vec<ObjectDefinition>,
input_object_definitions: Vec<InputObjectDefinition>,

field_definitions: Vec<FieldDefinition>,
input_value_definitions: Vec<InputValueDefinition>,

type_references: Vec<Type>,

string_literals: Vec<StringLiteral>,

values: Vec<Value>,
directives: Vec<Directive>,
arguments: Vec<Argument>,

definition_descriptions: HashMap<NodeId, NodeId>,
}

// TODO: NonZeroUsize these?
pub struct Node {
contents: NodeContents,
// span: Span
}

pub enum NodeContents {
Ident(StringId),
SchemaDefinition(SchemaDefinitionId),
ObjectDefiniton(ObjectDefinitionId),
FieldDefinition(FieldDefinitionId),
InputObjectDefiniton(InputObjectDefinitionId),
InputValueDefinition(InputValueDefinitionId),
StringLiteral(StringLiteralId),
}

pub struct SchemaDefinition {
pub roots: Vec<RootOperationTypeDefinition>,
}

pub struct ObjectDefinition {
pub name: StringId,
pub fields: Vec<NodeId>,
pub directives: Vec<DirectiveId>,
}

pub struct FieldDefinition {
pub name: StringId,
pub ty: TypeId,
pub arguments: Vec<NodeId>,
pub description: Option<NodeId>,
}

pub struct InputObjectDefinition {
pub name: StringId,
pub fields: Vec<NodeId>,
pub directives: Vec<DirectiveId>,
}

pub struct InputValueDefinition {
pub name: StringId,
pub ty: TypeId,
pub description: Option<NodeId>,
pub default: Option<ValueId>,
}

pub struct RootOperationTypeDefinition {
pub operation_type: OperationType,
pub named_type: StringId,
}

pub struct Type {
pub name: StringId,
pub wrappers: Vec<WrappingType>,
}

pub enum WrappingType {
NonNull,
List,
}

#[derive(Clone, Copy, Debug)]
pub enum OperationType {
Query,
Mutation,
Subscription,
}

impl std::fmt::Display for OperationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OperationType::Query => write!(f, "query"),
OperationType::Mutation => write!(f, "mutation"),
OperationType::Subscription => write!(f, "subscription"),
}
}
}

pub enum StringLiteral {
Normal(StringId),
Block(StringId),
}

pub struct Directive {
pub name: StringId,
pub arguments: Vec<ArgumentId>,
}

pub struct Argument {
pub name: StringId,
pub value: ValueId,
}

pub enum Value {
Variable(StringId),
Int(i32),
Float(f32),
String(StringId),
Boolean(bool),
Null,
Enum(StringId),
List(Vec<ValueId>),
Object(Vec<(StringId, ValueId)>),
}

// TODO: Don't forget the spans etc.
// TODO: make this whole impl into a builder that wraps an Ast.
// Then the default Reader stuff can just go on Ast - much more sensible...
impl Ast {
pub fn new() -> Self {
Ast::default()
}

pub fn definitions(&mut self, ids: Vec<(Option<NodeId>, NodeId)>) {
for (description, definition) in ids {
if let Some(description) = description {
self.definition_descriptions.insert(definition, description);
}
self.definition_nodes.push(definition);
}
}

pub fn schema_definition(&mut self, definition: SchemaDefinition) -> NodeId {
let definition_id = SchemaDefinitionId(self.schema_definitions.len());
self.schema_definitions.push(definition);

let node_id = NodeId(self.nodes.len());
let contents = NodeContents::SchemaDefinition(definition_id);

self.nodes.push(Node { contents });

node_id
}

pub fn object_definition(&mut self, definition: ObjectDefinition) -> NodeId {
let definition_id = ObjectDefinitionId(self.object_definitions.len());
self.object_definitions.push(definition);

let node_id = NodeId(self.nodes.len());
let contents = NodeContents::ObjectDefiniton(definition_id);

self.nodes.push(Node { contents });

node_id
}

pub fn field_definition(&mut self, definition: FieldDefinition) -> NodeId {
let definition_id = FieldDefinitionId(self.field_definitions.len());
self.field_definitions.push(definition);

let node_id = NodeId(self.nodes.len());
let contents = NodeContents::FieldDefinition(definition_id);

self.nodes.push(Node { contents });

node_id
}

pub fn input_object_definition(&mut self, definition: InputObjectDefinition) -> NodeId {
let definition_id = InputObjectDefinitionId(self.input_object_definitions.len());
self.input_object_definitions.push(definition);

let node_id = NodeId(self.nodes.len());
let contents = NodeContents::InputObjectDefiniton(definition_id);

self.nodes.push(Node { contents });

node_id
}

pub fn input_value_definition(&mut self, definition: InputValueDefinition) -> NodeId {
let definition_id = InputValueDefinitionId(self.input_value_definitions.len());
self.input_value_definitions.push(definition);

let node_id = NodeId(self.nodes.len());
let contents = NodeContents::InputValueDefinition(definition_id);
self.nodes.push(Node { contents });

node_id
}

pub fn type_reference(&mut self, ty: Type) -> TypeId {
let ty_id = TypeId(self.type_references.len());
self.type_references.push(ty);
ty_id
}

pub fn directive(&mut self, directive: Directive) -> DirectiveId {
let id = DirectiveId(self.directives.len());
self.directives.push(directive);
id
}

pub fn argument(&mut self, argument: Argument) -> ArgumentId {
let id = ArgumentId(self.arguments.len());
self.arguments.push(argument);
id
}

pub fn value(&mut self, value: Value) -> ValueId {
let id = ValueId(self.values.len());
self.values.push(value);
id
}

pub fn string_literal(&mut self, literal: StringLiteral) -> NodeId {
let literal_id = StringLiteralId(self.string_literals.len());
self.string_literals.push(literal);

let node_id = NodeId(self.nodes.len());
let contents = NodeContents::StringLiteral(literal_id);
self.nodes.push(Node { contents });

node_id
}

pub fn ident(&mut self, ident: &str) -> StringId {
self.intern_string(ident)
}

// TOOD: should this be pub? not sure...
pub fn intern_string(&mut self, string: &str) -> StringId {
let (id, _) = self.strings.insert_full(string.into());
StringId(id)
}
}
Loading

0 comments on commit dab97cb

Please sign in to comment.