Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 202 additions & 92 deletions crates/apollo-compiler/src/executable/from_ast.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,159 @@
use super::*;
use crate::ty;
use std::sync::Arc;

pub(crate) struct BuildErrors<'a> {
pub(crate) errors: &'a mut DiagnosticList,
pub(crate) path: SelectionPath,
}

pub(crate) fn document_from_ast(
schema: Option<&Schema>,
document: &ast::Document,
errors: &mut DiagnosticList,
type_system_definitions_are_errors: bool,
) -> ExecutableDocument {
let mut operations = OperationMap::default();
let mut multiple_anonymous = false;
let mut fragments = IndexMap::with_hasher(Default::default());
let mut errors = BuildErrors {
errors,
path: SelectionPath {
nested_fields: Vec::new(),
// overwritten:
root: ExecutableDefinitionName::AnonymousOperation(ast::OperationType::Query),
},
};
for definition in &document.definitions {
debug_assert!(errors.path.nested_fields.is_empty());
match definition {
ast::Definition::OperationDefinition(operation) => {
if let Some(name) = &operation.name {
if let Some(anonymous) = &operations.anonymous {
/// A builder for constructing an [`ExecutableDocument`] from multiple AST documents.
///
/// This builder allows you to parse and combine executable definitions (operations and fragments)
/// from multiple source files into a single [`ExecutableDocument`].
///
/// # Example
///
/// ```rust
/// use apollo_compiler::{Schema, ExecutableDocument};
/// use apollo_compiler::parser::Parser;
/// use apollo_compiler::validation::DiagnosticList;
/// # let schema_src = "type Query { user: User } type User { id: ID }";
/// # let schema = Schema::parse_and_validate(schema_src, "schema.graphql").unwrap();
///
/// // Create a builder
/// let mut errors = DiagnosticList::new(Default::default());
/// let mut errors = DiagnosticList::new(Default::default());
/// let doc = ExecutableDocument::builder(Some(&schema), &mut errors)
/// .parse("query GetUser { user { id } }", "query1.graphql")
/// .parse("query GetMore { user { id } }", "query2.graphql")
/// .build();
///
/// assert!(errors.is_empty());
/// assert_eq!(doc.operations.named.len(), 2);
/// ```
pub struct ExecutableDocumentBuilder<'schema, 'errors> {
/// The executable document being built
pub(crate) document: ExecutableDocument,
/// Optional schema for type checking during build
schema: Option<&'schema Schema>,
/// Accumulated diagnostics
pub(crate) errors: &'errors mut DiagnosticList,
/// Track if we've seen multiple anonymous operations
multiple_anonymous: bool,
}

impl<'schema, 'errors> ExecutableDocumentBuilder<'schema, 'errors> {
/// Creates a new [`ExecutableDocumentBuilder`].
pub fn new(schema: Option<&'schema Schema>, errors: &'errors mut DiagnosticList) -> Self {
Self {
document: ExecutableDocument::new(),
schema,
errors,
multiple_anonymous: false,
}
}

/// Parses a GraphQL executable document from source text and adds it to the builder.
///
/// This is a convenience method that creates a parser and calls
/// [`Parser::parse_into_executable_builder`](crate::parser::Parser::parse_into_executable_builder).
///
pub fn parse(
mut self,
source_text: impl Into<String>,
path: impl AsRef<std::path::Path>,
) -> Self {
Parser::new().parse_into_executable_builder(source_text, path, &mut self);
self
}

/// Adds an AST document to the executable document being built.
pub fn add_ast_document(
&mut self,
document: &ast::Document,
type_system_definitions_are_errors: bool,
) {
Arc::make_mut(&mut self.errors.sources)
.extend(document.sources.iter().map(|(k, v)| (*k, v.clone())));
self.add_ast_document_not_adding_sources(document, type_system_definitions_are_errors);
}

pub(crate) fn add_ast_document_not_adding_sources(
&mut self,
document: &ast::Document,
type_system_definitions_are_errors: bool,
) {
let mut errors = BuildErrors {
errors: self.errors,
path: SelectionPath {
nested_fields: Vec::new(),
// overwritten:
root: ExecutableDefinitionName::AnonymousOperation(ast::OperationType::Query),
},
};

for definition in &document.definitions {
debug_assert!(errors.path.nested_fields.is_empty());
match definition {
ast::Definition::OperationDefinition(operation) => {
if let Some(name) = &operation.name {
if let Some(anonymous) = &self.document.operations.anonymous {
errors.errors.push(
anonymous.location(),
BuildError::AmbiguousAnonymousOperation,
)
}
if let Entry::Vacant(entry) =
self.document.operations.named.entry(name.clone())
{
errors.path.root = ExecutableDefinitionName::NamedOperation(
operation.operation_type,
name.clone(),
);
if let Some(op) =
Operation::from_ast(self.schema, &mut errors, operation)
{
entry.insert(operation.same_location(op));
} else {
errors.errors.push(
operation.location(),
BuildError::UndefinedRootOperation {
operation_type: operation.operation_type.name(),
},
)
}
} else {
let (key, _) =
self.document.operations.named.get_key_value(name).unwrap();
errors.errors.push(
name.location(),
BuildError::OperationNameCollision {
name_at_previous_location: key.clone(),
},
);
}
} else if let Some(previous) = &self.document.operations.anonymous {
if !self.multiple_anonymous {
self.multiple_anonymous = true;
errors
.errors
.push(previous.location(), BuildError::AmbiguousAnonymousOperation)
}
errors.errors.push(
anonymous.location(),
operation.location(),
BuildError::AmbiguousAnonymousOperation,
)
}
if let Entry::Vacant(entry) = operations.named.entry(name.clone()) {
errors.path.root = ExecutableDefinitionName::NamedOperation(
operation.operation_type,
name.clone(),
);
if let Some(op) = Operation::from_ast(schema, &mut errors, operation) {
entry.insert(operation.same_location(op));
} else if !self.document.operations.named.is_empty() {
errors.errors.push(
operation.location(),
BuildError::AmbiguousAnonymousOperation,
)
} else {
errors.path.root =
ExecutableDefinitionName::AnonymousOperation(operation.operation_type);
if let Some(op) = Operation::from_ast(self.schema, &mut errors, operation) {
self.document.operations.anonymous = Some(operation.same_location(op));
} else {
errors.errors.push(
operation.location(),
Expand All @@ -49,79 +162,76 @@ pub(crate) fn document_from_ast(
},
)
}
}
}
ast::Definition::FragmentDefinition(fragment) => {
if let Entry::Vacant(entry) =
self.document.fragments.entry(fragment.name.clone())
{
errors.path.root =
ExecutableDefinitionName::Fragment(fragment.name.clone());
if let Some(node) = Fragment::from_ast(self.schema, &mut errors, fragment) {
entry.insert(fragment.same_location(node));
}
} else {
let (key, _) = operations.named.get_key_value(name).unwrap();
let (key, _) = self
.document
.fragments
.get_key_value(&fragment.name)
.unwrap();
errors.errors.push(
name.location(),
BuildError::OperationNameCollision {
fragment.name.location(),
BuildError::FragmentNameCollision {
name_at_previous_location: key.clone(),
},
);
}
} else if let Some(previous) = &operations.anonymous {
if !multiple_anonymous {
multiple_anonymous = true;
errors
.errors
.push(previous.location(), BuildError::AmbiguousAnonymousOperation)
)
}
errors.errors.push(
operation.location(),
BuildError::AmbiguousAnonymousOperation,
)
} else if !operations.named.is_empty() {
errors.errors.push(
operation.location(),
BuildError::AmbiguousAnonymousOperation,
)
} else {
errors.path.root =
ExecutableDefinitionName::AnonymousOperation(operation.operation_type);
if let Some(op) = Operation::from_ast(schema, &mut errors, operation) {
operations.anonymous = Some(operation.same_location(op));
} else {
}
_ => {
if type_system_definitions_are_errors {
errors.errors.push(
operation.location(),
BuildError::UndefinedRootOperation {
operation_type: operation.operation_type.name(),
definition.location(),
BuildError::TypeSystemDefinition {
name: definition.name().cloned(),
describe: definition.describe(),
},
)
}
}
}
ast::Definition::FragmentDefinition(fragment) => {
if let Entry::Vacant(entry) = fragments.entry(fragment.name.clone()) {
errors.path.root = ExecutableDefinitionName::Fragment(fragment.name.clone());
if let Some(node) = Fragment::from_ast(schema, &mut errors, fragment) {
entry.insert(fragment.same_location(node));
}
} else {
let (key, _) = fragments.get_key_value(&fragment.name).unwrap();
errors.errors.push(
fragment.name.location(),
BuildError::FragmentNameCollision {
name_at_previous_location: key.clone(),
},
)
}
}
_ => {
if type_system_definitions_are_errors {
errors.errors.push(
definition.location(),
BuildError::TypeSystemDefinition {
name: definition.name().cloned(),
describe: definition.describe(),
},
)
}
}
}

Arc::make_mut(&mut self.document.sources)
.extend(document.sources.iter().map(|(k, v)| (*k, v.clone())));
}

/// Returns the executable document built from all added AST documents.
pub fn build(self) -> ExecutableDocument {
self.build_inner()
}

pub(crate) fn build_inner(mut self) -> ExecutableDocument {
self.document.sources = self.errors.sources.clone();
self.document
}
}

pub(crate) fn document_from_ast(
schema: Option<&Schema>,
document: &ast::Document,
errors: &mut DiagnosticList,
type_system_definitions_are_errors: bool,
) -> ExecutableDocument {
let mut builder = ExecutableDocumentBuilder::new(schema, errors);

builder.add_ast_document_not_adding_sources(document, type_system_definitions_are_errors);

let doc = builder.build_inner();

ExecutableDocument {
sources: document.sources.clone(),
operations,
fragments,
operations: doc.operations,
fragments: doc.fragments,
}
}

Expand All @@ -134,7 +244,7 @@ impl Operation {
let ty = if let Some(s) = schema {
s.root_operation(ast.operation_type)?.clone()
} else {
// Hack for validate_standalone_excutable
// Hack for validate_standalone_executable
ast.operation_type.default_type_name().clone()
};
let mut selection_set = SelectionSet::new(ty);
Expand Down Expand Up @@ -252,7 +362,7 @@ impl SelectionSet {
)
}
Err(schema::FieldLookupError::NoSuchType) => {
// `self.ty` is the name of a type not definied in the schema.
// `self.ty` is the name of a type not defined in the schema.
// It can come from:
// * A root operation type, or a field definition:
// the schema is invalid, no need to record another error here.
Expand Down
30 changes: 30 additions & 0 deletions crates/apollo-compiler/src/executable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub(crate) mod from_ast;
mod serialize;
pub(crate) mod validation;

pub use self::from_ast::ExecutableDocumentBuilder;
pub use crate::ast::Argument;
use crate::ast::ArgumentByNameError;
pub use crate::ast::Directive;
Expand Down Expand Up @@ -348,6 +349,35 @@ impl ExecutableDocument {
Self::default()
}

/// Returns a new builder for creating an ExecutableDocument from multiple AST documents.
///
/// The builder allows you to parse and combine executable definitions (operations and fragments)
/// from multiple source files into a single [`ExecutableDocument`].
///
/// # Example
///
/// ```rust
/// use apollo_compiler::{Schema, ExecutableDocument};
/// use apollo_compiler::parser::Parser;
/// use apollo_compiler::validation::DiagnosticList;
/// # let schema_src = "type Query { user: User, post: Post } type User { id: ID } type Post { title: String }";
/// # let schema = Schema::parse_and_validate(schema_src, "schema.graphql").unwrap();
///
/// let mut errors = DiagnosticList::new(Default::default());
/// let doc = ExecutableDocument::builder(Some(&schema), &mut errors)
/// .parse("query GetUser { user { id } }", "query1.graphql")
/// .parse("query GetMore { user { id } }", "query2.graphql")
/// .build();
///
/// assert!(errors.is_empty());
/// assert_eq!(doc.operations.named.len(), 2);
pub fn builder<'schema, 'errors>(
schema: Option<&'schema Valid<Schema>>,
errors: &'errors mut DiagnosticList,
) -> from_ast::ExecutableDocumentBuilder<'schema, 'errors> {
from_ast::ExecutableDocumentBuilder::new(schema.map(|s| s.as_ref()), errors)
}

/// Parse an executable document with the default configuration.
///
/// `path` is the filesystem path (or arbitrary string) used in diagnostics
Expand Down
Loading