diff --git a/Cargo.lock b/Cargo.lock index f632b74d3..c3244df00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3829,6 +3829,7 @@ name = "rover" version = "0.15.0" dependencies = [ "anyhow", + "apollo-encoder", "apollo-federation-types", "apollo-parser", "assert-json-diff", diff --git a/Cargo.toml b/Cargo.toml index 0b1878e2b..155c9ab73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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 } diff --git a/src/cli.rs b/src/cli.rs index 70f5cd107..68efca366 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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( @@ -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), diff --git a/src/command/mod.rs b/src/command/mod.rs index 1a0768137..1b53359de 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod subgraph; mod supergraph; pub(crate) mod template; mod update; +pub(crate) mod tools; pub(crate) mod output; @@ -32,3 +33,4 @@ pub use subgraph::Subgraph; pub use supergraph::Supergraph; pub use template::Template; pub use update::Update; +pub use tools::Tools; diff --git a/src/command/output.rs b/src/command/output.rs index 93d659ebc..d81b9eb3b 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -69,6 +69,7 @@ pub enum RoverOutput { dry_run: bool, delete_response: SubgraphDeleteResponse, }, + ToolsSchemaMerge(String), TemplateList(Vec), TemplateUseSuccess { template_id: String, @@ -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(); @@ -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 }) diff --git a/src/command/tools/merge.rs b/src/command/tools/merge.rs new file mode 100644 index 000000000..a173af533 --- /dev/null +++ b/src/command/tools/merge.rs @@ -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 { + // 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>(&self, folder: P, extensions: &'_ [&str]) -> std::io::Result> { + 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) -> RoverResult { + 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() { + 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()) + } +} diff --git a/src/command/tools/mod.rs b/src/command/tools/mod.rs new file mode 100644 index 000000000..782c9dfe1 --- /dev/null +++ b/src/command/tools/mod.rs @@ -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 { + match &self.command { + Command::Merge(merge) => merge.run(), + } + } +} diff --git a/src/options/mod.rs b/src/options/mod.rs index 0aa8afbbe..2afa0c141 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -8,6 +8,7 @@ mod profile; mod schema; mod subgraph; mod template; +mod tools; pub(crate) use check::*; pub(crate) use compose::*; @@ -19,3 +20,4 @@ pub(crate) use profile::*; pub(crate) use schema::*; pub(crate) use subgraph::*; pub(crate) use template::*; +pub(crate) use tools::*; diff --git a/src/options/tools.rs b/src/options/tools.rs new file mode 100644 index 000000000..6982eb569 --- /dev/null +++ b/src/options/tools.rs @@ -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 { + +} diff --git a/tests/tools/merge.rs b/tests/tools/merge.rs new file mode 100644 index 000000000..464674cef --- /dev/null +++ b/tests/tools/merge.rs @@ -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(); +} \ No newline at end of file