Skip to content
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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ timber = { path = "./crates/timber" }

# https://github.com/apollographql/apollo-rs
apollo-parser = "0.5"
apollo-encoder = "0.5"

# https://github.com/apollographql/federation-rs
apollo-federation-types = "0.9.0"
Expand Down Expand Up @@ -145,6 +146,7 @@ anyhow = { workspace = true }
assert_fs = { workspace = true }
apollo-federation-types = { workspace = true }
apollo-parser = { workspace = true }
apollo-encoder = { workspace = true }
atty = { workspace = true }
billboard = { workspace = true }
binstall = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ impl Rover {
self.get_checks_timeout_seconds()?,
&self.output_opts,
),
Command::Tools(command) => command.run(),
Command::Template(command) => command.run(self.get_client_config()?),
Command::Readme(command) => command.run(self.get_client_config()?),
Command::Subgraph(command) => command.run(
Expand Down Expand Up @@ -377,6 +378,9 @@ pub enum Command {
/// Graph API schema commands
Graph(command::Graph),

/// Commands for manipulating schema files
Tools(command::Tools),

/// Commands for working with templates
Template(command::Template),

Expand Down
2 changes: 2 additions & 0 deletions src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) mod subgraph;
mod supergraph;
pub(crate) mod template;
mod update;
pub(crate) mod tools;

pub(crate) mod output;

Expand All @@ -32,3 +33,4 @@ pub use subgraph::Subgraph;
pub use supergraph::Supergraph;
pub use template::Template;
pub use update::Update;
pub use tools::Tools;
3 changes: 3 additions & 0 deletions src/command/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub enum RoverOutput {
dry_run: bool,
delete_response: SubgraphDeleteResponse,
},
ToolsSchemaMerge(String),
TemplateList(Vec<ListTemplatesForLanguageTemplates>),
TemplateUseSuccess {
template_id: String,
Expand Down Expand Up @@ -302,6 +303,7 @@ impl RoverOutput {
table, details.root_url, details.graph_ref.name
))
}
RoverOutput::ToolsSchemaMerge(schema) => Some(schema.to_string()),
RoverOutput::TemplateList(templates) => {
let mut table = table::get_table();

Expand Down Expand Up @@ -496,6 +498,7 @@ impl RoverOutput {
json!(delete_response)
}
RoverOutput::SubgraphList(list_response) => json!(list_response),
RoverOutput::ToolsSchemaMerge(merge_response) => json!({ "schema_merge_response": merge_response }),
RoverOutput::TemplateList(templates) => json!({ "templates": templates }),
RoverOutput::TemplateUseSuccess { template_id, path } => {
json!({ "template_id": template_id, "path": path })
Expand Down
114 changes: 114 additions & 0 deletions src/command/tools/merge.rs
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"])?;
Copy link
Member

@dariuszkuc dariuszkuc Jun 12, 2023

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).

Copy link
Contributor

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

// 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> {
Copy link
Member

Choose a reason for hiding this comment

The 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() {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick test suggests you will get both of them.

Copy link
Contributor

Choose a reason for hiding this comment

The 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())
}
}
28 changes: 28 additions & 0 deletions src/command/tools/mod.rs
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(),
}
}
}
2 changes: 2 additions & 0 deletions src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod profile;
mod schema;
mod subgraph;
mod template;
mod tools;

pub(crate) use check::*;
pub(crate) use compose::*;
Expand All @@ -19,3 +20,4 @@ pub(crate) use profile::*;
pub(crate) use schema::*;
pub(crate) use subgraph::*;
pub(crate) use template::*;
pub(crate) use tools::*;
17 changes: 17 additions & 0 deletions src/options/tools.rs
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 {

}
11 changes: 11 additions & 0 deletions tests/tools/merge.rs
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();
}