-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Tools: Schema Merge #1632
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
use apollo_parser::ast; | ||
use apollo_parser::Parser as ApolloParser; | ||
use clap::Parser; | ||
use serde::Serialize; | ||
|
||
use std::fs; | ||
use std::path::{Path, PathBuf}; | ||
use std::ffi::OsStr; | ||
|
||
use crate::options::ToolsMergeOpt; | ||
use crate::{RoverOutput, RoverResult}; | ||
|
||
#[derive(Clone, Debug, Parser, Serialize)] | ||
pub struct Merge { | ||
#[clap(flatten)] | ||
options: ToolsMergeOpt, | ||
} | ||
|
||
impl Merge { | ||
pub fn run(&self) -> RoverResult<RoverOutput> { | ||
// find files by extension | ||
let schemas = self.find_files_by_extensions(self.options.schemas.clone(), &["graphql", "gql"])?; | ||
// merge schemas into one | ||
let schema = self.merge_schemas_into_one(schemas)?; | ||
Ok(RoverOutput::ToolsSchemaMerge(schema)) | ||
} | ||
|
||
fn find_files_by_extensions<P: AsRef<Path>>(&self, folder: P, extensions: &'_ [&str]) -> std::io::Result<Vec<PathBuf>> { | ||
let mut result = Vec::new(); | ||
for entry in fs::read_dir(folder.as_ref())? { | ||
let entry = entry?; | ||
let path = entry.path(); | ||
if path.is_dir() { | ||
let subfolder_result = self.find_files_by_extensions(&path, extensions); | ||
if let Ok(subfolder_paths) = subfolder_result { | ||
result.extend(subfolder_paths); | ||
} | ||
} else if let Some(file_ext) = path.extension().and_then(OsStr::to_str) { | ||
if extensions.contains(&file_ext) { | ||
result.push(path); | ||
} | ||
} | ||
} | ||
|
||
Ok(result) | ||
} | ||
|
||
fn merge_schemas_into_one(&self, schemas: Vec<PathBuf>) -> RoverResult<String> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not for this PR, but something to consider in the future is that we'd want to validate that all these files getting merged are semantically valid (with apollo-compiler). the compiler now has multi-file support, so you can just add them all to a compiler instance and validate. |
||
let mut schema = apollo_encoder::Document::new(); | ||
for schema_path in schemas { | ||
let schema_content = fs::read_to_string(schema_path)?; | ||
let parser = ApolloParser::new(&schema_content); | ||
let ast = parser.parse(); | ||
let doc = ast.document(); | ||
|
||
for def in doc.definitions() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dumb question- does apollo-parser automatically aggregate multiple definitions together? Meaning, if I have: type User @key(fields:"id") {
id: ID!
name: String!
} and type User @key(fields:"id") {
id: ID!
username: String!
} Would I get a combo of both, an error, or one of them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A quick test suggests you will get both of them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Amazing, thanks for testing! |
||
match def { | ||
ast::Definition::SchemaDefinition(schema_def) => { | ||
schema.schema(schema_def.try_into()?); | ||
} | ||
ast::Definition::OperationDefinition(op_def) => { | ||
schema.operation(op_def.try_into()?); | ||
} | ||
ast::Definition::FragmentDefinition(frag_def) => { | ||
schema.fragment(frag_def.try_into()?); | ||
} | ||
ast::Definition::DirectiveDefinition(dir_def) => { | ||
schema.directive(dir_def.try_into()?); | ||
} | ||
ast::Definition::ScalarTypeDefinition(scalar_type_def) => { | ||
schema.scalar(scalar_type_def.try_into()?); | ||
} | ||
ast::Definition::ObjectTypeDefinition(object_type_def) => { | ||
schema.object(object_type_def.try_into()?); | ||
} | ||
ast::Definition::InterfaceTypeDefinition(interface_type_def) => { | ||
schema.interface(interface_type_def.try_into()?); | ||
} | ||
ast::Definition::UnionTypeDefinition(union_type_def) => { | ||
schema.union(union_type_def.try_into()?); | ||
} | ||
ast::Definition::EnumTypeDefinition(enum_type_def) => { | ||
schema.enum_(enum_type_def.try_into()?); | ||
} | ||
ast::Definition::InputObjectTypeDefinition(input_object_type_def) => { | ||
schema.input_object(input_object_type_def.try_into()?); | ||
} | ||
ast::Definition::SchemaExtension(schema_extension_def) => { | ||
schema.schema(schema_extension_def.try_into()?); | ||
} | ||
ast::Definition::ScalarTypeExtension(scalar_type_extension_def) => { | ||
schema.scalar(scalar_type_extension_def.try_into()?); | ||
} | ||
ast::Definition::ObjectTypeExtension(object_type_extension_def) => { | ||
schema.object(object_type_extension_def.try_into()?); | ||
} | ||
ast::Definition::InterfaceTypeExtension(interface_type_extension_def) => { | ||
schema.interface(interface_type_extension_def.try_into()?); | ||
} | ||
ast::Definition::UnionTypeExtension(union_type_extension_def) => { | ||
schema.union(union_type_extension_def.try_into()?); | ||
} | ||
ast::Definition::EnumTypeExtension(enum_type_extension_def) => { | ||
schema.enum_(enum_type_extension_def.try_into()?); | ||
} | ||
ast::Definition::InputObjectTypeExtension(input_object_type_extension_def) => { | ||
schema.input_object(input_object_type_extension_def.try_into()?); | ||
} | ||
} | ||
} | ||
} | ||
Ok(schema.to_string()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
mod merge; | ||
|
||
pub use merge::Merge; | ||
|
||
use clap::Parser; | ||
use serde::Serialize; | ||
|
||
use crate::{RoverOutput, RoverResult}; | ||
|
||
#[derive(Debug, Clone, Parser, Serialize)] | ||
pub struct Tools { | ||
#[clap(subcommand)] | ||
command: Command, | ||
} | ||
|
||
#[derive(Clone, Debug, Parser, Serialize)] | ||
enum Command { | ||
/// Merge multiple schema files into one | ||
Merge(Merge), | ||
} | ||
|
||
impl Tools { | ||
pub(crate) fn run(&self) -> RoverResult<RoverOutput> { | ||
match &self.command { | ||
Command::Merge(merge) => merge.run(), | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use clap::Parser; | ||
use serde::{Serialize, Deserialize}; | ||
|
||
// use std::{io::Read}; | ||
|
||
// use crate::RoverResult; | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, Parser)] | ||
pub struct ToolsMergeOpt { | ||
/// The path to schema files to merge. | ||
#[arg(long, short = 's')] | ||
pub schemas: String, | ||
} | ||
|
||
impl ToolsMergeOpt { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
use assert_cmd::Command; | ||
|
||
#[test] | ||
fn it_has_a_tools_merge_command() { | ||
let mut cmd = Command::cargo_bin("rover").unwrap(); | ||
cmd.arg("tools") | ||
.arg("merge") | ||
.arg("--help") | ||
.assert() | ||
.success(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some folks also use
.graphqls
extension for schemas and.graphql
for operations. Technically folks could define their schemas using any extension they want so unsure if there is a need to capture all of them (that being said.graphqls
is somewhat common).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch. might be worth allowing users to use a custom extension as a flag in that case