Skip to content

Commit

Permalink
Allow schema extensions to extend an implicit schema definition
Browse files Browse the repository at this point in the history
Fixes #682
  • Loading branch information
SimonSapin committed Oct 10, 2023
1 parent 140b91d commit 24c5aa5
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 162 deletions.
20 changes: 18 additions & 2 deletions crates/apollo-compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

# [x.x.x] (unreleased) - 2023-mm-dd

## BREAKING

Assorted `Schema` API changes by [SimonSapin] in [pull/678]:
- Type of the `schema_definition` field changed
from `Option<SchemaDefinition>` to `SchemaDefinition`.
Default root operations based on object type names
are now stored explicitly in `SchemaDefinition`.
Serialization relies on a heuristic to decide on implicit schema definition.
- Removed `schema_definition_directives` method: no longer having an `Option` allows
field `schema.schema_definition.directives` to be accessed directly
- Removed `query_root_operation`, `mutation_root_operation`, and `subscription_root_operation`
methods. Instead `schema.schema_definition.query` etc can be accessed directly.

## Features

- Add opt-in configuration for “orphan” extensions to be “adopted”, by [SimonSapin] in [pull/678]
Expand All @@ -37,14 +50,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
schema.validate()?;
```

[pull/678]: https://github.com/apollographql/apollo-rs/pull/678

## Fixes

- Allow built-in directives to be redefined, by [SimonSapin] in [pull/684], [issue/656]
- Allow schema extensions to extend a schema definition implied by object types named after default root operations, by [SimonSapin] in [pull/678], [issues/682]

[pull/684]: https://github.com/apollographql/apollo-rs/pull/684
[SimonSapin]: https://github.com/SimonSapin
[issue/656]: https://github.com/apollographql/apollo-rs/issues/656
[issue/682]: https://github.com/apollographql/apollo-rs/issues/682
[pull/678]: https://github.com/apollographql/apollo-rs/pull/678
[pull/684]: https://github.com/apollographql/apollo-rs/pull/684

# [1.0.0-beta.1](https://crates.io/crates/apollo-compiler/1.0.0-beta.1) - 2023-10-05

Expand Down
136 changes: 94 additions & 42 deletions crates/apollo-compiler/src/schema/from_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ use indexmap::map::Entry;
pub struct SchemaBuilder {
adopt_orphan_extensions: bool,
schema: Schema,
orphan_schema_extensions: Vec<Node<ast::SchemaExtension>>,
schema_definition: SchemaDefinitionStatus,
orphan_type_extensions: IndexMap<Name, Vec<ast::Definition>>,
}

enum SchemaDefinitionStatus {
Found,
NoneSoFar {
orphan_extensions: Vec<Node<ast::SchemaExtension>>,
},
}

impl Default for SchemaBuilder {
fn default() -> Self {
Self::new()
Expand All @@ -23,11 +30,19 @@ impl SchemaBuilder {
schema: Schema {
sources: IndexMap::new(),
build_errors: Vec::new(),
schema_definition: None,
schema_definition: Node::new(SchemaDefinition {
description: None,
directives: Directives::default(),
query: None,
mutation: None,
subscription: None,
}),
directive_definitions: IndexMap::new(),
types: IndexMap::new(),
},
orphan_schema_extensions: Vec::new(),
schema_definition: SchemaDefinitionStatus::NoneSoFar {
orphan_extensions: Vec::new(),
},
orphan_type_extensions: IndexMap::new(),
};

Expand All @@ -46,8 +61,11 @@ impl SchemaBuilder {
debug_assert!(
builder.schema.build_errors.is_empty()
&& builder.orphan_type_extensions.is_empty()
&& builder.orphan_schema_extensions.is_empty()
&& builder.schema.schema_definition.is_none(),
&& matches!(
&builder.schema_definition,
SchemaDefinitionStatus::NoneSoFar { orphan_extensions }
if orphan_extensions.is_empty()
)
);
builder
}
Expand Down Expand Up @@ -87,21 +105,21 @@ impl SchemaBuilder {
}
for definition in &document.definitions {
match definition {
ast::Definition::SchemaDefinition(def) => match &self.schema.schema_definition {
None => {
self.schema.schema_definition = Some(SchemaDefinition::from_ast(
ast::Definition::SchemaDefinition(def) => match &self.schema_definition {
SchemaDefinitionStatus::NoneSoFar { orphan_extensions } => {
self.schema.schema_definition = SchemaDefinition::from_ast(
&mut self.schema.build_errors,
def,
&self.orphan_schema_extensions,
));
self.orphan_schema_extensions = Vec::new();
orphan_extensions,
);
self.schema_definition = SchemaDefinitionStatus::Found;
}
Some(previous) => {
SchemaDefinitionStatus::Found => {
self.schema
.build_errors
.push(BuildError::SchemaDefinitionCollision {
location: def.location(),
previous_location: previous.location(),
previous_location: self.schema.schema_definition.location(),
})
}
},
Expand Down Expand Up @@ -258,14 +276,16 @@ impl SchemaBuilder {
})
}
}
ast::Definition::SchemaExtension(ext) => {
if let Some(root) = &mut self.schema.schema_definition {
root.make_mut()
.extend_ast(&mut self.schema.build_errors, ext)
} else {
self.orphan_schema_extensions.push(ext.clone())
ast::Definition::SchemaExtension(ext) => match &mut self.schema_definition {
SchemaDefinitionStatus::Found => self
.schema
.schema_definition
.make_mut()
.extend_ast(&mut self.schema.build_errors, ext),
SchemaDefinitionStatus::NoneSoFar { orphan_extensions } => {
orphan_extensions.push(ext.clone())
}
}
},
ast::Definition::ScalarTypeExtension(ext) => {
if let Some((_, ty_name, ty)) = self.schema.types.get_full_mut(&ext.name) {
if let ExtendedType::Scalar(ty) = ty {
Expand Down Expand Up @@ -413,42 +433,74 @@ impl SchemaBuilder {
}
}

/// Returns the schema built from all added documents, and orphan extensions:
///
/// * `Definition::SchemaExtension` variants if no `Definition::SchemaDefinition` was found
/// * `Definition::*TypeExtension` if no `Definition::*TypeDefinition` with the same name
/// was found, or if it is a different kind of type
/// Returns the schema built from all added documents
pub fn build(self) -> Schema {
let SchemaBuilder {
adopt_orphan_extensions,
mut schema,
orphan_schema_extensions,
schema_definition,
orphan_type_extensions,
} = self;
if adopt_orphan_extensions {
if !orphan_schema_extensions.is_empty() {
assert!(schema.schema_definition.is_none());
let schema_def = schema
.schema_definition
.get_or_insert_with(Default::default)
.make_mut();
for ext in &orphan_schema_extensions {
schema_def.extend_ast(&mut schema.build_errors, ext)
match schema_definition {
SchemaDefinitionStatus::Found => {}
SchemaDefinitionStatus::NoneSoFar { orphan_extensions } => {
// This a macro rather than a closure to generate separate `static`s
let mut has_implicit_root_operation = false;
macro_rules! default_root_operation {
($($operation_type: path: $root_operation: expr,)+) => {{
$(
let name = $operation_type.default_type_name();
if let Some(ExtendedType::Object(_)) = schema.types.get(name) {
static OBJECT_TYPE_NAME: OnceLock<ComponentStr> = OnceLock::new();
$root_operation = Some(OBJECT_TYPE_NAME.get_or_init(|| {
Name::new(name).to_component(ComponentOrigin::Definition)
}).clone());
has_implicit_root_operation = true;
}
)+
}};
}
let schema_def = schema.schema_definition.make_mut();
default_root_operation!(
ast::OperationType::Query: schema_def.query,
ast::OperationType::Mutation: schema_def.mutation,
ast::OperationType::Subscription: schema_def.subscription,
);

let apply_schema_extensions =
// https://github.com/apollographql/apollo-rs/issues/682
// If we have no explict `schema` definition but do have object type(s)
// with a default type name for root operations,
// an implicit schema definition is generated with those root operations.
// That implict definition can be extended:
has_implicit_root_operation ||
// https://github.com/apollographql/apollo-rs/pull/678
// In this opt-in mode we unconditionally assume
// an implicit schema definition to extend
adopt_orphan_extensions;
if apply_schema_extensions {
for ext in &orphan_extensions {
schema_def.extend_ast(&mut schema.build_errors, ext)
}
} else {
schema
.build_errors
.extend(orphan_extensions.into_iter().map(|ext| {
BuildError::OrphanSchemaExtension {
location: ext.location(),
}
}));
}
}
}
// https://github.com/apollographql/apollo-rs/pull/678
if adopt_orphan_extensions {
for (type_name, extensions) in orphan_type_extensions {
let type_def = adopt_type_extensions(&mut schema, &type_name, &extensions);
let previous = schema.types.insert(type_name, type_def);
assert!(previous.is_none());
}
} else {
schema
.build_errors
.extend(orphan_schema_extensions.into_iter().map(|ext| {
BuildError::OrphanSchemaExtension {
location: ext.location(),
}
}));
schema
.build_errors
.extend(orphan_type_extensions.into_values().flatten().map(|ext| {
Expand Down
92 changes: 13 additions & 79 deletions crates/apollo-compiler/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ pub struct Schema {
build_errors: Vec<BuildError>,

/// The `schema` definition and its extensions, defining root operations
///
/// For more convenient access to its directives regardless of `Option`,
/// see [`schema_definition_directives`][Self::schema_definition_directives]
pub schema_definition: Option<Node<SchemaDefinition>>,
pub schema_definition: Node<SchemaDefinition>,

/// Built-in and explicit directive definitions
pub directive_definitions: IndexMap<Name, Node<DirectiveDefinition>>,
Expand Down Expand Up @@ -332,15 +329,6 @@ impl Schema {
}
}

/// Directives of the `schema` definition and its extensions
pub fn schema_definition_directives(&self) -> &Directives {
if let Some(def) = &self.schema_definition {
&def.directives
} else {
Directives::EMPTY
}
}

/// Returns the type with the given name, if it is a scalar type
pub fn get_scalar(&self, name: &str) -> Option<&Node<ScalarType>> {
if let Some(ExtendedType::Scalar(ty)) = self.types.get(name) {
Expand Down Expand Up @@ -395,72 +383,15 @@ impl Schema {
}
}

/// Returns the name of the object type for the `query` root operation
pub fn query_root_operation(&self) -> Option<&NamedType> {
if let Some(root_operations) = &self.schema_definition {
root_operations
.query
.as_ref()
.map(|component| &component.node)
} else {
self.default_root_operation(ast::OperationType::Query)
}
}

/// Returns the name of the object type for the `mutation` root operation
pub fn mutation_root_operation(&self) -> Option<&NamedType> {
if let Some(root_operations) = &self.schema_definition {
root_operations
.mutation
.as_ref()
.map(|component| &component.node)
} else {
self.default_root_operation(ast::OperationType::Mutation)
}
}

/// Returns the name of the object type for the `subscription` root operation
pub fn subscription_root_operation(&self) -> Option<&NamedType> {
if let Some(root_operations) = &self.schema_definition {
root_operations
.subscription
.as_ref()
.map(|component| &component.node)
} else {
self.default_root_operation(ast::OperationType::Subscription)
}
}

/// Returns the name of the object type for the root operation with the given operation kind
pub fn root_operation(&self, operation_type: ast::OperationType) -> Option<&NamedType> {
if let Some(root_operations) = &self.schema_definition {
match operation_type {
ast::OperationType::Query => &root_operations.query,
ast::OperationType::Mutation => &root_operations.mutation,
ast::OperationType::Subscription => &root_operations.subscription,
}
.as_ref()
.map(|component| &component.node)
} else {
self.default_root_operation(operation_type)
match operation_type {
ast::OperationType::Query => &self.schema_definition.query,
ast::OperationType::Mutation => &self.schema_definition.mutation,
ast::OperationType::Subscription => &self.schema_definition.subscription,
}
}

fn default_root_operation(&self, operation_type: ast::OperationType) -> Option<&NamedType> {
let name = operation_type.default_type_name();
macro_rules! as_static {
() => {{
static OBJECT_TYPE_NAME: OnceLock<Name> = OnceLock::new();
OBJECT_TYPE_NAME.get_or_init(|| Name::new(name))
}};
}
self.get_object(name)
.is_some()
.then(|| match operation_type {
ast::OperationType::Query => as_static!(),
ast::OperationType::Mutation => as_static!(),
ast::OperationType::Subscription => as_static!(),
})
.as_ref()
.map(|component| &component.node)
}

/// Returns the definition of a type’s explicit field or meta-field.
Expand Down Expand Up @@ -581,7 +512,12 @@ impl Schema {
}),
]
});
if self.query_root_operation().is_some_and(|n| n == type_name) {
if self
.schema_definition
.query
.as_ref()
.is_some_and(|n| n == type_name)
{
// __typename: String!
// __schema: __Schema!
// __type(name: String!): __Type
Expand Down Expand Up @@ -886,8 +822,6 @@ impl InputObjectType {
}

impl Directives {
const EMPTY: &Self = &Self::new();

pub const fn new() -> Self {
Self(Vec::new())
}
Expand Down
Loading

0 comments on commit 24c5aa5

Please sign in to comment.