diff --git a/CHANGELOG.md b/CHANGELOG.md index 3855bf344..c1cdec8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,40 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - The CLI can now optionally format the generated code with rustfmt (enable the `rustfmt` feature). +- When deriving, the generated module now has the same visibility (private, `pub`, `pub(crate)` or `crate`) as the struct under derive. +- Codegen now supports type-refining fragments, i.e. fragments on interfaces or unions that only apply to one of the variants. Example: + + ```graphql + type Pie { + diameter: Integer + name: String + } + + type Sandwich { + length: Float + ingredients: [String] + } + + union Food = Sandwich | Pie + + type Query { + lunch: Food + } + + fragment PieName on Pie { + name + } + + query Test { + lunch { + ...PieName + ...on Sandwich { + length + } + } + } + + ``` ### Changed diff --git a/graphql_client/tests/interfaces.rs b/graphql_client/tests/interfaces.rs index 21b39eb88..9daf7ce07 100644 --- a/graphql_client/tests/interfaces.rs +++ b/graphql_client/tests/interfaces.rs @@ -11,7 +11,7 @@ const RESPONSE: &'static str = include_str!("interfaces/interface_response.json" #[graphql( query_path = "tests/interfaces/interface_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug, PartialEq", + response_derives = "Debug, PartialEq" )] pub struct InterfaceQuery; @@ -50,15 +50,13 @@ fn interface_deserialization() { }; assert_eq!(response_data, expected); - - assert_eq!(response_data.everything.map(|names| names.len()), Some(4)); } #[derive(GraphQLQuery)] #[graphql( query_path = "tests/interfaces/interface_not_on_everything_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug", + response_derives = "Debug" )] pub struct InterfaceNotOnEverythingQuery; @@ -84,7 +82,7 @@ fn interface_not_on_everything_deserialization() { #[graphql( query_path = "tests/interfaces/interface_with_fragment_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug,PartialEq", + response_derives = "Debug,PartialEq" )] pub struct InterfaceWithFragmentQuery; @@ -101,35 +99,37 @@ fn fragment_in_interface() { response_data, ResponseData { everything: Some(vec![ - RustMyQueryEverything { + RustInterfaceWithFragmentQueryEverything { name: "Audrey Lorde".to_string(), public_status: PublicStatus { display_name: false, }, - on: RustMyQueryEverythingOn::Person(RustMyQueryEverythingOnPerson { - birthday: Some("1934-02-18".to_string()), - }) + on: RustInterfaceWithFragmentQueryEverythingOn::Person( + RustInterfaceWithFragmentQueryEverythingOnPerson { + birthday: Some("1934-02-18".to_string()), + } + ) }, - RustMyQueryEverything { + RustInterfaceWithFragmentQueryEverything { name: "Laïka".to_string(), public_status: PublicStatus { display_name: true }, - on: RustMyQueryEverythingOn::Dog(RustMyQueryEverythingOnDog { - is_good_dog: true, - }) + on: RustInterfaceWithFragmentQueryEverythingOn::Dog( + RustInterfaceWithFragmentQueryEverythingOnDog { is_good_dog: true } + ) }, - RustMyQueryEverything { + RustInterfaceWithFragmentQueryEverything { name: "Mozilla".to_string(), public_status: PublicStatus { display_name: false }, - on: RustMyQueryEverythingOn::Organization, + on: RustInterfaceWithFragmentQueryEverythingOn::Organization, }, - RustMyQueryEverything { + RustInterfaceWithFragmentQueryEverything { name: "Norbert".to_string(), public_status: PublicStatus { display_name: true }, - on: RustMyQueryEverythingOn::Dog(RustMyQueryEverythingOnDog { - is_good_dog: true - }), + on: RustInterfaceWithFragmentQueryEverythingOn::Dog( + RustInterfaceWithFragmentQueryEverythingOnDog { is_good_dog: true } + ), }, ]) } diff --git a/graphql_client/tests/interfaces/interface_with_fragment_query.graphql b/graphql_client/tests/interfaces/interface_with_fragment_query.graphql index 2a425741a..51268c775 100644 --- a/graphql_client/tests/interfaces/interface_with_fragment_query.graphql +++ b/graphql_client/tests/interfaces/interface_with_fragment_query.graphql @@ -2,10 +2,10 @@ fragment PublicStatus on Named { displayName } -query MyQuery { +query InterfaceWithFragmentQuery { everything { - name __typename + name ...PublicStatus ... on Dog { isGoodDog diff --git a/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql b/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql new file mode 100644 index 000000000..ef93e544f --- /dev/null +++ b/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql @@ -0,0 +1,17 @@ +fragment Birthday on Person { + birthday +} + +query MyQuery { + everything { + __typename + name + ... on Dog { + isGoodDog + } + ...Birthday + ... on Organization { + industry + } + } +} diff --git a/graphql_client/tests/type_refining_fragments.rs b/graphql_client/tests/type_refining_fragments.rs new file mode 100644 index 000000000..6a6e849d5 --- /dev/null +++ b/graphql_client/tests/type_refining_fragments.rs @@ -0,0 +1,87 @@ +#[macro_use] +extern crate graphql_client; +#[macro_use] +extern crate serde_derive; +extern crate serde; +extern crate serde_json; + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/interfaces/interface_with_type_refining_fragment_query.graphql", + schema_path = "tests/interfaces/interface_schema.graphql", + response_derives = "Debug, PartialEq" +)] +pub struct QueryOnInterface; + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/unions/type_refining_fragment_on_union_query.graphql", + schema_path = "tests/unions/union_schema.graphql", + response_derives = "PartialEq, Debug" +)] +pub struct QueryOnUnion; + +#[test] +fn type_refining_fragment_on_union() { + const RESPONSE: &'static str = include_str!("unions/union_query_response.json"); + + let response_data: query_on_union::ResponseData = serde_json::from_str(RESPONSE).unwrap(); + + let expected = query_on_union::ResponseData { + names: Some(vec![ + query_on_union::RustMyQueryNames::Person(query_on_union::RustMyQueryNamesOnPerson { + first_name: "Audrey".to_string(), + last_name: Some("Lorde".to_string()), + }), + query_on_union::RustMyQueryNames::Dog(query_on_union::RustMyQueryNamesOnDog { + name: "Laïka".to_string(), + }), + query_on_union::RustMyQueryNames::Organization( + query_on_union::RustMyQueryNamesOnOrganization { + title: "Mozilla".to_string(), + }, + ), + query_on_union::RustMyQueryNames::Dog(query_on_union::RustMyQueryNamesOnDog { + name: "Norbert".to_string(), + }), + ]), + }; + + assert_eq!(response_data, expected); +} + +#[test] +fn type_refining_fragment_on_interface() { + use query_on_interface::*; + + const RESPONSE: &'static str = include_str!("interfaces/interface_response.json"); + + let response_data: query_on_interface::ResponseData = serde_json::from_str(RESPONSE).unwrap(); + + let expected = ResponseData { + everything: Some(vec![ + RustMyQueryEverything { + name: "Audrey Lorde".to_string(), + on: RustMyQueryEverythingOn::Person(RustMyQueryEverythingOnPerson { + birthday: Some("1934-02-18".to_string()), + }), + }, + RustMyQueryEverything { + name: "Laïka".to_string(), + on: RustMyQueryEverythingOn::Dog(RustMyQueryEverythingOnDog { is_good_dog: true }), + }, + RustMyQueryEverything { + name: "Mozilla".to_string(), + on: RustMyQueryEverythingOn::Organization(RustMyQueryEverythingOnOrganization { + industry: Industry::OTHER, + }), + }, + RustMyQueryEverything { + name: "Norbert".to_string(), + on: RustMyQueryEverythingOn::Dog(RustMyQueryEverythingOnDog { is_good_dog: true }), + }, + ]), + }; + + assert_eq!(response_data, expected); +} diff --git a/graphql_client/tests/unions/type_refining_fragment_on_union_query.graphql b/graphql_client/tests/unions/type_refining_fragment_on_union_query.graphql new file mode 100644 index 000000000..9b8fb30f6 --- /dev/null +++ b/graphql_client/tests/unions/type_refining_fragment_on_union_query.graphql @@ -0,0 +1,17 @@ +fragment DogName on Dog { + name +} + +query MyQuery { + names { + __typename + ...DogName + ... on Person { + firstName + lastName + } + ... on Organization { + title + } + } +} diff --git a/graphql_client_codegen/src/interfaces.rs b/graphql_client_codegen/src/interfaces.rs index 85cb9e1f1..430e3fc5a 100644 --- a/graphql_client_codegen/src/interfaces.rs +++ b/graphql_client_codegen/src/interfaces.rs @@ -2,15 +2,17 @@ use failure; use objects::GqlObjectField; use proc_macro2::{Ident, Span, TokenStream}; use query::QueryContext; -use selection::{Selection, SelectionItem}; +use selection::{Selection, SelectionField, SelectionFragmentSpread, SelectionItem}; use shared::*; use std::borrow::Cow; use std::cell::Cell; use std::collections::HashSet; use unions::union_variants; +/// Represents an Interface type extracted from the schema. #[derive(Debug, Clone, PartialEq)] pub struct GqlInterface { + /// The documentation for the interface. Extracted from the schema. pub description: Option, /// The set of object types implementing this interface. pub implemented_by: HashSet, @@ -23,7 +25,9 @@ pub struct GqlInterface { impl GqlInterface { /// filters the selection to keep only the fields that refer to the interface's own. - fn object_selection(&self, selection: &Selection) -> Selection { + /// + /// This does not include the __typename field because it is translated into the `on` enum. + fn object_selection(&self, selection: &Selection, query_context: &QueryContext) -> Selection { Selection( selection .0 @@ -31,13 +35,48 @@ impl GqlInterface { // Only keep what we can handle .filter(|f| match f { SelectionItem::Field(f) => f.name != "__typename", - SelectionItem::FragmentSpread(_) => true, + SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { + // only if the fragment refers to the interface’s own fields (to take into account type-refining fragments) + let fragment = query_context + .fragments + .get(fragment_name) + .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name)) + // TODO: fix this + .unwrap(); + + fragment.on == self.name + } SelectionItem::InlineFragment(_) => false, }).map(|a| (*a).clone()) .collect(), ) } + fn union_selection(&self, selection: &Selection, query_context: &QueryContext) -> Selection { + Selection( + selection + .0 + .iter() + // Only keep what we can handle + .filter(|f| match f { + SelectionItem::InlineFragment(_) => true, + SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { + let fragment = query_context + .fragments + .get(fragment_name) + .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name)) + // TODO: fix this + .unwrap(); + + // only the fragments _not_ on the interface + fragment.on != self.name + } + SelectionItem::Field(SelectionField { name, .. }) => name == "__typename", + }).map(|a| (*a).clone()) + .collect(), + ) + } + /// Create an empty interface. This needs to be mutated before it is useful. pub(crate) fn new(name: Cow, description: Option<&str>) -> GqlInterface { GqlInterface { @@ -59,7 +98,7 @@ impl GqlInterface { ::shared::field_impls_for_selection( &self.fields, context, - &self.object_selection(selection), + &self.object_selection(selection, context), prefix, ) } @@ -75,7 +114,7 @@ impl GqlInterface { &self.name, &self.fields, context, - &self.object_selection(selection), + &self.object_selection(selection, context), prefix, ) } @@ -90,26 +129,21 @@ impl GqlInterface { let name = Ident::new(&prefix, Span::call_site()); let derives = query_context.response_derives(); - selection - .extract_typename() - .ok_or_else(|| format_err!("Missing __typename in selection for {}", prefix))?; - - let union_selection = Selection( - selection - .0 - .iter() - // Only keep what we can handle - .filter(|f| match f { - SelectionItem::InlineFragment(_) => true, - SelectionItem::Field(_) | SelectionItem::FragmentSpread(_) => false, - }).map(|a| (*a).clone()) - .collect(), - ); + selection.extract_typename().ok_or_else(|| { + format_err!( + "Missing __typename in selection for the {} interface (type: {})", + prefix, + self.name + ) + })?; let object_fields = self.response_fields_for_selection(query_context, &selection, prefix)?; let object_children = self.field_impls_for_selection(query_context, &selection, prefix)?; + + let union_selection = self.union_selection(&selection, &query_context); + let (mut union_variants, union_children, used_variants) = union_variants(&union_selection, query_context, prefix)?; @@ -154,3 +188,60 @@ impl GqlInterface { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + // to be improved + #[test] + fn union_selection_works() { + let iface = GqlInterface { + description: None, + implemented_by: HashSet::new(), + name: "MyInterface".into(), + fields: vec![], + is_required: Cell::new(true), + }; + + let context = QueryContext::new_empty(); + + let typename_field = ::selection::SelectionItem::Field(::selection::SelectionField { + alias: None, + name: "__typename".to_string(), + fields: Selection(vec![]), + }); + let selection = Selection(vec![typename_field.clone()]); + + assert_eq!( + iface.union_selection(&selection, &context), + Selection(vec![typename_field]) + ); + } + + // to be improved + #[test] + fn object_selection_works() { + let iface = GqlInterface { + description: None, + implemented_by: HashSet::new(), + name: "MyInterface".into(), + fields: vec![], + is_required: Cell::new(true), + }; + + let context = QueryContext::new_empty(); + + let typename_field = ::selection::SelectionItem::Field(::selection::SelectionField { + alias: None, + name: "__typename".to_string(), + fields: Selection(vec![]), + }); + let selection = Selection(vec![typename_field]); + + assert_eq!( + iface.object_selection(&selection, &context), + Selection(vec![]) + ); + } +} diff --git a/graphql_client_codegen/src/unions.rs b/graphql_client_codegen/src/unions.rs index 20ea1c98d..e77f74c92 100644 --- a/graphql_client_codegen/src/unions.rs +++ b/graphql_client_codegen/src/unions.rs @@ -2,7 +2,7 @@ use constants::*; use failure; use proc_macro2::{Ident, Span, TokenStream}; use query::QueryContext; -use selection::{Selection, SelectionItem}; +use selection::{Selection, SelectionFragmentSpread, SelectionItem}; use std::cell::Cell; use std::collections::BTreeSet; @@ -46,40 +46,49 @@ pub(crate) fn union_variants( true } }).map(|item| { - match item { + let (on, fields) = match item { SelectionItem::Field(_) => Err(format_err!("field selection on union"))?, - SelectionItem::FragmentSpread(_) => Err(format_err!("fragment spread on union"))?, - SelectionItem::InlineFragment(frag) => { - let variant_name = Ident::new(&frag.on, Span::call_site()); - used_variants.push(frag.on.to_string()); - - let new_prefix = format!("{}On{}", prefix, frag.on); - - let variant_type = Ident::new(&new_prefix, Span::call_site()); - - let field_object_type = query_context.schema.objects.get(&frag.on).map(|_f| { - query_context.maybe_expand_field(&frag.on, &frag.fields, &new_prefix) - }); - let field_interface = query_context.schema.interfaces.get(&frag.on).map(|_f| { - query_context.maybe_expand_field(&frag.on, &frag.fields, &new_prefix) - }); - // nested unions, is that even a thing? - let field_union_type = query_context.schema.unions.get(&frag.on).map(|_f| { - query_context.maybe_expand_field(&frag.on, &frag.fields, &new_prefix) - }); - - match field_object_type.or(field_interface).or(field_union_type) { - Some(tokens) => children_definitions.push(tokens?), - None => Err(UnionError::UnknownType { - ty: frag.on.to_string(), - })?, - }; - - Ok(quote! { - #variant_name(#variant_type) - }) + SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { + let fragment = query_context + .fragments + .get(fragment_name) + .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))?; + + (&fragment.on, &fragment.selection) } - } + SelectionItem::InlineFragment(frag) => (&frag.on, &frag.fields), + }; + let variant_name = Ident::new(&on, Span::call_site()); + used_variants.push(on.to_string()); + + let new_prefix = format!("{}On{}", prefix, on); + + let variant_type = Ident::new(&new_prefix, Span::call_site()); + + let field_object_type = query_context + .schema + .objects + .get(on) + .map(|_f| query_context.maybe_expand_field(&on, &fields, &new_prefix)); + let field_interface = query_context + .schema + .interfaces + .get(on) + .map(|_f| query_context.maybe_expand_field(&on, &fields, &new_prefix)); + let field_union_type = query_context + .schema + .unions + .get(on) + .map(|_f| query_context.maybe_expand_field(&on, &fields, &new_prefix)); + + match field_object_type.or(field_interface).or(field_union_type) { + Some(tokens) => children_definitions.push(tokens?), + None => Err(UnionError::UnknownType { ty: on.to_string() })?, + }; + + Ok(quote! { + #variant_name(#variant_type) + }) }).collect(); let variants = variants?;