From 183ae488b5def080dc25a4f97d2c31270ff9a568 Mon Sep 17 00:00:00 2001 From: Zack Angelo Date: Mon, 24 Apr 2023 07:06:20 -0500 Subject: [PATCH] feat(encoder): impl TryFrom for apollo_encoder::* (#424) (#466) --------- Co-authored-by: Irina Shestak <8107784+lrlna@users.noreply.github.com> --- crates/apollo-compiler/Cargo.toml | 4 + ...modify_query_using_compiler_and_encoder.rs | 176 +++++ crates/apollo-compiler/src/database/hir.rs | 2 +- crates/apollo-encoder/Cargo.toml | 3 +- crates/apollo-encoder/src/from_hir.rs | 629 ++++++++++++++++++ crates/apollo-encoder/src/lib.rs | 2 + 6 files changed, 814 insertions(+), 2 deletions(-) create mode 100644 crates/apollo-compiler/examples/modify_query_using_compiler_and_encoder.rs create mode 100644 crates/apollo-encoder/src/from_hir.rs diff --git a/crates/apollo-compiler/Cargo.toml b/crates/apollo-compiler/Cargo.toml index 575eb951..624e2bb5 100644 --- a/crates/apollo-compiler/Cargo.toml +++ b/crates/apollo-compiler/Cargo.toml @@ -34,6 +34,10 @@ expect-test = "1.1" miette = "5.0" notify = "4.0.0" criterion = "0.3.0" +pretty_assertions = "0.7.1" +apollo-encoder = { path = "../apollo-encoder", version = "0.5.1", features = [ + "apollo-compiler", +] } [[bench]] name = "multi-source" diff --git a/crates/apollo-compiler/examples/modify_query_using_compiler_and_encoder.rs b/crates/apollo-compiler/examples/modify_query_using_compiler_and_encoder.rs new file mode 100644 index 00000000..241aadd4 --- /dev/null +++ b/crates/apollo-compiler/examples/modify_query_using_compiler_and_encoder.rs @@ -0,0 +1,176 @@ +use anyhow::Result; +use apollo_compiler::{ApolloCompiler, HirDatabase}; +use pretty_assertions::assert_eq; + +// This example merges the two operation definitions into a single one. +fn merge_queries() -> Result { + let ts = r"# + type Query { + user: String + me: String + launches: [Launch] + } + + type Launch { + launches: LaunchInfo + } + + type LaunchInfo { + id: Int + site: String + } + #"; + let executable = r"# + query LaunchSite { + launches { + launches { + id + site + } + } + } + + query AstronautInfo { + user + me + } + #"; + + let mut compiler = ApolloCompiler::new(); + compiler.add_type_system(ts, "ts.graphql"); + let file_id = compiler.add_executable(executable, "operation.graphql"); + let diagnostics = compiler.validate(); + assert_eq!(diagnostics.len(), 0); + + let mut new_query = apollo_encoder::Document::new(); + let mut sel_set = Vec::new(); + + for op in compiler.db.operations(file_id).iter() { + let selections: Vec = op + .selection_set() + .selection() + .iter() + .map(|sel| sel.try_into()) + .collect::, _>>()?; + sel_set.extend(selections); + } + + let op_def = apollo_encoder::OperationDefinition::new( + apollo_encoder::OperationType::Query, + apollo_encoder::SelectionSet::with_selections(sel_set), + ); + + new_query.operation(op_def); + + Ok(new_query) +} + +// This example only includes fields without the `@omitted` directive. +fn omitted_fields() -> Result { + let ts = r"# + type Query { + isbn: String + title: String + year: String + metadata: String + reviews: [String] + details: ProductDetails + } + + type ProductDetails { + country: String + } + + directive @omitted on FIELD + #"; + let executable = r"# + query Products { + isbn @omitted + title + year @omitted + metadata @omitted + reviews + details { + ...details + } + } + + fragment details on ProductDetails { + country + } + #"; + + let mut compiler = ApolloCompiler::new(); + compiler.add_type_system(ts, "ts.graphql"); + let file_id = compiler.add_executable(executable, "operation.graphql"); + let diagnostics = compiler.validate(); + for diag in &diagnostics { + println!("{diag}"); + } + + assert_eq!(diagnostics.len(), 0); + + let mut new_query = apollo_encoder::Document::new(); + + for op in compiler.db.operations(file_id).iter() { + let mut selection_set = apollo_encoder::SelectionSet::new(); + for sel in op.selection_set().selection().iter() { + match sel { + apollo_compiler::hir::Selection::Field(field) => { + let omit = field.directives().iter().any(|dir| dir.name() == "omitted"); + if !omit { + selection_set.selection(apollo_encoder::Selection::Field( + field.as_ref().try_into()?, + )); + } + } + _ => selection_set.selection(sel.try_into()?), + } + } + + let op_def = + apollo_encoder::OperationDefinition::new(op.operation_ty().try_into()?, selection_set); + new_query.operation(op_def); + } + + for fragment in compiler.db.fragments(file_id).values() { + new_query.fragment(fragment.as_ref().try_into()?) + } + Ok(new_query) +} + +fn main() -> Result<()> { + let merged = merge_queries()?; + assert_eq!( + merged.to_string(), + r#"query { + launches { + launches { + id + site + } + } + user + me +} +"# + ); + + let omitted_fields = omitted_fields()?; + assert_eq!( + omitted_fields.to_string(), + r#"query { + title + reviews + details { + ...details + } +} +fragment details on ProductDetails { + country +} +"# + ); + + Ok(()) +} diff --git a/crates/apollo-compiler/src/database/hir.rs b/crates/apollo-compiler/src/database/hir.rs index f2edc1b4..cde9c704 100644 --- a/crates/apollo-compiler/src/database/hir.rs +++ b/crates/apollo-compiler/src/database/hir.rs @@ -2608,10 +2608,10 @@ pub struct EnumValueDefinition { } impl EnumValueDefinition { + /// Get a reference to enum value description. pub fn description(&self) -> Option<&str> { self.description.as_deref() } - /// Get a reference to enum value definition's enum value pub fn enum_value(&self) -> &str { self.enum_value.src() diff --git a/crates/apollo-encoder/Cargo.toml b/crates/apollo-encoder/Cargo.toml index 26bd879f..76f546ab 100644 --- a/crates/apollo-encoder/Cargo.toml +++ b/crates/apollo-encoder/Cargo.toml @@ -21,10 +21,11 @@ edition = "2021" [dependencies] apollo-parser = { path = "../apollo-parser", version = "0.5.0", optional = true } +apollo-compiler = { path = "../apollo-compiler", version = "0.8.0", optional = true } thiserror = "1.0.37" [features] -default = ["apollo-parser"] +default = ["apollo-parser", "apollo-compiler"] [dev-dependencies] pretty_assertions = "0.7.1" diff --git a/crates/apollo-encoder/src/from_hir.rs b/crates/apollo-encoder/src/from_hir.rs new file mode 100644 index 00000000..d7ed54ca --- /dev/null +++ b/crates/apollo-encoder/src/from_hir.rs @@ -0,0 +1,629 @@ +use crate::*; +use apollo_compiler::hir; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum FromHirError { + #[error("float conversion error")] + FloatCoercionError, +} + +impl TryFrom<&hir::ObjectTypeDefinition> for ObjectDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::ObjectTypeDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = ObjectDefinition::new(name); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for interface in value.implements_interfaces() { + def.interface(interface.interface().to_owned()); + } + + for field in value.self_fields() { + def.field(field.try_into()?); + } + + for directive in value.self_directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::ObjectTypeExtension> for ObjectDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::ObjectTypeExtension) -> Result { + let name = value.name().to_owned(); + let mut def = ObjectDefinition::new(name); + def.extend(); + + for interface in value.implements_interfaces() { + def.interface(interface.interface().to_owned()); + } + + for field in value.fields_definition() { + def.field(field.try_into()?); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::InterfaceTypeDefinition> for InterfaceDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::InterfaceTypeDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = InterfaceDefinition::new(name); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for interface in value.implements_interfaces() { + def.interface(interface.interface().to_owned()); + } + + for field in value.self_fields() { + def.field(field.try_into()?); + } + + for directive in value.self_directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::InterfaceTypeExtension> for InterfaceDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::InterfaceTypeExtension) -> Result { + let name = value.name().to_owned(); + let mut def = InterfaceDefinition::new(name); + def.extend(); + + for interface in value.implements_interfaces() { + def.interface(interface.interface().to_owned()); + } + + for field in value.fields_definition() { + def.field(field.try_into()?); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::ScalarTypeDefinition> for ScalarDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::ScalarTypeDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = ScalarDefinition::new(name); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for directive in value.self_directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::ScalarTypeExtension> for ScalarDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::ScalarTypeExtension) -> Result { + let name = value.name().to_owned(); + let mut def = ScalarDefinition::new(name); + def.extend(); + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::UnionTypeDefinition> for UnionDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::UnionTypeDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = UnionDefinition::new(name); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for member in value.members() { + def.member(member.name().to_owned()); + } + + for directive in value.self_directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::UnionTypeExtension> for UnionDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::UnionTypeExtension) -> Result { + let name = value.name().to_owned(); + let mut def = UnionDefinition::new(name); + def.extend(); + + for member in value.union_members() { + def.member(member.name().to_owned()); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::EnumTypeDefinition> for EnumDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::EnumTypeDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = EnumDefinition::new(name); + + for value in value.self_values() { + def.value(value.try_into()?); + } + + for directive in value.self_directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::EnumTypeExtension> for EnumDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::EnumTypeExtension) -> Result { + let name = value.name().to_owned(); + let mut def = EnumDefinition::new(name); + def.extend(); + + for value in value.enum_values_definition() { + def.value(value.try_into()?); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::EnumValueDefinition> for EnumValue { + type Error = FromHirError; + + fn try_from(value: &hir::EnumValueDefinition) -> Result { + let enum_value = value.enum_value().to_owned(); + let mut def = EnumValue::new(enum_value); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::InputObjectTypeDefinition> for InputObjectDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::InputObjectTypeDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = InputObjectDefinition::new(name); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for directive in value.self_directives() { + def.directive(directive.try_into()?); + } + + for input_field in value.self_fields() { + def.field(input_field.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::InputObjectTypeExtension> for InputObjectDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::InputObjectTypeExtension) -> Result { + let name = value.name().to_owned(); + let mut def = InputObjectDefinition::new(name); + def.extend(); + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + for input_field in value.input_fields_definition() { + def.field(input_field.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::InputValueDefinition> for InputField { + type Error = FromHirError; + + fn try_from(value: &hir::InputValueDefinition) -> Result { + let name = value.name().to_owned(); + let typ = value.ty().try_into()?; + let mut def = InputField::new(name, typ); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + if let Some(default_value) = value.default_value() { + let encoder_value: Value = default_value.try_into()?; + let value_str = format!("{}", encoder_value); //TODO verify this + def.default_value(value_str); + } + + Ok(def) + } +} + +impl TryFrom<&hir::FieldDefinition> for FieldDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::FieldDefinition) -> Result { + let name = value.name().to_owned(); + let field_type = value.ty().try_into()?; + let mut def = FieldDefinition::new(name, field_type); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for iv_def in value.arguments().input_values() { + def.arg(iv_def.try_into()?); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::Type> for Type_ { + type Error = FromHirError; + + fn try_from(value: &hir::Type) -> Result { + let ty = match value { + hir::Type::NonNull { ty: hir_ty, .. } => Type_::NonNull { + ty: Box::new(hir_ty.as_ref().try_into()?), + }, + hir::Type::List { ty: hir_ty, .. } => Type_::List { + ty: Box::new(hir_ty.as_ref().try_into()?), + }, + hir::Type::Named { name, .. } => Type_::NamedType { + name: name.to_owned(), + }, + }; + + Ok(ty) + } +} + +impl TryFrom<&hir::Directive> for Directive { + type Error = FromHirError; + + fn try_from(value: &hir::Directive) -> Result { + let name = value.name().to_owned(); + let mut directive = Directive::new(name); + + for arg in value.arguments() { + directive.arg(arg.try_into()?); + } + + Ok(directive) + } +} + +impl TryFrom<&hir::Argument> for Argument { + type Error = FromHirError; + + fn try_from(value: &hir::Argument) -> Result { + let name = value.name().to_owned(); + let value = value.value().try_into()?; + let arg = Argument::new(name, value); + + Ok(arg) + } +} + +impl TryFrom<&hir::Value> for Value { + type Error = FromHirError; + + fn try_from(value: &hir::Value) -> Result { + let value = match value { + hir::Value::Variable(v) => Value::Variable(v.name().to_owned()), + + //TODO look more closely at int conversion + hir::Value::Int(i) => { + Value::Int(i.to_i32_checked().ok_or(FromHirError::FloatCoercionError)?) + } + hir::Value::Float(f) => Value::Float(f.get()), + hir::Value::String(s) => Value::String(s.clone()), + hir::Value::Boolean(b) => Value::Boolean(*b), + hir::Value::Null => Value::Null, + hir::Value::Enum(e) => Value::Enum(e.src().to_owned()), + hir::Value::List(l) => Value::List( + l.iter() + .map(TryInto::::try_into) + .collect::, FromHirError>>()?, + ), + hir::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(n, v)| v.try_into().map(|v: Value| (n.src().to_owned(), v))) + .collect::, FromHirError>>()?, + ), + }; + + Ok(value) + } +} + +impl TryFrom<&hir::InputValueDefinition> for InputValueDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::InputValueDefinition) -> Result { + let name = value.name().to_owned(); + let iv_type = value.ty().try_into()?; + let mut def = InputValueDefinition::new(name, iv_type); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + if let Some(default_value) = value.default_value() { + let encoder_value: Value = default_value.try_into()?; + let value_str = format!("{}", encoder_value); //TODO verify this + def.default_value(value_str); + } + + Ok(def) + } +} + +impl TryFrom<&hir::DirectiveDefinition> for DirectiveDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::DirectiveDefinition) -> Result { + let name = value.name().to_owned(); + let mut def = DirectiveDefinition::new(name); + + if let Some(description) = value.description().map(str::to_string) { + def.description(description); + } + + if value.repeatable() { + def.repeatable(); + } + + for arg in value.arguments().input_values() { + def.arg(arg.try_into()?); + } + + for directive_loc in value.directive_locations() { + def.location(directive_loc.name().to_owned()); + } + + Ok(def) + } +} + +impl TryFrom<&hir::FragmentDefinition> for FragmentDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::FragmentDefinition) -> Result { + let name = value.name().to_owned(); + let type_cond = value.type_condition().to_owned(); + let selection_set = value.selection_set().try_into()?; + + let def = FragmentDefinition::new(name, TypeCondition::new(type_cond), selection_set); + + Ok(def) + } +} + +impl TryFrom<&hir::SelectionSet> for SelectionSet { + type Error = FromHirError; + + fn try_from(value: &hir::SelectionSet) -> Result { + let mut selection_set = SelectionSet::new(); + + for selection in value.selection() { + selection_set.selection(selection.try_into()?) + } + + Ok(selection_set) + } +} + +impl TryFrom<&hir::Selection> for Selection { + type Error = FromHirError; + + fn try_from(value: &hir::Selection) -> Result { + let selection = match value { + hir::Selection::Field(field) => Selection::Field(field.as_ref().try_into()?), + hir::Selection::FragmentSpread(fragment) => { + Selection::FragmentSpread(fragment.as_ref().try_into()?) + } + hir::Selection::InlineFragment(fragment) => { + Selection::InlineFragment(fragment.as_ref().try_into()?) + } + }; + + Ok(selection) + } +} + +impl TryFrom<&hir::Field> for Field { + type Error = FromHirError; + + fn try_from(value: &hir::Field) -> Result { + let name = value.name().to_owned(); + let mut field = Field::new(name); + + field.alias(value.alias().map(|a| a.0.clone())); + + for arg in value.arguments() { + field.argument(arg.try_into()?); + } + + for directive in value.directives() { + field.directive(directive.try_into()?); + } + + if !value.selection_set().selection().is_empty() { + field.selection_set(Some(value.selection_set().try_into()?)); + } + + Ok(field) + } +} + +impl TryFrom<&hir::FragmentSpread> for FragmentSpread { + type Error = FromHirError; + + fn try_from(value: &hir::FragmentSpread) -> Result { + let name = value.name().to_owned(); + let mut fragment = FragmentSpread::new(name); + + for directive in value.directives() { + fragment.directive(directive.try_into()?); + } + + Ok(fragment) + } +} + +impl TryFrom<&hir::InlineFragment> for InlineFragment { + type Error = FromHirError; + + fn try_from(value: &hir::InlineFragment) -> Result { + let selection_set = value.selection_set().try_into()?; + let mut fragment = InlineFragment::new(selection_set); + + fragment.type_condition( + value + .type_condition() + .map(|tc| TypeCondition::new(tc.to_owned())), + ); + + for directive in value.directives() { + fragment.directive(directive.try_into()?); + } + + Ok(fragment) + } +} + +impl TryFrom<&hir::VariableDefinition> for VariableDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::VariableDefinition) -> Result { + let name = value.name().to_owned(); + let ty = value.ty().try_into()?; + let mut def = VariableDefinition::new(name, ty); + + if let Some(default_value) = value.default_value() { + def.default_value(default_value.try_into()?); + } + + Ok(def) + } +} + +impl TryFrom<&hir::OperationDefinition> for OperationDefinition { + type Error = FromHirError; + + fn try_from(value: &hir::OperationDefinition) -> Result { + let operation_type = value.operation_ty().try_into()?; + let selection_set = value.selection_set().try_into()?; + + let mut def = OperationDefinition::new(operation_type, selection_set); + + for directive in value.directives() { + def.directive(directive.try_into()?); + } + + for var in value.variables() { + def.variable_definition(var.try_into()?); + } + + Ok(def) + } +} + +impl TryInto for hir::OperationType { + type Error = FromHirError; + + fn try_into(self) -> Result { + Ok(match self { + hir::OperationType::Query => OperationType::Query, + hir::OperationType::Mutation => OperationType::Mutation, + hir::OperationType::Subscription => OperationType::Subscription, + }) + } +} diff --git a/crates/apollo-encoder/src/lib.rs b/crates/apollo-encoder/src/lib.rs index b8fbfba8..67948336 100644 --- a/crates/apollo-encoder/src/lib.rs +++ b/crates/apollo-encoder/src/lib.rs @@ -11,6 +11,8 @@ mod enum_def; mod enum_value; mod field; mod fragment; +#[cfg(feature = "apollo-compiler")] +mod from_hir; #[cfg(feature = "apollo-parser")] mod from_parser; mod input_field;