diff --git a/CHANGELOG.md b/CHANGELOG.md index 371fee526..18a5d0c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -258,7 +258,10 @@ There are a number of breaking changes due to the new features, read the `Added` ### Added -- Copy documentation from the GraphQL schema to the generated types (including their fields) as normal Rust documentation. Documentation will show up in the generated docs as well as IDEs that support expanding derive macros (which does not include the RLS yet). +- Copy documentation from the GraphQL schema to the generated types (including + their fields) as normal Rust documentation. Documentation will show up in the + generated docs as well as IDEs that support expanding derive macros (which + does not include the RLS yet). - Implement and test deserializing subscription responses. We also try to provide helpful error messages when a subscription query is not valid (i.e. when it has more than one top-level field). - Support the [new top-level errors shape from the June 2018 spec](https://github.com/facebook/graphql/blob/master/spec/Section%207%20--%20Response.md), except for the `extensions` field (see issue #64). diff --git a/README.md b/README.md index ac6353d08..41b1eecc5 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ A typed GraphQL client library for Rust. ```rust use graphql_client::{GraphQLQuery, Response}; + use std::error::Error; #[derive(GraphQLQuery)] #[graphql( @@ -68,7 +69,7 @@ A typed GraphQL client library for Rust. )] pub struct UnionQuery; - fn perform_my_query(variables: union_query::Variables) -> Result<(), anyhow::Error> { + fn perform_my_query(variables: union_query::Variables) -> Result<(), Box> { // this is the important line let request_body = UnionQuery::build_query(variables); diff --git a/examples/github/Cargo.toml b/examples/github/Cargo.toml index ca95d2604..8f19481ba 100644 --- a/examples/github/Cargo.toml +++ b/examples/github/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Tom Houlé "] edition = "2018" [dev-dependencies] -anyhow = "*" +anyhow = "1.0" graphql_client = { path = "../../graphql_client" } serde = "^1.0" reqwest = "^0.9" diff --git a/examples/hasura/Cargo.toml b/examples/hasura/Cargo.toml index fd88c28b1..7c4bf245e 100644 --- a/examples/hasura/Cargo.toml +++ b/examples/hasura/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Mark Catley "] edition = "2018" [dev-dependencies] -anyhow = "*" +anyhow = "1.0" graphql_client = { path = "../../graphql_client" } serde = "1.0" serde_derive = "1.0" diff --git a/graphql-introspection-query/src/introspection_response.rs b/graphql-introspection-query/src/introspection_response.rs index 9e7d1fe8e..d3daa1814 100644 --- a/graphql-introspection-query/src/introspection_response.rs +++ b/graphql-introspection-query/src/introspection_response.rs @@ -78,7 +78,7 @@ impl<'de> Deserialize<'de> for __DirectiveLocation { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum __TypeKind { SCALAR, OBJECT, @@ -130,11 +130,11 @@ pub struct FullType { pub kind: Option<__TypeKind>, pub name: Option, pub description: Option, - pub fields: Option>>, - pub input_fields: Option>>, - pub interfaces: Option>>, - pub enum_values: Option>>, - pub possible_types: Option>>, + pub fields: Option>, + pub input_fields: Option>, + pub interfaces: Option>, + pub enum_values: Option>, + pub possible_types: Option>, } #[derive(Clone, Debug, Deserialize)] @@ -196,19 +196,14 @@ pub struct FullTypePossibleTypes { #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InputValue { - pub name: Option, + pub name: String, pub description: Option, #[serde(rename = "type")] - pub type_: Option, + pub type_: InputValueType, pub default_value: Option, } -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InputValueType { - #[serde(flatten)] - pub type_ref: TypeRef, -} +type InputValueType = TypeRef; #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/graphql_client/src/lib.rs b/graphql_client/src/lib.rs index c83a750b1..b0dca88dd 100644 --- a/graphql_client/src/lib.rs +++ b/graphql_client/src/lib.rs @@ -31,6 +31,7 @@ doc_comment::doctest!("../../README.md"); /// ``` /// use graphql_client::*; /// use serde_json::json; +/// use std::error::Error; /// /// #[derive(GraphQLQuery)] /// #[graphql( @@ -39,7 +40,7 @@ doc_comment::doctest!("../../README.md"); /// )] /// struct StarWarsQuery; /// -/// fn main() -> Result<(), anyhow::Error> { +/// fn main() -> Result<(), Box> { /// use graphql_client::GraphQLQuery; /// /// let variables = star_wars_query::Variables { @@ -124,13 +125,14 @@ impl Display for PathFragment { /// # use serde_json::json; /// # use serde::Deserialize; /// # use graphql_client::GraphQLQuery; +/// # use std::error::Error; /// # /// # #[derive(Debug, Deserialize, PartialEq)] /// # struct ResponseData { /// # something: i32 /// # } /// # -/// # fn main() -> Result<(), anyhow::Error> { +/// # fn main() -> Result<(), Box> { /// use graphql_client::*; /// /// let body: Response = serde_json::from_value(json!({ @@ -230,6 +232,7 @@ impl Display for Error { /// # use serde_json::json; /// # use serde::Deserialize; /// # use graphql_client::GraphQLQuery; +/// # use std::error::Error; /// # /// # #[derive(Debug, Deserialize, PartialEq)] /// # struct User { @@ -247,7 +250,7 @@ impl Display for Error { /// # dogs: Vec, /// # } /// # -/// # fn main() -> Result<(), anyhow::Error> { +/// # fn main() -> Result<(), Box> { /// use graphql_client::Response; /// /// let body: Response = serde_json::from_value(json!({ diff --git a/graphql_client/tests/fragments.rs b/graphql_client/tests/fragments.rs index 28572b0e7..902ec409e 100644 --- a/graphql_client/tests/fragments.rs +++ b/graphql_client/tests/fragments.rs @@ -24,13 +24,7 @@ fn fragment_reference() { let valid_fragment_reference = serde_json::from_value::(valid_response).unwrap(); - assert_eq!( - valid_fragment_reference - .fragment_reference - .in_fragment - .unwrap(), - "value" - ); + assert_eq!(valid_fragment_reference.in_fragment.unwrap(), "value"); } #[test] @@ -42,13 +36,7 @@ fn fragments_with_snake_case_name() { let valid_fragment_reference = serde_json::from_value::(valid_response).unwrap(); - assert_eq!( - valid_fragment_reference - .snake_case_fragment - .in_fragment - .unwrap(), - "value" - ); + assert_eq!(valid_fragment_reference.in_fragment.unwrap(), "value"); } #[derive(GraphQLQuery)] @@ -64,11 +52,9 @@ fn recursive_fragment() { let _ = RecursiveFragment { head: Some("ABCD".to_string()), - tail: Some(RecursiveFragmentTail { - recursive_fragment: Box::new(RecursiveFragment { - head: Some("EFGH".to_string()), - tail: None, - }), - }), + tail: Some(Box::new(RecursiveFragment { + head: Some("EFGH".to_string()), + tail: None, + })), }; } diff --git a/graphql_client/tests/input_object_variables.rs b/graphql_client/tests/input_object_variables.rs index 573ca5247..6d8e58e3e 100644 --- a/graphql_client/tests/input_object_variables.rs +++ b/graphql_client/tests/input_object_variables.rs @@ -39,12 +39,13 @@ fn input_object_variables_default() { msg: default_input_object_variables_query::Variables::default_msg(), }; - let out = serde_json::to_string(&variables).unwrap(); + let out = serde_json::to_value(&variables).unwrap(); - assert_eq!( - out, - r#"{"msg":{"content":null,"to":{"category":null,"email":"rosa.luxemburg@example.com","name":null}}}"#, - ); + let expected_default = serde_json::json!({ + "msg":{"content":null,"to":{"category":null,"email":"rosa.luxemburg@example.com","name":null}} + }); + + assert_eq!(out, expected_default); } #[derive(GraphQLQuery)] 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 index ec364f4fb..471177ed9 100644 --- a/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql +++ b/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql @@ -1,4 +1,4 @@ -fragment Birthday on Person { +fragment BirthdayFragment on Person { birthday } @@ -9,7 +9,7 @@ query QueryOnInterface { ... on Dog { isGoodDog } - ...Birthday + ...BirthdayFragment ... on Organization { industry } diff --git a/graphql_client/tests/union_query.rs b/graphql_client/tests/union_query.rs index 10c0c6b59..87d4c976e 100644 --- a/graphql_client/tests/union_query.rs +++ b/graphql_client/tests/union_query.rs @@ -1,6 +1,7 @@ use graphql_client::*; const RESPONSE: &str = include_str!("unions/union_query_response.json"); +const FRAGMENT_AND_MORE_RESPONSE: &str = include_str!("unions/fragment_and_more_response.json"); #[derive(GraphQLQuery)] #[graphql( @@ -18,6 +19,14 @@ pub struct UnionQuery; )] pub struct FragmentOnUnion; +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/unions/union_query.graphql", + schema_path = "tests/unions/union_schema.graphql", + response_derives = "PartialEq, Debug" +)] +pub struct FragmentAndMoreOnUnion; + #[test] fn union_query_deserialization() { let response_data: union_query::ResponseData = serde_json::from_str(RESPONSE).unwrap(); @@ -53,26 +62,63 @@ fn fragment_on_union() { let expected = fragment_on_union::ResponseData { names: Some(vec![ - fragment_on_union::FragmentOnUnionNames::Person( - fragment_on_union::FragmentOnUnionNamesOnPerson { - first_name: "Audrey".to_string(), - }, - ), - fragment_on_union::FragmentOnUnionNames::Dog( - fragment_on_union::FragmentOnUnionNamesOnDog { - name: "Laïka".to_string(), - }, - ), - fragment_on_union::FragmentOnUnionNames::Organization( - fragment_on_union::FragmentOnUnionNamesOnOrganization { + fragment_on_union::NamesFragment::Person(fragment_on_union::NamesFragmentOnPerson { + first_name: "Audrey".to_string(), + }), + fragment_on_union::NamesFragment::Dog(fragment_on_union::NamesFragmentOnDog { + name: "Laïka".to_string(), + }), + fragment_on_union::NamesFragment::Organization( + fragment_on_union::NamesFragmentOnOrganization { title: "Mozilla".to_string(), }, ), - fragment_on_union::FragmentOnUnionNames::Dog( - fragment_on_union::FragmentOnUnionNamesOnDog { - name: "Norbert".to_string(), - }, - ), + fragment_on_union::NamesFragment::Dog(fragment_on_union::NamesFragmentOnDog { + name: "Norbert".to_string(), + }), + ]), + }; + + assert_eq!(response_data, expected); +} + +#[test] +fn fragment_and_more_on_union() { + use fragment_and_more_on_union::*; + + let response_data: fragment_and_more_on_union::ResponseData = + serde_json::from_str(FRAGMENT_AND_MORE_RESPONSE).unwrap(); + + let expected = fragment_and_more_on_union::ResponseData { + names: Some(vec![ + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Person(NamesFragmentOnPerson { + first_name: "Larry".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Person, + }, + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Dog(NamesFragmentOnDog { + name: "Laïka".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Dog(FragmentAndMoreOnUnionNamesOnDog { + is_good_dog: true, + }), + }, + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Organization(NamesFragmentOnOrganization { + title: "Mozilla".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Organization, + }, + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Dog(NamesFragmentOnDog { + name: "Norbert".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Dog(FragmentAndMoreOnUnionNamesOnDog { + is_good_dog: true, + }), + }, ]), }; diff --git a/graphql_client/tests/unions/fragment_and_more_response.json b/graphql_client/tests/unions/fragment_and_more_response.json new file mode 100644 index 000000000..067cb0ab2 --- /dev/null +++ b/graphql_client/tests/unions/fragment_and_more_response.json @@ -0,0 +1,22 @@ +{ + "names": [ + { + "__typename": "Person", + "firstName": "Larry" + }, + { + "__typename": "Dog", + "name": "Laïka", + "isGoodDog": true + }, + { + "__typename": "Organization", + "title": "Mozilla" + }, + { + "__typename": "Dog", + "name": "Norbert", + "isGoodDog": true + } + ] +} diff --git a/graphql_client/tests/unions/union_query.graphql b/graphql_client/tests/unions/union_query.graphql index 28387924f..aec63d083 100644 --- a/graphql_client/tests/unions/union_query.graphql +++ b/graphql_client/tests/unions/union_query.graphql @@ -32,3 +32,12 @@ query FragmentOnUnion { ...NamesFragment } } + +query FragmentAndMoreOnUnion { + names { + ...NamesFragment + ... on Dog { + isGoodDog + } + } +} diff --git a/graphql_client_cli/Cargo.toml b/graphql_client_cli/Cargo.toml index 7857e0b25..d654704cb 100644 --- a/graphql_client_cli/Cargo.toml +++ b/graphql_client_cli/Cargo.toml @@ -14,7 +14,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" reqwest = "^0.9" -graphql_client = { version = "0.9.0", path = "../graphql_client" } +graphql_client = { version = "0.9.0", path = "../graphql_client", features = [] } graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.9.0" } structopt = "0.3" serde = { version = "^1.0", features = ["derive"] } diff --git a/graphql_client_cli/src/generate.rs b/graphql_client_cli/src/generate.rs index f32fa0cc7..be23e3703 100644 --- a/graphql_client_cli/src/generate.rs +++ b/graphql_client_cli/src/generate.rs @@ -59,7 +59,7 @@ pub(crate) fn generate_code(params: CliCodegenParams) -> Result<()> { options.set_deprecation_strategy(deprecation_strategy); } - let gen = generate_module_token_stream(query_path.clone(), &schema_path, options).map_err(|fail| fail.compat())?; + let gen = generate_module_token_stream(query_path.clone(), &schema_path, options)?; let generated_code = gen.to_string(); let generated_code = if cfg!(feature = "rustfmt") && !no_formatting { @@ -103,5 +103,5 @@ fn format(codes: &str) -> String { return String::from_utf8(out).unwrap(); } #[cfg(not(feature = "rustfmt"))] - unreachable!() + unreachable!("called format() without the rustfmt feature") } diff --git a/graphql_client_cli/src/introspect_schema.rs b/graphql_client_cli/src/introspect_schema.rs index 4650e05f1..4954fb463 100644 --- a/graphql_client_cli/src/introspect_schema.rs +++ b/graphql_client_cli/src/introspect_schema.rs @@ -8,7 +8,8 @@ use std::str::FromStr; #[graphql( schema_path = "src/graphql/introspection_schema.graphql", query_path = "src/graphql/introspection_query.graphql", - response_derives = "Serialize" + response_derives = "Serialize", + variable_derives = "Deserialize" )] #[allow(dead_code)] struct IntrospectionQuery; @@ -23,7 +24,7 @@ pub fn introspect_schema( let out: Box = match output { Some(path) => Box::new(::std::fs::File::create(path)?), - None => Box::new(::std::io::stdout()), + None => Box::new(std::io::stdout()), }; let request_body: graphql_client::QueryBody<()> = graphql_client::QueryBody { diff --git a/graphql_client_codegen/.gitignore b/graphql_client_codegen/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/graphql_client_codegen/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/graphql_client_codegen/Cargo.toml b/graphql_client_codegen/Cargo.toml index 954816c5b..1906e1c1b 100644 --- a/graphql_client_codegen/Cargo.toml +++ b/graphql_client_codegen/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/graphql-rust/graphql-client" edition = "2018" [dependencies] -failure = "0.1" +anyhow = "1.0" graphql-introspection-query = { version = "0.1.0", path = "../graphql-introspection-query" } graphql-parser = "^0.2" heck = "0.3" @@ -18,3 +18,4 @@ quote = "^1.0" serde_json = "1.0" serde = { version = "^1.0", features = ["derive"] } syn = "^1.0" +thiserror = "1.0.10" diff --git a/graphql_client_codegen/src/codegen.rs b/graphql_client_codegen/src/codegen.rs index 3ab9a0ba4..3feece889 100644 --- a/graphql_client_codegen/src/codegen.rs +++ b/graphql_client_codegen/src/codegen.rs @@ -1,163 +1,48 @@ -use crate::fragments::GqlFragment; -use crate::normalization::Normalization; -use crate::operations::Operation; -use crate::query::QueryContext; -use crate::schema; -use crate::selection::Selection; -use failure::*; -use graphql_parser::query; -use proc_macro2::TokenStream; -use quote::*; - -/// Selects the first operation matching `struct_name`. Returns `None` when the query document defines no operation, or when the selected operation does not match any defined operation. -pub(crate) fn select_operation<'query>( - query: &'query query::Document, - struct_name: &str, - norm: Normalization, -) -> Option> { - let operations = all_operations(query); - - operations - .iter() - .find(|op| norm.operation(&op.name) == struct_name) - .map(ToOwned::to_owned) -} +mod enums; +mod inputs; +mod selection; +mod shared; -pub(crate) fn all_operations(query: &query::Document) -> Vec> { - let mut operations: Vec> = Vec::new(); - - for definition in &query.definitions { - if let query::Definition::Operation(op) = definition { - operations.push(op.into()); - } - } - operations -} +use crate::{ + query::*, + schema::{InputId, TypeId}, + type_qualifiers::GraphqlTypeQualifier, + GraphQLClientCodegenOptions, +}; +use heck::SnakeCase; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use selection::*; +use std::collections::BTreeMap; /// The main code generation function. pub(crate) fn response_for_query( - schema: &schema::Schema<'_>, - query: &query::Document, - operation: &Operation<'_>, - options: &crate::GraphQLClientCodegenOptions, -) -> Result { - let mut context = QueryContext::new( - schema, - options.deprecation_strategy(), - options.normalization(), - ); + operation_id: OperationId, + options: &GraphQLClientCodegenOptions, + query: BoundQuery<'_>, +) -> anyhow::Result { + let all_used_types = all_used_types(operation_id, &query); + let response_derives = render_derives(options.all_response_derives()); + let variable_derives = render_derives(options.all_variable_derives()); - if let Some(derives) = options.variables_derives() { - context.ingest_variables_derives(&derives)?; - } - - if let Some(derives) = options.response_derives() { - context.ingest_response_derives(&derives)?; - } - - let mut definitions = Vec::new(); - - for definition in &query.definitions { - match definition { - query::Definition::Operation(_op) => (), - query::Definition::Fragment(fragment) => { - let &query::TypeCondition::On(ref on) = &fragment.type_condition; - let on = schema.fragment_target(on).ok_or_else(|| { - format_err!( - "Fragment {} is defined on unknown type: {}", - &fragment.name, - on, - ) - })?; - context.fragments.insert( - &fragment.name, - GqlFragment { - name: &fragment.name, - selection: Selection::from(&fragment.selection_set), - on, - is_required: false.into(), - }, - ); - } - } - } + let scalar_definitions = generate_scalar_definitions(&all_used_types, options, query); + let enum_definitions = enums::generate_enum_definitions(&all_used_types, options, query); + let fragment_definitions = + generate_fragment_definitions(&all_used_types, &response_derives, options, &query); + let input_object_definitions = inputs::generate_input_object_definitions( + &all_used_types, + options, + &variable_derives, + &query, + ); - let response_data_fields = { - let root_name = operation.root_name(&context.schema); - let opt_definition = context.schema.objects.get(&root_name); - let definition = if let Some(definition) = opt_definition { - definition - } else { - panic!( - "operation type '{:?}' not in schema", - operation.operation_type - ); - }; - let prefix = &operation.name; - let selection = &operation.selection; - - if operation.is_subscription() && selection.len() > 1 { - return Err(format_err!( - "{}", - crate::constants::MULTIPLE_SUBSCRIPTION_FIELDS_ERROR - )); - } + let variables_struct = + generate_variables_struct(operation_id, &variable_derives, options, &query); - definitions.extend(definition.field_impls_for_selection(&context, &selection, &prefix)?); - definition.response_fields_for_selection(&context, &selection, &prefix)? - }; + let definitions = + render_response_data_fields(operation_id, options, &query).render(&response_derives); - let enum_definitions = context.schema.enums.values().filter_map(|enm| { - if enm.is_required.get() { - Some(enm.to_rust(&context)) - } else { - None - } - }); - let fragment_definitions: Result, _> = context - .fragments - .values() - .filter_map(|fragment| { - if fragment.is_required.get() { - Some(fragment.to_rust(&context)) - } else { - None - } - }) - .collect(); - let fragment_definitions = fragment_definitions?; - let variables_struct = operation.expand_variables(&context); - - let input_object_definitions: Result, _> = context - .schema - .inputs - .values() - .filter_map(|i| { - if i.is_required.get() { - Some(i.to_rust(&context)) - } else { - None - } - }) - .collect(); - let input_object_definitions = input_object_definitions?; - - let scalar_definitions: Vec = context - .schema - .scalars - .values() - .filter_map(|s| { - if s.is_required.get() { - Some(s.to_rust(context.normalization)) - } else { - None - } - }) - .collect(); - - let response_derives = context.response_derives(); - - Ok(quote! { + let q = quote! { use serde::{Serialize, Deserialize}; #[allow(dead_code)] @@ -171,21 +56,260 @@ pub(crate) fn response_for_query( #(#scalar_definitions)* + #(#enum_definitions)* + #(#input_object_definitions)* - #(#enum_definitions)* + #variables_struct #(#fragment_definitions)* - #(#definitions)* + #definitions + }; - #variables_struct + Ok(q) +} + +fn generate_variables_struct( + operation_id: OperationId, + variable_derives: &impl quote::ToTokens, + options: &GraphQLClientCodegenOptions, + query: &BoundQuery<'_>, +) -> TokenStream { + if operation_has_no_variables(operation_id, query.query) { + return quote!( + #variable_derives + pub struct Variables; + ); + } + + let variable_fields = walk_operation_variables(operation_id, query.query) + .map(|(_id, variable)| generate_variable_struct_field(variable, options, query)); + let variable_defaults = + walk_operation_variables(operation_id, query.query).map(|(_id, variable)| { + let method_name = format!("default_{}", variable.name); + let method_name = Ident::new(&method_name, Span::call_site()); + let method_return_type = render_variable_field_type(variable, options, query); + + variable.default.as_ref().map(|default| { + let value = graphql_parser_value_to_literal( + default, + variable.r#type.id, + variable + .r#type + .qualifiers + .get(0) + .map(|qual| !qual.is_required()) + .unwrap_or(true), + query, + ); + + quote!( + pub fn #method_name() -> #method_return_type { + #value + } + ) + }) + }); + + let variables_struct = quote!( + #variable_derives + pub struct Variables { + #(#variable_fields,)* + } - #response_derives + impl Variables { + #(#variable_defaults)* + } + ); + + variables_struct +} + +fn generate_variable_struct_field( + variable: &ResolvedVariable, + options: &GraphQLClientCodegenOptions, + query: &BoundQuery<'_>, +) -> TokenStream { + let snake_case_name = variable.name.to_snake_case(); + let ident = Ident::new( + &shared::keyword_replace(&snake_case_name), + Span::call_site(), + ); + let annotation = shared::field_rename_annotation(&variable.name, &snake_case_name); + let r#type = render_variable_field_type(variable, options, query); + + quote::quote!(#annotation pub #ident : #r#type) +} + +fn generate_scalar_definitions<'a, 'schema: 'a>( + all_used_types: &'a crate::query::UsedTypes, + options: &'a GraphQLClientCodegenOptions, + query: BoundQuery<'schema>, +) -> impl Iterator + 'a { + all_used_types + .scalars(query.schema) + .map(move |(_id, scalar)| { + let ident = syn::Ident::new( + options.normalization().scalar_name(&scalar.name).as_ref(), + proc_macro2::Span::call_site(), + ); + + quote!(type #ident = super::#ident;) + }) +} - pub struct ResponseData { - #(#response_data_fields,)* +fn render_derives<'a>(derives: impl Iterator) -> impl quote::ToTokens { + let idents = derives.map(|s| Ident::new(s, Span::call_site())); + + quote!(#[derive(#(#idents),*)]) +} + +fn render_variable_field_type( + variable: &ResolvedVariable, + options: &GraphQLClientCodegenOptions, + query: &BoundQuery<'_>, +) -> TokenStream { + let normalized_name = options + .normalization() + .input_name(variable.type_name(query.schema)); + let full_name = Ident::new(normalized_name.as_ref(), Span::call_site()); + + decorate_type(&full_name, &variable.r#type.qualifiers) +} + +fn decorate_type(ident: &Ident, qualifiers: &[GraphqlTypeQualifier]) -> TokenStream { + let mut qualified = quote!(#ident); + + let mut non_null = false; + + // Note: we iterate over qualifiers in reverse because it is more intuitive. This + // means we start from the _inner_ type and make our way to the outside. + for qualifier in qualifiers.iter().rev() { + match (non_null, qualifier) { + // We are in non-null context, and we wrap the non-null type into a list. + // We switch back to null context. + (true, GraphqlTypeQualifier::List) => { + qualified = quote!(Vec<#qualified>); + non_null = false; + } + // We are in nullable context, and we wrap the nullable type into a list. + (false, GraphqlTypeQualifier::List) => { + qualified = quote!(Vec>); + } + // We are in non-nullable context, but we can't double require a type + // (!!). + (true, GraphqlTypeQualifier::Required) => panic!("double required annotation"), + // We are in nullable context, and we switch to non-nullable context. + (false, GraphqlTypeQualifier::Required) => { + non_null = true; + } } + } + + // If we are in nullable context at the end of the iteration, we wrap the whole + // type with an Option. + if !non_null { + qualified = quote!(Option<#qualified>); + } + + qualified +} + +fn generate_fragment_definitions<'a>( + all_used_types: &'a UsedTypes, + response_derives: &'a impl quote::ToTokens, + options: &'a GraphQLClientCodegenOptions, + query: &'a BoundQuery<'a>, +) -> impl Iterator + 'a { + all_used_types.fragment_ids().map(move |fragment_id| { + selection::render_fragment(fragment_id, options, query).render(&response_derives) + }) +} + +/// For default value constructors. +fn graphql_parser_value_to_literal( + value: &graphql_parser::query::Value, + ty: TypeId, + is_optional: bool, + query: &BoundQuery<'_>, +) -> TokenStream { + use graphql_parser::query::Value; + + let inner = match value { + Value::Boolean(b) => { + if *b { + quote!(true) + } else { + quote!(false) + } + } + Value::String(s) => quote!(#s.to_string()), + Value::Variable(_) => panic!("variable in variable"), + Value::Null => panic!("null as default value"), + Value::Float(f) => quote!(#f), + Value::Int(i) => { + let i = i.as_i64(); + quote!(#i) + } + Value::Enum(en) => quote!(#en), + Value::List(inner) => { + let elements = inner + .iter() + .map(|val| graphql_parser_value_to_literal(val, ty, false, query)); + quote! { + vec![ + #(#elements,)* + ] + } + } + Value::Object(obj) => ty + .as_input_id() + .map(|input_id| render_object_literal(obj, input_id, query)) + .unwrap_or_else(|| { + quote!(compile_error!( + "Object literal on a non-input-object field." + )) + }), + }; + + if is_optional { + quote!(Some(#inner)) + } else { + inner + } +} + +/// For default value constructors. +fn render_object_literal( + object_map: &BTreeMap, + input_id: InputId, + query: &BoundQuery<'_>, +) -> TokenStream { + let input = query.schema.get_input(input_id); + let constructor = Ident::new(&input.name, Span::call_site()); + let fields: Vec = input + .fields + .iter() + .map(|(name, r#type)| { + let field_name = Ident::new(&name, Span::call_site()); + let provided_value = object_map.get(name); + match provided_value { + Some(default_value) => { + let value = graphql_parser_value_to_literal( + default_value, + r#type.id, + r#type.is_optional(), + query, + ); + quote!(#field_name: #value) + } + None => quote!(#field_name: None), + } + }) + .collect(); + quote!(#constructor { + #(#fields,)* }) } diff --git a/graphql_client_codegen/src/codegen/enums.rs b/graphql_client_codegen/src/codegen/enums.rs new file mode 100644 index 000000000..d36992e6c --- /dev/null +++ b/graphql_client_codegen/src/codegen/enums.rs @@ -0,0 +1,85 @@ +use crate::{ + codegen::render_derives, codegen_options::GraphQLClientCodegenOptions, query::BoundQuery, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +/** + * About rust keyword escaping: variant_names and constructors must be escaped, + * variant_str not. + * Example schema: enum AnEnum { where \n self } + * Generated "variant_names" enum: pub enum AnEnum { where_, self_, Other(String), } + * Generated serialize line: "AnEnum::where_ => "where"," + */ +pub(super) fn generate_enum_definitions<'a, 'schema: 'a>( + all_used_types: &'a crate::query::UsedTypes, + options: &'a GraphQLClientCodegenOptions, + query: BoundQuery<'schema>, +) -> impl Iterator + 'a { + let derives = render_derives( + options + .all_response_derives() + .filter(|d| !&["Serialize", "Deserialize", "Default"].contains(d)), + ); + let normalization = options.normalization(); + + all_used_types.enums(query.schema).map(move |(_id, r#enum)| { + let variant_names: Vec = r#enum + .variants + .iter() + .map(|v| { + let safe_name = super::shared::keyword_replace(v.as_str()); + let name = normalization.enum_variant(safe_name.as_ref()); + let name = Ident::new(&name, Span::call_site()); + + quote!(#name) + }) + .collect(); + let variant_names = &variant_names; + let name_ident = normalization.enum_name(r#enum.name.as_str()); + let name_ident = Ident::new(&name_ident, Span::call_site()); + let constructors: Vec<_> = r#enum + .variants + .iter() + .map(|v| { + let safe_name = super::shared::keyword_replace(v); + let name = normalization.enum_variant(safe_name.as_ref()); + let v = Ident::new(&name, Span::call_site()); + + quote!(#name_ident::#v) + }) + .collect(); + let constructors = &constructors; + let variant_str: Vec<&str> = r#enum.variants.iter().map(|s| s.as_str()).collect(); + let variant_str = &variant_str; + + let name = name_ident; + + quote! { + #derives + pub enum #name { + #(#variant_names,)* + Other(String), + } + + impl ::serde::Serialize for #name { + fn serialize(&self, ser: S) -> Result { + ser.serialize_str(match *self { + #(#constructors => #variant_str,)* + #name::Other(ref s) => &s, + }) + } + } + + impl<'de> ::serde::Deserialize<'de> for #name { + fn deserialize>(deserializer: D) -> Result { + let s = ::deserialize(deserializer)?; + + match s.as_str() { + #(#variant_str => Ok(#constructors),)* + _ => Ok(#name::Other(s)), + } + } + } + }}) +} diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs new file mode 100644 index 000000000..b82cacc12 --- /dev/null +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -0,0 +1,52 @@ +use super::shared::keyword_replace; +use crate::{ + codegen_options::GraphQLClientCodegenOptions, + query::{BoundQuery, UsedTypes}, + schema::input_is_recursive_without_indirection, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +pub(super) fn generate_input_object_definitions( + all_used_types: &UsedTypes, + options: &GraphQLClientCodegenOptions, + variable_derives: &impl quote::ToTokens, + query: &BoundQuery<'_>, +) -> Vec { + all_used_types + .inputs(query.schema) + .map(|(_input_id, input)| { + let normalized_name = options.normalization().input_name(input.name.as_str()); + let safe_name = keyword_replace(normalized_name); + let struct_name = Ident::new(safe_name.as_ref(), Span::call_site()); + + let fields = input.fields.iter().map(|(field_name, field_type)| { + let safe_field_name = keyword_replace(field_name); + let name_ident = Ident::new(safe_field_name.as_ref(), Span::call_site()); + let normalized_field_type_name = options + .normalization() + .field_type(field_type.id.name(query.schema)); + let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); + let field_type_tokens = super::decorate_type(&type_name, &field_type.qualifiers); + let field_type = if field_type + .id + .as_input_id() + .map(|input_id| input_is_recursive_without_indirection(input_id, query.schema)) + .unwrap_or(false) + { + quote!(Box<#field_type_tokens>) + } else { + field_type_tokens + }; + quote!(pub #name_ident: #field_type) + }); + + quote! { + #variable_derives + pub struct #struct_name { + #(#fields,)* + } + } + }) + .collect() +} diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs new file mode 100644 index 000000000..1a6d1bea3 --- /dev/null +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -0,0 +1,584 @@ +//! Code generation for the selection on an operation or a fragment. + +use crate::{ + codegen::{ + decorate_type, + shared::{field_rename_annotation, keyword_replace}, + }, + deprecation::DeprecationStrategy, + query::{ + fragment_is_recursive, full_path_prefix, BoundQuery, InlineFragment, OperationId, + ResolvedFragment, ResolvedFragmentId, SelectedField, Selection, SelectionId, + }, + schema::{Schema, TypeId}, + type_qualifiers::GraphqlTypeQualifier, + GraphQLClientCodegenOptions, +}; +use heck::*; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use std::borrow::Cow; + +pub(crate) fn render_response_data_fields<'a>( + operation_id: OperationId, + options: &'a GraphQLClientCodegenOptions, + query: &'a BoundQuery<'a>, +) -> ExpandedSelection<'a> { + let operation = query.query.get_operation(operation_id); + let mut expanded_selection = ExpandedSelection { + query, + types: Vec::with_capacity(8), + aliases: Vec::new(), + variants: Vec::new(), + fields: Vec::with_capacity(operation.selection_set.len()), + options, + }; + + let response_data_type_id = expanded_selection.push_type(ExpandedType { + name: Cow::Borrowed("ResponseData"), + }); + + calculate_selection( + &mut expanded_selection, + &operation.selection_set, + response_data_type_id, + TypeId::Object(operation.object_id), + options, + ); + + expanded_selection +} + +pub(super) fn render_fragment<'a>( + fragment_id: ResolvedFragmentId, + options: &'a GraphQLClientCodegenOptions, + query: &'a BoundQuery<'a>, +) -> ExpandedSelection<'a> { + let fragment = query.query.get_fragment(fragment_id); + let mut expanded_selection = ExpandedSelection { + query, + aliases: Vec::new(), + types: Vec::with_capacity(8), + variants: Vec::new(), + fields: Vec::with_capacity(fragment.selection_set.len()), + options, + }; + + let response_type_id = expanded_selection.push_type(ExpandedType { + name: fragment.name.as_str().into(), + }); + + calculate_selection( + &mut expanded_selection, + &fragment.selection_set, + response_type_id, + fragment.on, + options, + ); + + expanded_selection +} + +/// A sub-selection set (spread) on one of the variants of a union or interface. +enum VariantSelection<'a> { + InlineFragment(&'a InlineFragment), + FragmentSpread((ResolvedFragmentId, &'a ResolvedFragment)), +} + +impl<'a> VariantSelection<'a> { + /// The second argument is the parent type id, so it can be excluded. + fn from_selection( + selection: &'a Selection, + type_id: TypeId, + query: &BoundQuery<'a>, + ) -> Option> { + match selection { + Selection::InlineFragment(inline_fragment) => { + Some(VariantSelection::InlineFragment(inline_fragment)) + } + Selection::FragmentSpread(fragment_id) => { + let fragment = query.query.get_fragment(*fragment_id); + + if fragment.on == type_id { + // The selection is on the type itself. + None + } else { + // The selection is on one of the variants of the type. + Some(VariantSelection::FragmentSpread((*fragment_id, fragment))) + } + } + Selection::Field(_) | Selection::Typename => None, + } + } + + fn variant_type_id(&self) -> TypeId { + match self { + VariantSelection::InlineFragment(f) => f.type_id, + VariantSelection::FragmentSpread((_id, f)) => f.on, + } + } +} + +fn calculate_selection<'a>( + context: &mut ExpandedSelection<'a>, + selection_set: &[SelectionId], + struct_id: ResponseTypeId, + type_id: TypeId, + options: &'a GraphQLClientCodegenOptions, +) { + // If the selection only contains a fragment, replace the selection with + // that fragment. + if selection_set.len() == 1 { + if let Selection::FragmentSpread(fragment_id) = + context.query.query.get_selection(selection_set[0]) + { + let fragment = context.query.query.get_fragment(*fragment_id); + context.push_type_alias(TypeAlias { + name: &fragment.name, + struct_id, + boxed: fragment_is_recursive(*fragment_id, context.query.query), + }); + return; + } + } + + // If we are on a union or an interface, we need to generate an enum that matches the variants _exhaustively_. + { + let variants: Option> = match type_id { + TypeId::Interface(interface_id) => { + let variants = context + .query + .schema + .objects() + .filter(|(_, obj)| obj.implements_interfaces.contains(&interface_id)) + .map(|(id, _)| TypeId::Object(id)); + + Some(variants.collect::>().into()) + } + TypeId::Union(union_id) => { + let union = context.schema().get_union(union_id); + Some(union.variants.as_slice().into()) + } + _ => None, + }; + + if let Some(variants) = variants { + let variant_selections: Vec<(SelectionId, &Selection, VariantSelection<'_>)> = + selection_set + .iter() + .map(|id| (id, context.query.query.get_selection(*id))) + .filter_map(|(id, selection)| { + VariantSelection::from_selection(&selection, type_id, context.query) + .map(|variant_selection| (*id, selection, variant_selection)) + }) + .collect(); + + // For each variant, get the corresponding fragment spreads and + // inline fragments, or default to an empty variant (one with no + // associated data). + for variant_type_id in variants.as_ref() { + let variant_name_str = variant_type_id.name(context.schema()); + + let variant_selections: Vec<_> = variant_selections + .iter() + .filter(|(_id, _selection_ref, variant)| { + variant.variant_type_id() == *variant_type_id + }) + .collect(); + + if let Some((selection_id, selection, _variant)) = variant_selections.get(0) { + let mut variant_struct_name_str = + full_path_prefix(*selection_id, &context.query); + variant_struct_name_str.reserve(2 + variant_name_str.len()); + variant_struct_name_str.push_str("On"); + variant_struct_name_str.push_str(variant_name_str); + + context.push_variant(ExpandedVariant { + name: variant_name_str.into(), + variant_type: Some(variant_struct_name_str.clone().into()), + on: struct_id, + }); + + let expanded_type = ExpandedType { + name: variant_struct_name_str.into(), + }; + + let struct_id = context.push_type(expanded_type); + + if variant_selections.len() == 1 { + if let VariantSelection::FragmentSpread((fragment_id, fragment)) = + variant_selections[0].2 + { + context.push_type_alias(TypeAlias { + boxed: fragment_is_recursive(fragment_id, context.query.query), + name: &fragment.name, + struct_id, + }); + continue; + } + } + + for (_selection_id, _selection, variant_selection) in variant_selections { + match variant_selection { + VariantSelection::InlineFragment(_) => { + calculate_selection( + context, + selection.subselection(), + struct_id, + *variant_type_id, + options, + ); + } + VariantSelection::FragmentSpread((fragment_id, fragment)) => context + .push_field(ExpandedField { + field_type: fragment.name.as_str().into(), + field_type_qualifiers: &[GraphqlTypeQualifier::Required], + flatten: true, + graphql_name: None, + rust_name: fragment.name.to_snake_case().into(), + struct_id, + deprecation: None, + boxed: fragment_is_recursive(*fragment_id, context.query.query), + }), + } + } + } else { + context.push_variant(ExpandedVariant { + name: variant_name_str.into(), + on: struct_id, + variant_type: None, + }); + } + } + } + } + + for id in selection_set { + let selection = context.query.query.get_selection(*id); + + match selection { + Selection::Field(field) => { + let (graphql_name, rust_name) = context.field_name(&field); + let schema_field = field.schema_field(context.schema()); + let field_type_id = schema_field.r#type.id; + + match field_type_id { + TypeId::Enum(enm) => { + context.push_field(ExpandedField { + graphql_name: Some(graphql_name), + rust_name, + struct_id, + field_type: options + .normalization() + .field_type(&context.schema().get_enum(enm).name), + field_type_qualifiers: &schema_field.r#type.qualifiers, + flatten: false, + deprecation: schema_field.deprecation(), + boxed: false, + }); + } + TypeId::Scalar(scalar) => { + context.push_field(ExpandedField { + field_type: options + .normalization() + .field_type(context.schema().get_scalar(scalar).name.as_str()), + field_type_qualifiers: &field + .schema_field(context.schema()) + .r#type + .qualifiers, + graphql_name: Some(graphql_name), + struct_id, + rust_name, + flatten: false, + deprecation: schema_field.deprecation(), + boxed: false, + }); + } + TypeId::Object(_) | TypeId::Interface(_) | TypeId::Union(_) => { + let struct_name_string = full_path_prefix(*id, &context.query); + + context.push_field(ExpandedField { + struct_id, + graphql_name: Some(graphql_name), + rust_name, + field_type_qualifiers: &schema_field.r#type.qualifiers, + field_type: Cow::Owned(struct_name_string.clone()), + flatten: false, + boxed: false, + deprecation: schema_field.deprecation(), + }); + + let type_id = context.push_type(ExpandedType { + name: Cow::Owned(struct_name_string), + }); + + calculate_selection( + context, + selection.subselection(), + type_id, + field_type_id, + options, + ); + } + TypeId::Input(_) => unreachable!("field selection on input type"), + }; + } + Selection::Typename => (), + Selection::InlineFragment(_inline) => (), + Selection::FragmentSpread(fragment_id) => { + // Here we only render fragments that are directly on the type + // itself, and not on one of its variants. + + let fragment = context.query.query.get_fragment(*fragment_id); + + // Assuming the query was validated properly, a fragment spread + // is either on the field's type itself, or on one of the + // variants (union or interfaces). If it's not directly a field + // on the struct, it will be handled in the `on` variants. + if fragment.on != type_id { + continue; + } + + let original_field_name = fragment.name.to_snake_case(); + let final_field_name = keyword_replace(original_field_name); + + context.push_field(ExpandedField { + field_type: fragment.name.as_str().into(), + field_type_qualifiers: &[GraphqlTypeQualifier::Required], + graphql_name: None, + rust_name: final_field_name, + struct_id, + flatten: true, + deprecation: None, + boxed: fragment_is_recursive(*fragment_id, context.query.query), + }); + + // We stop here, because the structs for the fragments are generated separately, to + // avoid duplication. + } + } + } +} + +#[derive(Clone, Copy, PartialEq)] +struct ResponseTypeId(u32); + +struct TypeAlias<'a> { + name: &'a str, + struct_id: ResponseTypeId, + boxed: bool, +} + +struct ExpandedField<'a> { + graphql_name: Option<&'a str>, + rust_name: Cow<'a, str>, + field_type: Cow<'a, str>, + field_type_qualifiers: &'a [GraphqlTypeQualifier], + struct_id: ResponseTypeId, + flatten: bool, + deprecation: Option>, + boxed: bool, +} + +impl<'a> ExpandedField<'a> { + fn render(&self, options: &GraphQLClientCodegenOptions) -> Option { + let ident = Ident::new(&self.rust_name, Span::call_site()); + let qualified_type = decorate_type( + &Ident::new(&self.field_type, Span::call_site()), + self.field_type_qualifiers, + ); + + let qualified_type = if self.boxed { + quote!(Box<#qualified_type>) + } else { + qualified_type + }; + + let optional_rename = self + .graphql_name + .as_ref() + .map(|graphql_name| field_rename_annotation(graphql_name, &self.rust_name)); + let optional_flatten = if self.flatten { + Some(quote!(#[serde(flatten)])) + } else { + None + }; + + let optional_deprecation_annotation = + match (self.deprecation, options.deprecation_strategy()) { + (None, _) | (Some(_), DeprecationStrategy::Allow) => None, + (Some(msg), DeprecationStrategy::Warn) => { + let optional_msg = msg.map(|msg| quote!((note = #msg))); + + Some(quote!(#[deprecated#optional_msg])) + } + (Some(_), DeprecationStrategy::Deny) => return None, + }; + + let tokens = quote! { + #optional_flatten + #optional_rename + #optional_deprecation_annotation + pub #ident: #qualified_type + }; + + Some(tokens) + } +} + +struct ExpandedVariant<'a> { + name: Cow<'a, str>, + variant_type: Option>, + on: ResponseTypeId, +} + +impl<'a> ExpandedVariant<'a> { + fn render(&self) -> TokenStream { + let name_ident = Ident::new(&self.name, Span::call_site()); + let optional_type_ident = self.variant_type.as_ref().map(|variant_type| { + let ident = Ident::new(&variant_type, Span::call_site()); + quote!((#ident)) + }); + + quote!(#name_ident #optional_type_ident) + } +} + +pub(crate) struct ExpandedType<'a> { + name: Cow<'a, str>, +} + +pub(crate) struct ExpandedSelection<'a> { + query: &'a BoundQuery<'a>, + types: Vec>, + fields: Vec>, + variants: Vec>, + aliases: Vec>, + options: &'a GraphQLClientCodegenOptions, +} + +impl<'a> ExpandedSelection<'a> { + pub(crate) fn schema(&self) -> &'a Schema { + self.query.schema + } + + fn push_type(&mut self, tpe: ExpandedType<'a>) -> ResponseTypeId { + let id = self.types.len(); + self.types.push(tpe); + + ResponseTypeId(id as u32) + } + + fn push_field(&mut self, field: ExpandedField<'a>) { + self.fields.push(field); + } + + fn push_type_alias(&mut self, alias: TypeAlias<'a>) { + self.aliases.push(alias) + } + + fn push_variant(&mut self, variant: ExpandedVariant<'a>) { + self.variants.push(variant); + } + + /// Returns a tuple to be interpreted as (graphql_name, rust_name). + pub(crate) fn field_name(&self, field: &'a SelectedField) -> (&'a str, Cow<'a, str>) { + let name = field + .alias() + .unwrap_or_else(|| &field.schema_field(self.query.schema).name); + let snake_case_name = name.to_snake_case(); + let final_name = keyword_replace(snake_case_name); + + (name, final_name) + } + + fn types(&self) -> impl Iterator)> { + self.types + .iter() + .enumerate() + .map(|(idx, ty)| (ResponseTypeId(idx as u32), ty)) + } + + pub fn render(&self, response_derives: &impl quote::ToTokens) -> TokenStream { + let mut items = Vec::with_capacity(self.types.len()); + + for (type_id, ty) in self.types() { + let struct_name = Ident::new(&ty.name, Span::call_site()); + + // If the type is aliased, stop here. + if let Some(alias) = self.aliases.iter().find(|alias| alias.struct_id == type_id) { + let fragment_name = Ident::new(&alias.name, Span::call_site()); + let fragment_name = if alias.boxed { + quote!(Box<#fragment_name>) + } else { + quote!(#fragment_name) + }; + let item = quote! { + pub type #struct_name = #fragment_name; + }; + items.push(item); + continue; + } + + let mut fields = self + .fields + .iter() + .filter(|field| field.struct_id == type_id) + .filter_map(|field| field.render(self.options)) + .peekable(); + + let on_variants: Vec = self + .variants + .iter() + .filter(|variant| variant.on == type_id) + .map(|variant| variant.render()) + .collect(); + + // If we only have an `on` field, turn the struct into the enum + // of the variants. + if fields.peek().is_none() { + let item = quote! { + #response_derives + #[serde(tag = "__typename")] + pub enum #struct_name { + #(#on_variants),* + } + }; + items.push(item); + continue; + } + + let (on_field, on_enum) = if !on_variants.is_empty() { + let enum_name = Ident::new(&format!("{}On", ty.name), Span::call_site()); + + let on_field = quote!(#[serde(flatten)] pub on: #enum_name); + + let on_enum = quote!( + #response_derives + #[serde(tag = "__typename")] + pub enum #enum_name { + #(#on_variants,)* + } + ); + + (Some(on_field), Some(on_enum)) + } else { + (None, None) + }; + + let tokens = quote! { + #response_derives + pub struct #struct_name { + #(#fields,)* + #on_field + } + + #on_enum + }; + + items.push(tokens); + } + + quote!(#(#items)*) + } +} diff --git a/graphql_client_codegen/src/codegen/shared.rs b/graphql_client_codegen/src/codegen/shared.rs new file mode 100644 index 000000000..2d95508a9 --- /dev/null +++ b/graphql_client_codegen/src/codegen/shared.rs @@ -0,0 +1,97 @@ +use proc_macro2::TokenStream; +use quote::quote; +use std::borrow::Cow; + +// List of keywords based on https://doc.rust-lang.org/grammar.html#keywords +const RUST_KEYWORDS: &[&str] = &[ + "abstract", + "alignof", + "as", + "async", + "await", + "become", + "box", + "break", + "const", + "continue", + "crate", + "do", + "else", + "enum", + "extern crate", + "extern", + "false", + "final", + "fn", + "for", + "for", + "if let", + "if", + "if", + "impl", + "impl", + "in", + "let", + "loop", + "macro", + "match", + "mod", + "move", + "mut", + "offsetof", + "override", + "priv", + "proc", + "pub", + "pure", + "ref", + "return", + "self", + "sizeof", + "static", + "struct", + "super", + "trait", + "true", + "type", + "typeof", + "unsafe", + "unsized", + "use", + "use", + "virtual", + "where", + "while", + "yield", +]; + +pub(crate) fn keyword_replace<'a>(needle: impl Into>) -> Cow<'a, str> { + let needle = needle.into(); + match RUST_KEYWORDS.binary_search(&needle.as_ref()) { + Ok(index) => [RUST_KEYWORDS[index], "_"].concat().into(), + Err(_) => needle, + } +} + +/// Given the GraphQL schema name for an object/interface/input object field and +/// the equivalent rust name, produces a serde annotation to map them during +/// (de)serialization if it is necessary, otherwise an empty TokenStream. +pub(crate) fn field_rename_annotation(graphql_name: &str, rust_name: &str) -> Option { + if graphql_name != rust_name { + Some(quote!(#[serde(rename = #graphql_name)])) + } else { + None + } +} + +#[cfg(test)] +mod tests { + #[test] + fn keyword_replace_works() { + use super::keyword_replace; + assert_eq!("fora", keyword_replace("fora")); + assert_eq!("in_", keyword_replace("in")); + assert_eq!("fn_", keyword_replace("fn")); + assert_eq!("struct_", keyword_replace("struct")); + } +} diff --git a/graphql_client_codegen/src/codegen_options.rs b/graphql_client_codegen/src/codegen_options.rs index 22cb02c6d..2d1e83579 100644 --- a/graphql_client_codegen/src/codegen_options.rs +++ b/graphql_client_codegen/src/codegen_options.rs @@ -87,9 +87,34 @@ impl GraphQLClientCodegenOptions { self.variables_derives = Some(variables_derives); } - /// Comma-separated list of additional traits we want to derive for responses. - pub fn response_derives(&self) -> Option<&str> { - self.response_derives.as_deref() + /// All the variable derives to be rendered. + pub fn all_variable_derives(&self) -> impl Iterator { + let additional = self + .variables_derives + .as_deref() + .into_iter() + .flat_map(|s| s.split(',')); + + std::iter::once("Serialize").chain(additional) + } + + /// Traits we want to derive for responses. + pub fn all_response_derives(&self) -> impl Iterator { + let base_derives = std::iter::once("Deserialize"); + + base_derives.chain( + self.additional_response_derives() + .filter(|additional| additional != &"Deserialize"), + ) + } + + /// Additional traits we want to derive for responses. + pub fn additional_response_derives(&self) -> impl Iterator { + self.response_derives + .as_deref() + .into_iter() + .flat_map(|s| s.split(',')) + .map(|s| s.trim()) } /// Comma-separated list of additional traits we want to derive for responses. @@ -146,7 +171,7 @@ impl GraphQLClientCodegenOptions { } /// The normalization mode for the generated code. - pub fn normalization(&self) -> Normalization { - self.normalization + pub fn normalization(&self) -> &Normalization { + &self.normalization } } diff --git a/graphql_client_codegen/src/constants.rs b/graphql_client_codegen/src/constants.rs index 5fc1ac1da..c051347fc 100644 --- a/graphql_client_codegen/src/constants.rs +++ b/graphql_client_codegen/src/constants.rs @@ -1,29 +1,5 @@ -use crate::deprecation::DeprecationStatus; -use crate::field_type::FieldType; -use crate::objects::GqlObjectField; - pub(crate) const TYPENAME_FIELD: &str = "__typename"; -pub(crate) fn string_type() -> &'static str { - "String" -} - -#[cfg(test)] -pub(crate) fn float_type() -> &'static str { - "Float" -} - -pub(crate) fn typename_field() -> GqlObjectField<'static> { - GqlObjectField { - description: None, - name: TYPENAME_FIELD, - /// Non-nullable, see spec: - /// https://github.com/facebook/graphql/blob/master/spec/Section%204%20--%20Introspection.md - type_: FieldType::new(string_type()), - deprecation: DeprecationStatus::Current, - } -} - pub(crate) const MULTIPLE_SUBSCRIPTION_FIELDS_ERROR: &str = r##" Multiple-field queries on the root subscription field are forbidden by the spec. diff --git a/graphql_client_codegen/src/enums.rs b/graphql_client_codegen/src/enums.rs deleted file mode 100644 index 563166c09..000000000 --- a/graphql_client_codegen/src/enums.rs +++ /dev/null @@ -1,95 +0,0 @@ -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; - -pub const ENUMS_PREFIX: &str = ""; - -#[derive(Debug, Clone, PartialEq)] -pub struct EnumVariant<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct GqlEnum<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, - pub variants: Vec>, - pub is_required: Cell, -} - -impl<'schema> GqlEnum<'schema> { - /** - * About rust keyword escaping: variant_names and constructors must be escaped, - * variant_str not. - * Example schema: enum AnEnum { where \n self } - * Generated "variant_names" enum: pub enum AnEnum { where_, self_, Other(String), } - * Generated serialize line: "AnEnum::where_ => "where"," - */ - pub(crate) fn to_rust( - &self, - query_context: &crate::query::QueryContext<'_, '_>, - ) -> TokenStream { - let derives = query_context.response_enum_derives(); - let norm = query_context.normalization; - let variant_names: Vec = self - .variants - .iter() - .map(|v| { - let name = norm.enum_variant(crate::shared::keyword_replace(&v.name)); - let name = Ident::new(&name, Span::call_site()); - - let description = &v.description; - let description = description.as_ref().map(|d| quote!(#[doc = #d])); - - quote!(#description #name) - }) - .collect(); - let variant_names = &variant_names; - let name_ident = norm.enum_name(format!("{}{}", ENUMS_PREFIX, self.name)); - let name_ident = Ident::new(&name_ident, Span::call_site()); - let constructors: Vec<_> = self - .variants - .iter() - .map(|v| { - let name = norm.enum_variant(crate::shared::keyword_replace(&v.name)); - let v = Ident::new(&name, Span::call_site()); - - quote!(#name_ident::#v) - }) - .collect(); - let constructors = &constructors; - let variant_str: Vec<&str> = self.variants.iter().map(|v| v.name).collect(); - let variant_str = &variant_str; - - let name = name_ident; - - quote! { - #derives - pub enum #name { - #(#variant_names,)* - Other(String), - } - - impl ::serde::Serialize for #name { - fn serialize(&self, ser: S) -> Result { - ser.serialize_str(match *self { - #(#constructors => #variant_str,)* - #name::Other(ref s) => &s, - }) - } - } - - impl<'de> ::serde::Deserialize<'de> for #name { - fn deserialize>(deserializer: D) -> Result { - let s = ::deserialize(deserializer)?; - - match s.as_str() { - #(#variant_str => Ok(#constructors),)* - _ => Ok(#name::Other(s)), - } - } - } - } - } -} diff --git a/graphql_client_codegen/src/field_type.rs b/graphql_client_codegen/src/field_type.rs deleted file mode 100644 index a2c7a10fb..000000000 --- a/graphql_client_codegen/src/field_type.rs +++ /dev/null @@ -1,282 +0,0 @@ -use crate::enums::ENUMS_PREFIX; -use crate::query::QueryContext; -use crate::schema::DEFAULT_SCALARS; -use graphql_introspection_query::introspection_response; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; - -#[derive(Clone, Debug, PartialEq, Hash)] -enum GraphqlTypeQualifier { - Required, - List, -} - -#[derive(Clone, Debug, PartialEq, Hash)] -pub struct FieldType<'a> { - /// The type name of the field. - /// - /// e.g. for `[Int]!`, this would return `Int`. - name: &'a str, - /// An ordered list of qualifiers, from outer to inner. - /// - /// e.g. `[Int]!` would have `vec![List, Optional]`, but `[Int!]` would have `vec![Optional, - /// List]`. - qualifiers: Vec, -} - -impl<'a> FieldType<'a> { - pub(crate) fn new(name: &'a str) -> Self { - FieldType { - name, - qualifiers: Vec::new(), - } - } - - #[cfg(test)] - pub(crate) fn list(mut self) -> Self { - self.qualifiers.insert(0, GraphqlTypeQualifier::List); - self - } - - #[cfg(test)] - pub(crate) fn nonnull(mut self) -> Self { - self.qualifiers.insert(0, GraphqlTypeQualifier::Required); - self - } - - /// Takes a field type with its name. - pub(crate) fn to_rust(&self, context: &QueryContext<'_, '_>, prefix: &str) -> TokenStream { - let prefix: &str = if prefix.is_empty() { - self.inner_name_str() - } else { - prefix - }; - - let full_name = { - if context - .schema - .scalars - .get(&self.name) - .map(|s| s.is_required.set(true)) - .is_some() - || DEFAULT_SCALARS.iter().any(|elem| elem == &self.name) - { - self.name.to_string() - } else if context - .schema - .enums - .get(&self.name) - .map(|enm| enm.is_required.set(true)) - .is_some() - { - format!("{}{}", ENUMS_PREFIX, self.name) - } else { - if prefix.is_empty() { - panic!("Empty prefix for {:?}", self); - } - prefix.to_string() - } - }; - - let norm = context.normalization; - let full_name = norm.field_type(crate::shared::keyword_replace(&full_name)); - - let full_name = Ident::new(&full_name, Span::call_site()); - let mut qualified = quote!(#full_name); - - let mut non_null = false; - - // Note: we iterate over qualifiers in reverse because it is more intuitive. This - // means we start from the _inner_ type and make our way to the outside. - for qualifier in self.qualifiers.iter().rev() { - match (non_null, qualifier) { - // We are in non-null context, and we wrap the non-null type into a list. - // We switch back to null context. - (true, GraphqlTypeQualifier::List) => { - qualified = quote!(Vec<#qualified>); - non_null = false; - } - // We are in nullable context, and we wrap the nullable type into a list. - (false, GraphqlTypeQualifier::List) => { - qualified = quote!(Vec>); - } - // We are in non-nullable context, but we can't double require a type - // (!!). - (true, GraphqlTypeQualifier::Required) => panic!("double required annotation"), - // We are in nullable context, and we switch to non-nullable context. - (false, GraphqlTypeQualifier::Required) => { - non_null = true; - } - } - } - - // If we are in nullable context at the end of the iteration, we wrap the whole - // type with an Option. - if !non_null { - qualified = quote!(Option<#qualified>); - } - - qualified - } - - /// Return the innermost name - we mostly use this for looking types up in our Schema struct. - pub fn inner_name_str(&self) -> &str { - self.name - } - - /// Is the type nullable? - /// - /// Note: a list of nullable values is considered nullable only if the list itself is nullable. - pub fn is_optional(&self) -> bool { - if let Some(qualifier) = self.qualifiers.get(0) { - qualifier != &GraphqlTypeQualifier::Required - } else { - true - } - } - - /// A type is indirected if it is a (flat or nested) list type, optional or not. - /// - /// We use this to determine whether a type needs to be boxed for recursion. - pub fn is_indirected(&self) -> bool { - self.qualifiers - .iter() - .any(|qualifier| qualifier == &GraphqlTypeQualifier::List) - } -} - -impl<'schema> std::convert::From<&'schema graphql_parser::schema::Type> for FieldType<'schema> { - fn from(schema_type: &'schema graphql_parser::schema::Type) -> FieldType<'schema> { - from_schema_type_inner(schema_type) - } -} - -fn graphql_parser_depth(schema_type: &graphql_parser::schema::Type) -> usize { - match schema_type { - graphql_parser::schema::Type::ListType(inner) => 1 + graphql_parser_depth(inner), - graphql_parser::schema::Type::NonNullType(inner) => 1 + graphql_parser_depth(inner), - graphql_parser::schema::Type::NamedType(_) => 0, - } -} - -fn from_schema_type_inner(inner: &graphql_parser::schema::Type) -> FieldType<'_> { - use graphql_parser::schema::Type::*; - - let qualifiers_depth = graphql_parser_depth(inner); - let mut qualifiers = Vec::with_capacity(qualifiers_depth); - - let mut inner = inner; - - loop { - match inner { - ListType(new_inner) => { - qualifiers.push(GraphqlTypeQualifier::List); - inner = new_inner; - } - NonNullType(new_inner) => { - qualifiers.push(GraphqlTypeQualifier::Required); - inner = new_inner; - } - NamedType(name) => return FieldType { name, qualifiers }, - } - } -} - -fn json_type_qualifiers_depth(typeref: &introspection_response::TypeRef) -> usize { - use graphql_introspection_query::introspection_response::*; - - match (typeref.kind.as_ref(), typeref.of_type.as_ref()) { - (Some(__TypeKind::NON_NULL), Some(inner)) => 1 + json_type_qualifiers_depth(inner), - (Some(__TypeKind::LIST), Some(inner)) => 1 + json_type_qualifiers_depth(inner), - (Some(_), None) => 0, - _ => panic!("Non-convertible type in JSON schema: {:?}", typeref), - } -} - -fn from_json_type_inner(inner: &introspection_response::TypeRef) -> FieldType<'_> { - use graphql_introspection_query::introspection_response::*; - - let qualifiers_depth = json_type_qualifiers_depth(inner); - let mut qualifiers = Vec::with_capacity(qualifiers_depth); - - let mut inner = inner; - - loop { - match ( - inner.kind.as_ref(), - inner.of_type.as_ref(), - inner.name.as_ref(), - ) { - (Some(__TypeKind::NON_NULL), Some(new_inner), _) => { - qualifiers.push(GraphqlTypeQualifier::Required); - inner = &new_inner; - } - (Some(__TypeKind::LIST), Some(new_inner), _) => { - qualifiers.push(GraphqlTypeQualifier::List); - inner = &new_inner; - } - (Some(_), None, Some(name)) => return FieldType { name, qualifiers }, - _ => panic!("Non-convertible type in JSON schema: {:?}", inner), - } - } -} - -impl<'schema> std::convert::From<&'schema introspection_response::FullTypeFieldsType> - for FieldType<'schema> -{ - fn from( - schema_type: &'schema introspection_response::FullTypeFieldsType, - ) -> FieldType<'schema> { - from_json_type_inner(&schema_type.type_ref) - } -} - -impl<'a> std::convert::From<&'a introspection_response::InputValueType> for FieldType<'a> { - fn from(schema_type: &'a introspection_response::InputValueType) -> FieldType<'a> { - from_json_type_inner(&schema_type.type_ref) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use graphql_introspection_query::introspection_response::{ - FullTypeFieldsType, TypeRef, __TypeKind, - }; - use graphql_parser::schema::Type as GqlParserType; - - #[test] - fn field_type_from_graphql_parser_schema_type_works() { - let ty = GqlParserType::NamedType("Cat".to_owned()); - assert_eq!(FieldType::from(&ty), FieldType::new("Cat")); - - let ty = GqlParserType::NonNullType(Box::new(GqlParserType::NamedType("Cat".to_owned()))); - - assert_eq!(FieldType::from(&ty), FieldType::new("Cat").nonnull()); - } - - #[test] - fn field_type_from_introspection_response_works() { - let ty = FullTypeFieldsType { - type_ref: TypeRef { - kind: Some(__TypeKind::OBJECT), - name: Some("Cat".into()), - of_type: None, - }, - }; - assert_eq!(FieldType::from(&ty), FieldType::new("Cat")); - - let ty = FullTypeFieldsType { - type_ref: TypeRef { - kind: Some(__TypeKind::NON_NULL), - name: None, - of_type: Some(Box::new(TypeRef { - kind: Some(__TypeKind::OBJECT), - name: Some("Cat".into()), - of_type: None, - })), - }, - }; - assert_eq!(FieldType::from(&ty), FieldType::new("Cat").nonnull()); - } -} diff --git a/graphql_client_codegen/src/fragments.rs b/graphql_client_codegen/src/fragments.rs deleted file mode 100644 index ac5c8d901..000000000 --- a/graphql_client_codegen/src/fragments.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::query::QueryContext; -use crate::selection::Selection; -use proc_macro2::TokenStream; -use std::cell::Cell; - -/// Represents which type a fragment is defined on. This is the type mentioned in the fragment's `on` clause. -#[derive(Debug, PartialEq)] -pub(crate) enum FragmentTarget<'context> { - Object(&'context crate::objects::GqlObject<'context>), - Interface(&'context crate::interfaces::GqlInterface<'context>), - Union(&'context crate::unions::GqlUnion<'context>), -} - -impl<'context> FragmentTarget<'context> { - pub(crate) fn name(&self) -> &str { - match self { - FragmentTarget::Object(obj) => obj.name, - FragmentTarget::Interface(iface) => iface.name, - FragmentTarget::Union(unn) => unn.name, - } - } -} - -/// Represents a fragment extracted from a query document. -#[derive(Debug, PartialEq)] -pub(crate) struct GqlFragment<'query> { - /// The name of the fragment, matching one-to-one with the name in the GraphQL query document. - pub name: &'query str, - /// The `on` clause of the fragment. - pub on: FragmentTarget<'query>, - /// The selected fields. - pub selection: Selection<'query>, - /// Whether the fragment is used in the current query - pub is_required: Cell, -} - -impl<'query> GqlFragment<'query> { - /// Generate all the Rust code required by the fragment's object selection. - pub(crate) fn to_rust( - &self, - context: &QueryContext<'_, '_>, - ) -> Result { - match self.on { - FragmentTarget::Object(obj) => { - obj.response_for_selection(context, &self.selection, &self.name) - } - FragmentTarget::Interface(iface) => { - iface.response_for_selection(context, &self.selection, &self.name) - } - FragmentTarget::Union(_) => { - unreachable!("Wrong code path. Fragment on unions are treated differently.") - } - } - } - - pub(crate) fn is_recursive(&self) -> bool { - self.selection.contains_fragment(&self.name) - } - - pub(crate) fn require<'schema>(&self, context: &QueryContext<'query, 'schema>) { - self.is_required.set(true); - self.selection.require_items(context); - } -} diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index 09e649a6d..7319ac234 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -1,37 +1,52 @@ -use crate::codegen_options::*; +use crate::{ + codegen_options::*, + query::{BoundQuery, OperationId}, +}; use heck::*; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; /// This struct contains the parameters necessary to generate code for a given operation. pub(crate) struct GeneratedModule<'a> { - pub operation: &'a crate::operations::Operation<'a>, + pub operation: &'a str, pub query_string: &'a str, - pub query_document: &'a graphql_parser::query::Document, - pub schema: &'a crate::schema::Schema<'a>, + pub resolved_query: &'a crate::query::Query, + pub schema: &'a crate::schema::Schema, pub options: &'a crate::GraphQLClientCodegenOptions, } impl<'a> GeneratedModule<'a> { /// Generate the items for the variables and the response that will go inside the module. - fn build_impls(&self) -> Result { + fn build_impls(&self) -> anyhow::Result { Ok(crate::codegen::response_for_query( - &self.schema, - &self.query_document, - &self.operation, + self.root()?, &self.options, + BoundQuery { + query: self.resolved_query, + schema: self.schema, + }, )?) } + fn root(&self) -> anyhow::Result { + let op_name = self.options.normalization().operation(self.operation); + self.resolved_query + .select_operation(&op_name, *self.options.normalization()) + .map(|op| op.0) + .ok_or_else(|| { + anyhow::anyhow!( + "Could not find an operation named {} in the query document.", + op_name + ) + }) + } + /// Generate the module and all the code inside. - pub(crate) fn to_token_stream(&self) -> Result { - let module_name = Ident::new(&self.operation.name.to_snake_case(), Span::call_site()); + pub(crate) fn to_token_stream(&self) -> anyhow::Result { + let module_name = Ident::new(&self.operation.to_snake_case(), Span::call_site()); let module_visibility = &self.options.module_visibility(); - let operation_name_literal = &self.operation.name; - let operation_name_ident = self - .options - .normalization() - .operation(operation_name_literal); + let operation_name = self.operation; + let operation_name_ident = self.options.normalization().operation(self.operation); let operation_name_ident = Ident::new(&operation_name_ident, Span::call_site()); // Force cargo to refresh the generated code when the query file changes. @@ -61,7 +76,7 @@ impl<'a> GeneratedModule<'a> { #module_visibility mod #module_name { #![allow(dead_code)] - pub const OPERATION_NAME: &'static str = #operation_name_literal; + pub const OPERATION_NAME: &'static str = #operation_name; pub const QUERY: &'static str = #query_string; #query_include diff --git a/graphql_client_codegen/src/inputs.rs b/graphql_client_codegen/src/inputs.rs deleted file mode 100644 index c91cb29a7..000000000 --- a/graphql_client_codegen/src/inputs.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::deprecation::DeprecationStatus; -use crate::objects::GqlObjectField; -use crate::query::QueryContext; -use crate::schema::Schema; -use graphql_introspection_query::introspection_response; -use heck::SnakeCase; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; -use std::collections::HashMap; - -/// Represents an input object type from a GraphQL schema -#[derive(Debug, Clone, PartialEq)] -pub struct GqlInput<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, - pub fields: HashMap<&'schema str, GqlObjectField<'schema>>, - pub is_required: Cell, -} - -impl<'schema> GqlInput<'schema> { - pub(crate) fn require(&self, schema: &Schema<'schema>) { - if self.is_required.get() { - return; - } - self.is_required.set(true); - self.fields.values().for_each(|field| { - schema.require(&field.type_.inner_name_str()); - }) - } - - fn contains_type_without_indirection( - &self, - context: &QueryContext<'_, '_>, - type_name: &str, - ) -> bool { - // the input type is recursive if any of its members contains it, without indirection - self.fields.values().any(|field| { - // the field is indirected, so no boxing is needed - if field.type_.is_indirected() { - return false; - } - - let field_type_name = field.type_.inner_name_str(); - let input = context.schema.inputs.get(field_type_name); - - if let Some(input) = input { - // the input contains itself, not indirected - if input.name == type_name { - return true; - } - - // we check if the other input contains this one (without indirection) - input.contains_type_without_indirection(context, type_name) - } else { - // the field is not referring to an input type - false - } - }) - } - - fn is_recursive_without_indirection(&self, context: &QueryContext<'_, '_>) -> bool { - self.contains_type_without_indirection(context, &self.name) - } - - pub(crate) fn to_rust( - &self, - context: &QueryContext<'_, '_>, - ) -> Result { - let norm = context.normalization; - let mut fields: Vec<&GqlObjectField<'_>> = self.fields.values().collect(); - fields.sort_unstable_by(|a, b| a.name.cmp(&b.name)); - let fields = fields.iter().map(|field| { - let ty = field.type_.to_rust(&context, ""); - - // If the type is recursive, we have to box it - let ty = if let Some(input) = context.schema.inputs.get(field.type_.inner_name_str()) { - if input.is_recursive_without_indirection(context) { - quote! { Box<#ty> } - } else { - quote!(#ty) - } - } else { - quote!(#ty) - }; - - context.schema.require(&field.type_.inner_name_str()); - let name = crate::shared::keyword_replace(&field.name.to_snake_case()); - let rename = crate::shared::field_rename_annotation(&field.name, &name); - let name = norm.field_name(name); - let name = Ident::new(&name, Span::call_site()); - - quote!(#rename pub #name: #ty) - }); - let variables_derives = context.variables_derives(); - - // Prevent generated code like "pub struct crate" for a schema input like "input crate { ... }" - // This works in tandem with renamed struct Variables field types, eg: pub struct Variables { pub criteria : crate_ , } - let name = crate::shared::keyword_replace(&self.name); - let name = norm.input_name(name); - let name = Ident::new(&name, Span::call_site()); - Ok(quote! { - #variables_derives - pub struct #name { - #(#fields,)* - } - }) - } -} - -impl<'schema> std::convert::From<&'schema graphql_parser::schema::InputObjectType> - for GqlInput<'schema> -{ - fn from(schema_input: &'schema graphql_parser::schema::InputObjectType) -> GqlInput<'schema> { - GqlInput { - description: schema_input.description.as_deref(), - name: &schema_input.name, - fields: schema_input - .fields - .iter() - .map(|field| { - let name = field.name.as_str(); - let field = GqlObjectField { - description: None, - name: &field.name, - type_: crate::field_type::FieldType::from(&field.value_type), - deprecation: DeprecationStatus::Current, - }; - (name, field) - }) - .collect(), - is_required: false.into(), - } - } -} - -impl<'schema> std::convert::From<&'schema introspection_response::FullType> for GqlInput<'schema> { - fn from(schema_input: &'schema introspection_response::FullType) -> GqlInput<'schema> { - GqlInput { - description: schema_input.description.as_deref(), - name: schema_input.name.as_deref().expect("unnamed input object"), - fields: schema_input - .input_fields - .as_ref() - .expect("fields on input object") - .iter() - .filter_map(Option::as_ref) - .map(|f| { - let name = f - .input_value - .name - .as_ref() - .expect("unnamed input object field") - .as_str(); - let field = GqlObjectField { - description: None, - name: &name, - type_: f - .input_value - .type_ - .as_ref() - .map(|s| s.into()) - .expect("type on input object field"), - deprecation: DeprecationStatus::Current, - }; - (name, field) - }) - .collect(), - is_required: false.into(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::*; - use crate::field_type::FieldType; - - #[test] - fn gql_input_to_rust() { - let cat = GqlInput { - description: None, - name: "Cat", - fields: vec![ - ( - "pawsCount", - GqlObjectField { - description: None, - name: "pawsCount", - type_: FieldType::new(float_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - ), - ( - "offsprings", - GqlObjectField { - description: None, - name: "offsprings", - type_: FieldType::new("Cat").nonnull().list().nonnull(), - deprecation: DeprecationStatus::Current, - }, - ), - ( - "requirements", - GqlObjectField { - description: None, - name: "requirements", - type_: FieldType::new("CatRequirements"), - deprecation: DeprecationStatus::Current, - }, - ), - ] - .into_iter() - .collect(), - is_required: false.into(), - }; - - let expected: String = vec![ - "# [ derive ( Clone , Serialize ) ] ", - "pub struct Cat { ", - "pub offsprings : Vec < Cat > , ", - "# [ serde ( rename = \"pawsCount\" ) ] ", - "pub paws_count : Float , ", - "pub requirements : Option < CatRequirements > , ", - "}", - ] - .into_iter() - .collect(); - - let mut schema = crate::schema::Schema::new(); - schema.inputs.insert(cat.name, cat); - let mut context = QueryContext::new_empty(&schema); - context.ingest_variables_derives("Clone").unwrap(); - - assert_eq!( - format!( - "{}", - context.schema.inputs["Cat"].to_rust(&context).unwrap() - ), - expected - ); - } -} diff --git a/graphql_client_codegen/src/interfaces.rs b/graphql_client_codegen/src/interfaces.rs deleted file mode 100644 index 1ae87ff5a..000000000 --- a/graphql_client_codegen/src/interfaces.rs +++ /dev/null @@ -1,273 +0,0 @@ -use crate::constants::TYPENAME_FIELD; -use crate::objects::GqlObjectField; -use crate::query::QueryContext; -use crate::selection::{Selection, SelectionField, SelectionFragmentSpread, SelectionItem}; -use crate::shared::*; -use crate::unions::union_variants; -use failure::*; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; -use std::collections::HashSet; - -/// A GraphQL interface (simplified schema representation). -/// -/// In the generated code, fragments nesting is preserved, including for selection on union variants. See the tests in the graphql client crate for examples. -#[derive(Debug, Clone, PartialEq)] -pub struct GqlInterface<'schema> { - /// The documentation for the interface. Extracted from the schema. - pub description: Option<&'schema str>, - /// The set of object types implementing this interface. - pub implemented_by: HashSet<&'schema str>, - /// The name of the interface. Should match 1-to-1 to its name in the GraphQL schema. - pub name: &'schema str, - /// The interface's fields. Analogous to object fields. - pub fields: Vec>, - pub is_required: Cell, -} - -impl<'schema> GqlInterface<'schema> { - /// filters the selection to keep only the fields that refer to the interface's own. - /// - /// This does not include the __typename field because it is translated into the `on` enum. - fn object_selection<'query>( - &self, - selection: &'query Selection<'query>, - query_context: &QueryContext<'_, '_>, - ) -> Selection<'query> { - (&selection) - .into_iter() - // Only keep what we can handle - .filter(|f| match f { - SelectionItem::Field(f) => f.name != TYPENAME_FIELD, - 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.name() == self.name - } - SelectionItem::InlineFragment(_) => false, - }) - .map(|a| (*a).clone()) - .collect() - } - - fn union_selection<'query>( - &self, - selection: &'query Selection<'_>, - query_context: &QueryContext<'_, '_>, - ) -> Selection<'query> { - (&selection) - .into_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.name() != 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: &'schema str, - description: Option<&'schema str>, - ) -> GqlInterface<'schema> { - GqlInterface { - name, - description, - implemented_by: HashSet::new(), - fields: vec![], - is_required: false.into(), - } - } - - /// The generated code for each of the selected field's types. See [shared::field_impls_for_selection]. - pub(crate) fn field_impls_for_selection( - &self, - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - crate::shared::field_impls_for_selection( - &self.fields, - context, - &self.object_selection(selection, context), - prefix, - ) - } - - /// The code for the interface's corresponding struct's fields. - pub(crate) fn response_fields_for_selection( - &self, - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - response_fields_for_selection( - &self.name, - &self.fields, - context, - &self.object_selection(selection, context), - prefix, - ) - } - - /// Generate all the code for the interface. - pub(crate) fn response_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result { - let name = Ident::new(&prefix, Span::call_site()); - let derives = query_context.response_derives(); - - selection.extract_typename(query_context).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, &self.name)?; - - for used_variant in used_variants.iter() { - if !self.implemented_by.contains(used_variant) { - return Err(format_err!( - "Type {} does not implement the {} interface", - used_variant, - self.name, - )); - } - } - - // Add the non-selected variants to the generated enum's variants. - union_variants.extend( - self.implemented_by - .iter() - .filter(|obj| used_variants.iter().find(|v| v == obj).is_none()) - .map(|v| { - let v = Ident::new(v, Span::call_site()); - quote!(#v) - }), - ); - - let attached_enum_name = Ident::new(&format!("{}On", name), Span::call_site()); - let (attached_enum, last_object_field) = - if selection.extract_typename(query_context).is_some() { - let attached_enum = quote! { - #derives - #[serde(tag = "__typename")] - pub enum #attached_enum_name { - #(#union_variants,)* - } - }; - let last_object_field = quote!(#[serde(flatten)] pub on: #attached_enum_name,); - (Some(attached_enum), Some(last_object_field)) - } else { - (None, None) - }; - - Ok(quote! { - - #(#object_children)* - - #(#union_children)* - - #attached_enum - - #derives - pub struct #name { - #(#object_fields,)* - #last_object_field - } - }) - } -} - -#[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", - fields: vec![], - is_required: Cell::new(true), - }; - - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - - let typename_field = - crate::selection::SelectionItem::Field(crate::selection::SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }); - let selection = Selection::from_vec(vec![typename_field.clone()]); - - assert_eq!( - iface.union_selection(&selection, &context), - Selection::from_vec(vec![typename_field]) - ); - } - - // to be improved - #[test] - fn object_selection_works() { - let iface = GqlInterface { - description: None, - implemented_by: HashSet::new(), - name: "MyInterface", - fields: vec![], - is_required: Cell::new(true), - }; - - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - - let typename_field = - crate::selection::SelectionItem::Field(crate::selection::SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }); - let selection: Selection<'_> = vec![typename_field].into_iter().collect(); - - assert_eq!( - iface.object_selection(&selection, &context), - Selection::new_empty() - ); - } -} diff --git a/graphql_client_codegen/src/lib.rs b/graphql_client_codegen/src/lib.rs index f5fa21e5b..9f0442162 100644 --- a/graphql_client_codegen/src/lib.rs +++ b/graphql_client_codegen/src/lib.rs @@ -1,12 +1,12 @@ -#![recursion_limit = "128"] #![deny(missing_docs)] #![warn(rust_2018_idioms)] +#![allow(clippy::option_option)] //! Crate for internal use by other graphql-client crates, for code generation. //! //! It is not meant to be used directly by users of the library. -use failure::*; +use anyhow::format_err; use lazy_static::*; use proc_macro2::TokenStream; use quote::*; @@ -15,26 +15,15 @@ mod codegen; mod codegen_options; /// Deprecation-related code pub mod deprecation; -mod query; /// Contains the [Schema] type and its implementation. pub mod schema; mod constants; -mod enums; -mod field_type; -mod fragments; mod generated_module; -mod inputs; -mod interfaces; /// Normalization-related code pub mod normalization; -mod objects; -mod operations; -mod scalars; -mod selection; -mod shared; -mod unions; -mod variables; +mod query; +mod type_qualifiers; #[cfg(test)] mod tests; @@ -46,7 +35,7 @@ use std::collections::HashMap; type CacheMap = std::sync::Mutex>; lazy_static! { - static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); + static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); static ref QUERY_CACHE: CacheMap<(String, graphql_parser::query::Document)> = CacheMap::default(); } @@ -56,8 +45,38 @@ pub fn generate_module_token_stream( query_path: std::path::PathBuf, schema_path: &std::path::Path, options: GraphQLClientCodegenOptions, -) -> Result { +) -> anyhow::Result { use std::collections::hash_map; + + let schema_extension = schema_path + .extension() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or("INVALID"); + + // Check the schema cache. + let schema: schema::Schema = { + let mut lock = SCHEMA_CACHE.lock().expect("schema cache is poisoned"); + match lock.entry(schema_path.to_path_buf()) { + hash_map::Entry::Occupied(o) => o.get().clone(), + hash_map::Entry::Vacant(v) => { + let schema_string = read_file(v.key())?; + let schema = match schema_extension { + "graphql" | "gql" => { + let s = graphql_parser::schema::parse_schema(&schema_string).map_err(|parser_error| anyhow::anyhow!("Parser error: {}", parser_error))?; + schema::Schema::from(s) + } + "json" => { + let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string)?; + schema::Schema::from(parsed) + } + extension => panic!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension) + }; + + v.insert(schema).clone() + } + } + }; + // We need to qualify the query with the path to the crate it is part of let (query_string, query) = { let mut lock = QUERY_CACHE.lock().expect("query cache is poisoned"); @@ -65,24 +84,25 @@ pub fn generate_module_token_stream( hash_map::Entry::Occupied(o) => o.get().clone(), hash_map::Entry::Vacant(v) => { let query_string = read_file(v.key())?; - let query = graphql_parser::parse_query(&query_string)?; + let query = graphql_parser::parse_query(&query_string) + .map_err(|err| anyhow::anyhow!("Query parser error: {}", err))?; v.insert((query_string, query)).clone() } } }; + let query = crate::query::resolve(&schema, &query)?; + // Determine which operation we are generating code for. This will be used in operationName. let operations = options .operation_name .as_ref() - .and_then(|operation_name| { - codegen::select_operation(&query, &operation_name, options.normalization()) - }) + .and_then(|operation_name| query.select_operation(operation_name, *options.normalization())) .map(|op| vec![op]); let operations = match (operations, &options.mode) { (Some(ops), _) => ops, - (None, &CodegenMode::Cli) => codegen::all_operations(&query), + (None, &CodegenMode::Cli) => query.operations().collect(), (None, &CodegenMode::Derive) => { return Err(derive_operation_not_found_error( options.struct_ident(), @@ -91,37 +111,6 @@ pub fn generate_module_token_stream( } }; - let schema_extension = schema_path - .extension() - .and_then(std::ffi::OsStr::to_str) - .unwrap_or("INVALID"); - - // Check the schema cache. - let schema_string: String = { - let mut lock = SCHEMA_CACHE.lock().expect("schema cache is poisoned"); - match lock.entry(schema_path.to_path_buf()) { - hash_map::Entry::Occupied(o) => o.get().clone(), - hash_map::Entry::Vacant(v) => { - let schema_string = read_file(v.key())?; - (*v.insert(schema_string)).to_string() - } - } - }; - - let parsed_schema = match schema_extension { - "graphql" | "gql" => { - let s = graphql_parser::schema::parse_schema(&schema_string)?; - schema::ParsedSchema::GraphQLParser(s) - } - "json" => { - let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string)?; - schema::ParsedSchema::Json(parsed) - } - extension => panic!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension) - }; - - let schema = schema::Schema::from(&parsed_schema); - // The generated modules. let mut modules = Vec::with_capacity(operations.len()); @@ -129,8 +118,8 @@ pub fn generate_module_token_stream( let generated = generated_module::GeneratedModule { query_string: query_string.as_str(), schema: &schema, - query_document: &query, - operation, + resolved_query: &query, + operation: &operation.1.name, options: &options, } .to_token_stream()?; @@ -142,13 +131,13 @@ pub fn generate_module_token_stream( Ok(modules) } -fn read_file(path: &std::path::Path) -> Result { +fn read_file(path: &std::path::Path) -> anyhow::Result { use std::fs; use std::io::prelude::*; let mut out = String::new(); let mut file = fs::File::open(path).map_err(|io_err| { - let err: failure::Error = io_err.into(); + let err: anyhow::Error = io_err.into(); err.context(format!( r#" Could not find file with path: {} @@ -164,34 +153,16 @@ fn read_file(path: &std::path::Path) -> Result { /// In derive mode, build an error when the operation with the same name as the struct is not found. fn derive_operation_not_found_error( ident: Option<&proc_macro2::Ident>, - query: &graphql_parser::query::Document, -) -> failure::Error { - use graphql_parser::query::*; - + query: &crate::query::Query, +) -> anyhow::Error { let operation_name = ident.map(ToString::to_string); let struct_ident = operation_name.as_deref().unwrap_or(""); - let available_operations = query - .definitions - .iter() - .filter_map(|definition| match definition { - Definition::Operation(op) => match op { - OperationDefinition::Mutation(m) => Some(m.name.as_ref().unwrap()), - OperationDefinition::Query(m) => Some(m.name.as_ref().unwrap()), - OperationDefinition::Subscription(m) => Some(m.name.as_ref().unwrap()), - OperationDefinition::SelectionSet(_) => { - unreachable!("Bare selection sets are not supported.") - } - }, - _ => None, - }) - .fold(String::new(), |mut acc, item| { - acc.push_str(&item); - acc.push_str(", "); - acc - }); - - let available_operations = available_operations.trim_end_matches(", "); + let available_operations: Vec<&str> = query + .operations() + .map(|(_id, op)| op.name.as_str()) + .collect(); + let available_operations: String = available_operations.join(", "); return format_err!( "The struct name does not match any defined operation in the query file.\nStruct name: {}\nDefined operations: {}", diff --git a/graphql_client_codegen/src/normalization.rs b/graphql_client_codegen/src/normalization.rs index 3fb1a04b0..d8fef5b8b 100644 --- a/graphql_client_codegen/src/normalization.rs +++ b/graphql_client_codegen/src/normalization.rs @@ -1,4 +1,4 @@ -use heck::{CamelCase, SnakeCase}; +use heck::CamelCase; use std::borrow::Cow; /// Normalization conventions available for generated code. @@ -11,75 +11,43 @@ pub enum Normalization { } impl Normalization { - fn camel_case(self, name: Cow<'_, str>) -> Cow<'_, str> { + fn camel_case(self, name: &str) -> Cow<'_, str> { match self { - Self::None => name, + Self::None => name.into(), Self::Rust => name.to_camel_case().into(), } } - fn snake_case(self, name: Cow<'_, str>) -> Cow<'_, str> { - match self { - Self::None => name, - Self::Rust => name.to_snake_case().into(), - } - } - - pub(crate) fn operation<'a, S>(self, op: S) -> Cow<'a, str> - where - S: Into>, - { - self.camel_case(op.into()) + pub(crate) fn operation(self, op: &str) -> Cow<'_, str> { + self.camel_case(op) } - pub(crate) fn enum_variant<'a, S>(self, enm: S) -> Cow<'a, str> - where - S: Into>, - { - self.camel_case(enm.into()) + pub(crate) fn enum_variant(self, enm: &str) -> Cow<'_, str> { + self.camel_case(enm) } - pub(crate) fn enum_name<'a, S>(self, enm: S) -> Cow<'a, str> - where - S: Into>, - { - self.camel_case(enm.into()) + pub(crate) fn enum_name(self, enm: &str) -> Cow<'_, str> { + self.camel_case(enm) } - fn field_type_impl(self, fty: Cow<'_, str>) -> Cow<'_, str> { + fn field_type_impl(self, fty: &str) -> Cow<'_, str> { if fty == "ID" || fty.starts_with("__") { - fty + fty.into() } else { self.camel_case(fty) } } - pub(crate) fn field_type<'a, S>(self, fty: S) -> Cow<'a, str> - where - S: Into>, - { - self.field_type_impl(fty.into()) - } - - pub(crate) fn field_name<'a, S>(self, fnm: S) -> Cow<'a, str> - where - S: Into>, - { - self.snake_case(fnm.into()) + pub(crate) fn field_type(self, fty: &str) -> Cow<'_, str> { + self.field_type_impl(fty) } - pub(crate) fn input_name<'a, S>(self, inm: S) -> Cow<'a, str> - where - S: Into>, - { - self.camel_case(inm.into()) + pub(crate) fn input_name(self, inm: &str) -> Cow<'_, str> { + self.camel_case(inm) } - pub(crate) fn scalar_name<'a, S>(self, snm: S) -> Cow<'a, str> - where - S: Into>, - { - self.camel_case(snm.into()) + pub(crate) fn scalar_name(self, snm: &str) -> Cow<'_, str> { + self.camel_case(snm) } } diff --git a/graphql_client_codegen/src/objects.rs b/graphql_client_codegen/src/objects.rs deleted file mode 100644 index 48f0607e2..000000000 --- a/graphql_client_codegen/src/objects.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::constants::*; -use crate::deprecation::DeprecationStatus; -use crate::field_type::FieldType; -use crate::query::QueryContext; -use crate::schema::Schema; -use crate::selection::*; -use crate::shared::{field_impls_for_selection, response_fields_for_selection}; -use graphql_parser::schema; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; - -#[derive(Debug, Clone, PartialEq)] -pub struct GqlObject<'schema> { - pub description: Option<&'schema str>, - pub fields: Vec>, - pub name: &'schema str, - pub is_required: Cell, -} - -#[derive(Clone, Debug, PartialEq, Hash)] -pub struct GqlObjectField<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, - pub type_: FieldType<'schema>, - pub deprecation: DeprecationStatus, -} - -fn parse_deprecation_info(field: &schema::Field) -> DeprecationStatus { - let deprecated = field - .directives - .iter() - .find(|x| x.name.to_lowercase() == "deprecated"); - let reason = if let Some(d) = deprecated { - if let Some((_, value)) = d.arguments.iter().find(|x| x.0.to_lowercase() == "reason") { - match value { - schema::Value::String(reason) => Some(reason.clone()), - schema::Value::Null => None, - _ => panic!("deprecation reason is not a string"), - } - } else { - None - } - } else { - None - }; - match deprecated { - Some(_) => DeprecationStatus::Deprecated(reason), - None => DeprecationStatus::Current, - } -} - -impl<'schema> GqlObject<'schema> { - pub fn new(name: &'schema str, description: Option<&'schema str>) -> GqlObject<'schema> { - GqlObject { - description, - name, - fields: vec![typename_field()], - is_required: false.into(), - } - } - - pub fn from_graphql_parser_object(obj: &'schema schema::ObjectType) -> Self { - let description = obj.description.as_deref(); - let mut item = GqlObject::new(&obj.name, description); - item.fields.extend(obj.fields.iter().map(|f| { - let deprecation = parse_deprecation_info(&f); - GqlObjectField { - description: f.description.as_deref(), - name: &f.name, - type_: FieldType::from(&f.field_type), - deprecation, - } - })); - item - } - - pub fn from_introspected_schema_json( - obj: &'schema graphql_introspection_query::introspection_response::FullType, - ) -> Self { - let description = obj.description.as_deref(); - let mut item = GqlObject::new(obj.name.as_ref().expect("missing object name"), description); - let fields = obj.fields.as_ref().unwrap().iter().filter_map(|t| { - t.as_ref().map(|t| { - let deprecation = if t.is_deprecated.unwrap_or(false) { - DeprecationStatus::Deprecated(t.deprecation_reason.clone()) - } else { - DeprecationStatus::Current - }; - GqlObjectField { - description: t.description.as_deref(), - name: t.name.as_ref().expect("field name"), - type_: FieldType::from(t.type_.as_ref().expect("field type")), - deprecation, - } - }) - }); - - item.fields.extend(fields); - - item - } - - pub(crate) fn require(&self, schema: &Schema<'_>) { - if self.is_required.get() { - return; - } - self.is_required.set(true); - self.fields.iter().for_each(|field| { - schema.require(&field.type_.inner_name_str()); - }) - } - - pub(crate) fn response_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result { - let derives = query_context.response_derives(); - let name = Ident::new(prefix, Span::call_site()); - let fields = self.response_fields_for_selection(query_context, selection, prefix)?; - let field_impls = self.field_impls_for_selection(query_context, selection, &prefix)?; - let description = self.description.as_ref().map(|desc| quote!(#[doc = #desc])); - Ok(quote! { - #(#field_impls)* - - #derives - #description - pub struct #name { - #(#fields,)* - } - }) - } - - pub(crate) fn field_impls_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - field_impls_for_selection(&self.fields, query_context, selection, prefix) - } - - pub(crate) fn response_fields_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - response_fields_for_selection(&self.name, &self.fields, query_context, selection, prefix) - } -} - -#[cfg(test)] -mod test { - use super::*; - use graphql_parser::query; - use graphql_parser::Pos; - - fn mock_field(directives: Vec) -> schema::Field { - schema::Field { - position: Pos::default(), - description: None, - name: "foo".to_string(), - arguments: vec![], - field_type: schema::Type::NamedType("x".to_string()), - directives, - } - } - - #[test] - fn deprecation_no_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![], - }; - let result = parse_deprecation_info(&mock_field(vec![directive])); - assert_eq!(DeprecationStatus::Deprecated(None), result); - } - - #[test] - fn deprecation_with_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![( - "reason".to_string(), - query::Value::String("whatever".to_string()), - )], - }; - let result = parse_deprecation_info(&mock_field(vec![directive])); - assert_eq!( - DeprecationStatus::Deprecated(Some("whatever".to_string())), - result - ); - } - - #[test] - fn null_deprecation_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![("reason".to_string(), query::Value::Null)], - }; - let result = parse_deprecation_info(&mock_field(vec![directive])); - assert_eq!(DeprecationStatus::Deprecated(None), result); - } - - #[test] - #[should_panic] - fn invalid_deprecation_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![("reason".to_string(), query::Value::Boolean(true))], - }; - let _ = parse_deprecation_info(&mock_field(vec![directive])); - } - - #[test] - fn no_deprecation() { - let result = parse_deprecation_info(&mock_field(vec![])); - assert_eq!(DeprecationStatus::Current, result); - } -} diff --git a/graphql_client_codegen/src/operations.rs b/graphql_client_codegen/src/operations.rs deleted file mode 100644 index 50b22a828..000000000 --- a/graphql_client_codegen/src/operations.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::constants::*; -use crate::query::QueryContext; -use crate::selection::Selection; -use crate::variables::Variable; -use graphql_parser::query::OperationDefinition; -use heck::SnakeCase; -use proc_macro2::{Span, TokenStream}; -use quote::quote; -use syn::Ident; - -#[derive(Debug, Clone)] -pub enum OperationType { - Query, - Mutation, - Subscription, -} - -#[derive(Debug, Clone)] -pub struct Operation<'query> { - pub name: String, - pub operation_type: OperationType, - pub variables: Vec>, - pub selection: Selection<'query>, -} - -impl<'query> Operation<'query> { - pub(crate) fn root_name<'schema>( - &self, - schema: &'schema crate::schema::Schema<'_>, - ) -> &'schema str { - match self.operation_type { - OperationType::Query => schema.query_type.unwrap_or("Query"), - OperationType::Mutation => schema.mutation_type.unwrap_or("Mutation"), - OperationType::Subscription => schema.subscription_type.unwrap_or("Subscription"), - } - } - - pub(crate) fn is_subscription(&self) -> bool { - match self.operation_type { - OperationType::Subscription => true, - _ => false, - } - } - - /// Generate the Variables struct and all the necessary supporting code. - pub(crate) fn expand_variables(&self, context: &QueryContext<'_, '_>) -> TokenStream { - let variables = &self.variables; - let variables_derives = context.variables_derives(); - - if variables.is_empty() { - return quote! { - #variables_derives - pub struct Variables; - }; - } - - let fields = variables.iter().map(|variable| { - let ty = variable.ty.to_rust(context, ""); - let rust_safe_field_name = - crate::shared::keyword_replace(&variable.name.to_snake_case()); - let rename = - crate::shared::field_rename_annotation(&variable.name, &rust_safe_field_name); - let name = Ident::new(&rust_safe_field_name, Span::call_site()); - - quote!(#rename pub #name: #ty) - }); - - let default_constructors = variables - .iter() - .map(|variable| variable.generate_default_value_constructor(context)); - - quote! { - #variables_derives - pub struct Variables { - #(#fields,)* - } - - impl Variables { - #(#default_constructors)* - } - } - } -} - -impl<'query> std::convert::From<&'query OperationDefinition> for Operation<'query> { - fn from(definition: &'query OperationDefinition) -> Operation<'query> { - match *definition { - OperationDefinition::Query(ref q) => Operation { - name: q.name.clone().expect("unnamed operation"), - operation_type: OperationType::Query, - variables: q.variable_definitions.iter().map(|v| v.into()).collect(), - selection: (&q.selection_set).into(), - }, - OperationDefinition::Mutation(ref m) => Operation { - name: m.name.clone().expect("unnamed operation"), - operation_type: OperationType::Mutation, - variables: m.variable_definitions.iter().map(|v| v.into()).collect(), - selection: (&m.selection_set).into(), - }, - OperationDefinition::Subscription(ref s) => Operation { - name: s.name.clone().expect("unnamed operation"), - operation_type: OperationType::Subscription, - variables: s.variable_definitions.iter().map(|v| v.into()).collect(), - selection: (&s.selection_set).into(), - }, - OperationDefinition::SelectionSet(_) => panic!(SELECTION_SET_AT_ROOT), - } - } -} diff --git a/graphql_client_codegen/src/query.rs b/graphql_client_codegen/src/query.rs index 15aef89d1..36b894a6a 100644 --- a/graphql_client_codegen/src/query.rs +++ b/graphql_client_codegen/src/query.rs @@ -1,220 +1,673 @@ -use crate::deprecation::DeprecationStrategy; -use crate::fragments::GqlFragment; -use crate::normalization::Normalization; -use crate::schema::Schema; -use crate::selection::Selection; -use failure::*; -use proc_macro2::Span; -use proc_macro2::TokenStream; -use quote::quote; -use std::collections::{BTreeMap, BTreeSet}; -use syn::Ident; - -/// This holds all the information we need during the code generation phase. -pub(crate) struct QueryContext<'query, 'schema: 'query> { - pub fragments: BTreeMap<&'query str, GqlFragment<'query>>, - pub schema: &'schema Schema<'schema>, - pub deprecation_strategy: DeprecationStrategy, - pub normalization: Normalization, - variables_derives: Vec, - response_derives: Vec, -} - -impl<'query, 'schema> QueryContext<'query, 'schema> { - /// Create a QueryContext with the given Schema. - pub(crate) fn new( - schema: &'schema Schema<'schema>, - deprecation_strategy: DeprecationStrategy, - normalization: Normalization, - ) -> QueryContext<'query, 'schema> { - QueryContext { - fragments: BTreeMap::new(), - schema, - deprecation_strategy, - normalization, - variables_derives: vec![Ident::new("Serialize", Span::call_site())], - response_derives: vec![Ident::new("Deserialize", Span::call_site())], +//! The responsibility of this module is to bind and validate a query +//! against a given schema. + +mod fragments; +mod operations; +mod selection; +mod validation; + +pub(crate) use fragments::{fragment_is_recursive, ResolvedFragment}; +pub(crate) use operations::ResolvedOperation; +pub(crate) use selection::*; + +use crate::{ + constants::TYPENAME_FIELD, + normalization::Normalization, + schema::{ + resolve_field_type, EnumId, InputId, ScalarId, Schema, StoredEnum, StoredFieldType, + StoredInputType, StoredScalar, TypeId, UnionId, + }, +}; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(crate) struct SelectionId(u32); +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct OperationId(u32); + +impl OperationId { + pub(crate) fn new(idx: usize) -> Self { + OperationId(idx as u32) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(crate) struct ResolvedFragmentId(u32); + +#[derive(Debug, Clone, Copy)] +pub(crate) struct VariableId(u32); + +pub(crate) fn resolve( + schema: &Schema, + query: &graphql_parser::query::Document, +) -> anyhow::Result { + let mut resolved_query: Query = Default::default(); + + create_roots(&mut resolved_query, query, schema)?; + + // Then resolve the selections. + for definition in &query.definitions { + match definition { + graphql_parser::query::Definition::Fragment(fragment) => { + resolve_fragment(&mut resolved_query, schema, fragment)? + } + graphql_parser::query::Definition::Operation(operation) => { + resolve_operation(&mut resolved_query, schema, operation)? + } } } - /// Mark a fragment as required, so code is actually generated for it. - pub(crate) fn require_fragment(&self, typename_: &str) { - if let Some(fragment) = self.fragments.get(typename_) { - fragment.require(&self); + // Validation: to be expanded and factored out. + validation::validate_typename_presence(&BoundQuery { + query: &resolved_query, + schema, + })?; + + for (selection_id, _) in resolved_query.selections() { + selection::validate_type_conditions( + selection_id, + &BoundQuery { + query: &resolved_query, + schema, + }, + )? + } + + Ok(resolved_query) +} + +fn create_roots( + resolved_query: &mut Query, + query: &graphql_parser::query::Document, + schema: &Schema, +) -> anyhow::Result<()> { + // First, give ids to all fragments and operations. + for definition in &query.definitions { + match definition { + graphql_parser::query::Definition::Fragment(fragment) => { + let graphql_parser::query::TypeCondition::On(on) = &fragment.type_condition; + resolved_query.fragments.push(ResolvedFragment { + name: fragment.name.clone(), + on: schema.find_type(on).ok_or_else(|| { + anyhow::anyhow!( + "Could not find type {} for fragment {} in schema.", + on, + fragment.name + ) + })?, + selection_set: Vec::new(), + }); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::Mutation(m), + ) => { + let on = schema.mutation_type().ok_or_else(|| { + anyhow::anyhow!( + "Query contains a mutation operation, but the schema has no mutation type." + ) + })?; + let resolved_operation: ResolvedOperation = ResolvedOperation { + object_id: on, + name: m.name.as_ref().expect("mutation without name").to_owned(), + _operation_type: operations::OperationType::Mutation, + selection_set: Vec::with_capacity(m.selection_set.items.len()), + }; + + resolved_query.operations.push(resolved_operation); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::Query(q), + ) => { + let on = schema.query_type(); + let resolved_operation: ResolvedOperation = ResolvedOperation { + name: q.name.as_ref().expect("query without name").to_owned(), + _operation_type: operations::OperationType::Query, + object_id: on, + selection_set: Vec::with_capacity(q.selection_set.items.len()), + }; + + resolved_query.operations.push(resolved_operation); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::Subscription(s), + ) => { + let on = schema.subscription_type().ok_or_else(|| { + anyhow::anyhow!( + "Query contains a subscription operation, but the schema has no subscription type." + ) + })?; + + if s.selection_set.items.len() != 1 { + anyhow::bail!("{}", crate::constants::MULTIPLE_SUBSCRIPTION_FIELDS_ERROR) + } + + let resolved_operation: ResolvedOperation = ResolvedOperation { + name: s + .name + .as_ref() + .expect("subscription without name") + .to_owned(), + _operation_type: operations::OperationType::Subscription, + object_id: on, + selection_set: Vec::with_capacity(s.selection_set.items.len()), + }; + + resolved_query.operations.push(resolved_operation); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::SelectionSet(_), + ) => anyhow::bail!("{}", crate::constants::SELECTION_SET_AT_ROOT), } } - /// For testing only. creates an empty QueryContext with an empty Schema. - #[cfg(test)] - pub(crate) fn new_empty(schema: &'schema Schema<'_>) -> QueryContext<'query, 'schema> { - QueryContext { - fragments: BTreeMap::new(), - schema, - deprecation_strategy: DeprecationStrategy::Allow, - normalization: Normalization::None, - variables_derives: vec![Ident::new("Serialize", Span::call_site())], - response_derives: vec![Ident::new("Deserialize", Span::call_site())], + Ok(()) +} + +fn resolve_fragment( + query: &mut Query, + schema: &Schema, + fragment_definition: &graphql_parser::query::FragmentDefinition, +) -> anyhow::Result<()> { + let graphql_parser::query::TypeCondition::On(on) = &fragment_definition.type_condition; + let on = schema.find_type(&on).ok_or_else(|| { + anyhow::anyhow!( + "Could not find type `{}` referenced by fragment `{}`", + on, + fragment_definition.name + ) + })?; + + let (id, _) = query + .find_fragment(&fragment_definition.name) + .ok_or_else(|| { + anyhow::anyhow!("Could not find fragment `{}`.", fragment_definition.name) + })?; + + resolve_selection( + query, + on, + &fragment_definition.selection_set, + SelectionParent::Fragment(id), + schema, + )?; + + Ok(()) +} + +fn resolve_union_selection( + query: &mut Query, + _union_id: UnionId, + selection_set: &graphql_parser::query::SelectionSet, + parent: SelectionParent, + schema: &Schema, +) -> anyhow::Result<()> { + for item in selection_set.items.iter() { + match item { + graphql_parser::query::Selection::Field(field) => { + if field.name == TYPENAME_FIELD { + let id = query.push_selection(Selection::Typename, parent); + parent.add_to_selection_set(query, id); + } else { + anyhow::bail!("Invalid field selection on union field ({:?})", parent); + } + } + graphql_parser::query::Selection::InlineFragment(inline_fragment) => { + let selection_id = resolve_inline_fragment(query, schema, inline_fragment, parent)?; + parent.add_to_selection_set(query, selection_id); + } + graphql_parser::query::Selection::FragmentSpread(fragment_spread) => { + let (fragment_id, _fragment) = query + .find_fragment(&fragment_spread.fragment_name) + .ok_or_else(|| { + anyhow::anyhow!( + "Could not find fragment `{}` referenced by fragment spread.", + fragment_spread.fragment_name + ) + })?; + + let id = query.push_selection(Selection::FragmentSpread(fragment_id), parent); + + parent.add_to_selection_set(query, id); + } } } - /// Expand the deserialization data structures for the given field. - pub(crate) fn maybe_expand_field( - &self, - ty: &str, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - if self.schema.contains_scalar(ty) { - Ok(None) - } else if let Some(enm) = self.schema.enums.get(ty) { - enm.is_required.set(true); - Ok(None) // we already expand enums separately - } else if let Some(obj) = self.schema.objects.get(ty) { - obj.is_required.set(true); - obj.response_for_selection(self, &selection, prefix) - .map(Some) - } else if let Some(iface) = self.schema.interfaces.get(ty) { - iface.is_required.set(true); - iface - .response_for_selection(self, &selection, prefix) - .map(Some) - } else if let Some(unn) = self.schema.unions.get(ty) { - unn.is_required.set(true); - unn.response_for_selection(self, &selection, prefix) - .map(Some) - } else { - Err(format_err!("Unknown type: {}", ty)) + Ok(()) +} + +fn resolve_object_selection<'a>( + query: &mut Query, + object: &dyn crate::schema::ObjectLike, + selection_set: &graphql_parser::query::SelectionSet, + parent: SelectionParent, + schema: &'a Schema, +) -> anyhow::Result<()> { + for item in selection_set.items.iter() { + match item { + graphql_parser::query::Selection::Field(field) => { + if field.name == TYPENAME_FIELD { + let id = query.push_selection(Selection::Typename, parent); + parent.add_to_selection_set(query, id); + continue; + } + + let (field_id, schema_field) = object + .get_field_by_name(&field.name, schema) + .ok_or_else(|| { + anyhow::anyhow!("No field named {} on {}", &field.name, object.name()) + })?; + + let id = query.push_selection( + Selection::Field(SelectedField { + alias: field.alias.clone(), + field_id, + selection_set: Vec::with_capacity(selection_set.items.len()), + }), + parent, + ); + + resolve_selection( + query, + schema_field.r#type.id, + &field.selection_set, + SelectionParent::Field(id), + schema, + )?; + + parent.add_to_selection_set(query, id); + } + graphql_parser::query::Selection::InlineFragment(inline) => { + let selection_id = resolve_inline_fragment(query, schema, inline, parent)?; + + parent.add_to_selection_set(query, selection_id); + } + graphql_parser::query::Selection::FragmentSpread(fragment_spread) => { + let (fragment_id, _fragment) = query + .find_fragment(&fragment_spread.fragment_name) + .ok_or_else(|| { + anyhow::anyhow!( + "Could not find fragment `{}` referenced by fragment spread.", + fragment_spread.fragment_name + ) + })?; + + let id = query.push_selection(Selection::FragmentSpread(fragment_id), parent); + + parent.add_to_selection_set(query, id); + } } } - pub(crate) fn ingest_response_derives( - &mut self, - attribute_value: &str, - ) -> Result<(), failure::Error> { - if self.response_derives.len() > 1 { - return Err(format_err!( - "ingest_response_derives should only be called once" - )); + Ok(()) +} + +fn resolve_selection( + ctx: &mut Query, + on: TypeId, + selection_set: &graphql_parser::query::SelectionSet, + parent: SelectionParent, + schema: &Schema, +) -> anyhow::Result<()> { + match on { + TypeId::Object(oid) => { + let object = schema.get_object(oid); + resolve_object_selection(ctx, object, selection_set, parent, schema)?; + } + TypeId::Interface(interface_id) => { + let interface = schema.get_interface(interface_id); + resolve_object_selection(ctx, interface, selection_set, parent, schema)?; + } + TypeId::Union(union_id) => { + resolve_union_selection(ctx, union_id, selection_set, parent, schema)?; + } + other => { + anyhow::ensure!( + selection_set.items.is_empty(), + "Selection set on non-object, non-interface type. ({:?})", + other + ); + } + }; + + Ok(()) +} + +fn resolve_inline_fragment( + query: &mut Query, + schema: &Schema, + inline_fragment: &graphql_parser::query::InlineFragment, + parent: SelectionParent, +) -> anyhow::Result { + let graphql_parser::query::TypeCondition::On(on) = inline_fragment + .type_condition + .as_ref() + .expect("missing type condition on inline fragment"); + let type_id = schema.find_type(on).ok_or_else(|| { + anyhow::anyhow!( + "Could not find type `{}` referenced by inline fragment.", + on + ) + })?; + + let id = query.push_selection( + Selection::InlineFragment(InlineFragment { + type_id, + selection_set: Vec::with_capacity(inline_fragment.selection_set.items.len()), + }), + parent, + ); + + resolve_selection( + query, + type_id, + &inline_fragment.selection_set, + SelectionParent::InlineFragment(id), + schema, + )?; + + Ok(id) +} + +fn resolve_operation( + query: &mut Query, + schema: &Schema, + operation: &graphql_parser::query::OperationDefinition, +) -> anyhow::Result<()> { + match operation { + graphql_parser::query::OperationDefinition::Mutation(m) => { + let on = schema.mutation_type().ok_or_else(|| { + anyhow::anyhow!( + "Query contains a mutation operation, but the schema has no mutation type." + ) + })?; + let on = schema.get_object(on); + + let (id, _) = query.find_operation(m.name.as_ref().unwrap()).unwrap(); + + resolve_variables(query, &m.variable_definitions, schema, id); + resolve_object_selection( + query, + on, + &m.selection_set, + SelectionParent::Operation(id), + schema, + )?; + } + graphql_parser::query::OperationDefinition::Query(q) => { + let on = schema.get_object(schema.query_type()); + let (id, _) = query.find_operation(q.name.as_ref().unwrap()).unwrap(); + + resolve_variables(query, &q.variable_definitions, schema, id); + resolve_object_selection( + query, + on, + &q.selection_set, + SelectionParent::Operation(id), + schema, + )?; } + graphql_parser::query::OperationDefinition::Subscription(s) => { + let on = schema.subscription_type().ok_or_else(|| anyhow::anyhow!("Query contains a subscription operation, but the schema has no subscription type."))?; + let on = schema.get_object(on); + let (id, _) = query.find_operation(s.name.as_ref().unwrap()).unwrap(); - self.response_derives.extend( - attribute_value - .split(',') - .map(str::trim) - .map(|s| Ident::new(s, Span::call_site())), - ); - Ok(()) - } - - pub(crate) fn ingest_variables_derives( - &mut self, - attribute_value: &str, - ) -> Result<(), failure::Error> { - if self.variables_derives.len() > 1 { - return Err(format_err!( - "ingest_variables_derives should only be called once" - )); + resolve_variables(query, &s.variable_definitions, schema, id); + resolve_object_selection( + query, + on, + &s.selection_set, + SelectionParent::Operation(id), + schema, + )?; + } + graphql_parser::query::OperationDefinition::SelectionSet(_) => { + unreachable!("unnamed queries are not supported") } + } + + Ok(()) +} + +#[derive(Default)] +pub(crate) struct Query { + fragments: Vec, + operations: Vec, + selection_parent_idx: HashMap, + selections: Vec, + variables: Vec, +} + +impl Query { + fn push_selection(&mut self, node: Selection, parent: SelectionParent) -> SelectionId { + let id = SelectionId(self.selections.len() as u32); + self.selections.push(node); - self.variables_derives.extend( - attribute_value - .split(',') - .map(str::trim) - .map(|s| Ident::new(s, Span::call_site())), - ); - Ok(()) + self.selection_parent_idx.insert(id, parent); + + id } - pub(crate) fn variables_derives(&self) -> TokenStream { - let derives: BTreeSet<&Ident> = self.variables_derives.iter().collect(); - let derives = derives.iter(); + pub fn operations(&self) -> impl Iterator { + walk_operations(self) + } - quote! { - #[derive( #(#derives),* )] - } + pub(crate) fn get_selection(&self, id: SelectionId) -> &Selection { + self.selections + .get(id.0 as usize) + .expect("Query.get_selection") } - pub(crate) fn response_derives(&self) -> TokenStream { - let derives: BTreeSet<&Ident> = self.response_derives.iter().collect(); - let derives = derives.iter(); - quote! { - #[derive( #(#derives),* )] - } + pub(crate) fn get_fragment(&self, id: ResolvedFragmentId) -> &ResolvedFragment { + self.fragments + .get(id.0 as usize) + .expect("Query.get_fragment") } - pub(crate) fn response_enum_derives(&self) -> TokenStream { - let always_derives = [ - Ident::new("Eq", Span::call_site()), - Ident::new("PartialEq", Span::call_site()), - ]; - let mut enum_derives: BTreeSet<_> = self - .response_derives + pub(crate) fn get_operation(&self, id: OperationId) -> &ResolvedOperation { + self.operations + .get(id.0 as usize) + .expect("Query.get_operation") + } + + /// Selects the first operation matching `struct_name`. Returns `None` when the query document defines no operation, or when the selected operation does not match any defined operation. + pub(crate) fn select_operation<'a>( + &'a self, + name: &str, + normalization: Normalization, + ) -> Option<(OperationId, &'a ResolvedOperation)> { + walk_operations(self).find(|(_id, op)| normalization.operation(&op.name) == name) + } + + fn find_fragment(&mut self, name: &str) -> Option<(ResolvedFragmentId, &mut ResolvedFragment)> { + self.fragments + .iter_mut() + .enumerate() + .find(|(_, frag)| frag.name == name) + .map(|(id, f)| (ResolvedFragmentId(id as u32), f)) + } + + fn find_operation(&mut self, name: &str) -> Option<(OperationId, &mut ResolvedOperation)> { + self.operations + .iter_mut() + .enumerate() + .find(|(_, op)| op.name == name) + .map(|(id, op)| (OperationId::new(id), op)) + } + + fn selections(&self) -> impl Iterator { + self.selections + .iter() + .enumerate() + .map(|(idx, selection)| (SelectionId(idx as u32), selection)) + } + + fn walk_selection_set<'a>( + &'a self, + selection_ids: &'a [SelectionId], + ) -> impl Iterator + 'a { + selection_ids .iter() - .filter(|derive| { - // Do not apply the "Default" derive to enums. - let derive = derive.to_string(); - derive != "Serialize" && derive != "Deserialize" && derive != "Default" - }) - .collect(); - enum_derives.extend(always_derives.iter()); - quote! { - #[derive( #(#enum_derives),* )] + .map(move |id| (*id, self.get_selection(*id))) + } +} + +#[derive(Debug)] +pub(crate) struct ResolvedVariable { + pub(crate) operation_id: OperationId, + pub(crate) name: String, + pub(crate) default: Option, + pub(crate) r#type: StoredFieldType, +} + +impl ResolvedVariable { + pub(crate) fn type_name<'schema>(&self, schema: &'schema Schema) -> &'schema str { + self.r#type.id.name(schema) + } + + fn collect_used_types(&self, used_types: &mut UsedTypes, schema: &Schema) { + match self.r#type.id { + TypeId::Input(input_id) => { + used_types.types.insert(TypeId::Input(input_id)); + + let input = schema.get_input(input_id); + + input.used_input_ids_recursive(used_types, schema) + } + type_id @ TypeId::Scalar(_) | type_id @ TypeId::Enum(_) => { + used_types.types.insert(type_id); + } + _ => (), } } } -#[cfg(test)] -mod tests { - use super::*; +#[derive(Debug, Default)] +pub(crate) struct UsedTypes { + pub(crate) types: HashSet, + fragments: HashSet, +} - #[test] - fn response_derives_ingestion_works() { - let schema = crate::schema::Schema::new(); - let mut context = QueryContext::new_empty(&schema); +impl UsedTypes { + pub(crate) fn inputs<'s, 'a: 's>( + &'s self, + schema: &'a Schema, + ) -> impl Iterator + 's { + schema + .inputs() + .filter(move |(id, _input)| self.types.contains(&TypeId::Input(*id))) + } - context - .ingest_response_derives("PartialEq, PartialOrd, Serialize") - .unwrap(); + pub(crate) fn scalars<'s, 'a: 's>( + &'s self, + schema: &'a Schema, + ) -> impl Iterator + 's { + self.types + .iter() + .filter_map(TypeId::as_scalar_id) + .map(move |scalar_id| (scalar_id, schema.get_scalar(scalar_id))) + .filter(|(_id, scalar)| !crate::schema::DEFAULT_SCALARS.contains(&scalar.name.as_str())) + } - assert_eq!( - context.response_derives().to_string(), - "# [ derive ( Deserialize , PartialEq , PartialOrd , Serialize ) ]" - ); + pub(crate) fn enums<'a, 'schema: 'a>( + &'a self, + schema: &'schema Schema, + ) -> impl Iterator + 'a { + self.types + .iter() + .filter_map(TypeId::as_enum_id) + .map(move |enum_id| (enum_id, schema.get_enum(enum_id))) } - #[test] - fn response_enum_derives_does_not_produce_empty_list() { - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - assert_eq!( - context.response_enum_derives().to_string(), - "# [ derive ( Eq , PartialEq ) ]" - ); + pub(crate) fn fragment_ids<'b>(&'b self) -> impl Iterator + 'b { + self.fragments.iter().copied() } +} - #[test] - fn response_enum_derives_works() { - let schema = crate::schema::Schema::new(); - let mut context = QueryContext::new_empty(&schema); +fn resolve_variables( + query: &mut Query, + variables: &[graphql_parser::query::VariableDefinition], + schema: &Schema, + operation_id: OperationId, +) { + for var in variables { + query.variables.push(ResolvedVariable { + operation_id, + name: var.name.clone(), + default: var.default_value.clone(), + r#type: resolve_field_type(schema, &var.var_type), + }); + } +} - context - .ingest_response_derives("PartialEq, PartialOrd, Serialize") - .unwrap(); +pub(crate) fn walk_operations( + query: &Query, +) -> impl Iterator { + query + .operations + .iter() + .enumerate() + .map(|(id, op)| (OperationId(id as u32), op)) +} + +pub(crate) fn operation_has_no_variables(operation_id: OperationId, query: &Query) -> bool { + walk_operation_variables(operation_id, query) + .next() + .is_none() +} - assert_eq!( - context.response_enum_derives().to_string(), - "# [ derive ( Eq , PartialEq , PartialOrd ) ]" - ); +pub(crate) fn walk_operation_variables( + operation_id: OperationId, + query: &Query, +) -> impl Iterator { + query + .variables + .iter() + .enumerate() + .map(|(idx, var)| (VariableId(idx as u32), var)) + .filter(move |(_id, var)| var.operation_id == operation_id) +} + +pub(crate) fn all_used_types(operation_id: OperationId, query: &BoundQuery<'_>) -> UsedTypes { + let mut used_types = UsedTypes::default(); + + let operation = query.query.get_operation(operation_id); + + for (_id, selection) in query.query.walk_selection_set(&operation.selection_set) { + selection.collect_used_types(&mut used_types, query); } - #[test] - fn response_derives_fails_when_called_twice() { - let schema = crate::schema::Schema::new(); - let mut context = QueryContext::new_empty(&schema); + for (_id, variable) in walk_operation_variables(operation_id, query.query) { + variable.collect_used_types(&mut used_types, query.schema); + } + + used_types +} + +pub(crate) fn full_path_prefix(selection_id: SelectionId, query: &BoundQuery<'_>) -> String { + let mut path = match query.query.get_selection(selection_id) { + Selection::FragmentSpread(_) | Selection::InlineFragment(_) => Vec::new(), + selection => vec![selection.to_path_segment(query)], + }; + + let mut item = selection_id; - assert!(context - .ingest_response_derives("PartialEq, PartialOrd") - .is_ok()); - assert!(context.ingest_response_derives("Serialize").is_err()); + while let Some(parent) = query.query.selection_parent_idx.get(&item) { + path.push(parent.to_path_segment(query)); + + match parent { + SelectionParent::Field(id) | SelectionParent::InlineFragment(id) => { + item = *id; + } + _ => break, + } } + + path.reverse(); + path.join("") +} + +#[derive(Clone, Copy)] +pub(crate) struct BoundQuery<'a> { + pub(crate) query: &'a Query, + pub(crate) schema: &'a Schema, } diff --git a/graphql_client_codegen/src/query/fragments.rs b/graphql_client_codegen/src/query/fragments.rs new file mode 100644 index 000000000..2a667696d --- /dev/null +++ b/graphql_client_codegen/src/query/fragments.rs @@ -0,0 +1,24 @@ +use super::{Query, ResolvedFragmentId, SelectionId}; +use crate::schema::TypeId; +use heck::*; + +#[derive(Debug)] +pub(crate) struct ResolvedFragment { + pub(crate) name: String, + pub(crate) on: TypeId, + pub(crate) selection_set: Vec, +} + +impl ResolvedFragment { + pub(super) fn to_path_segment(&self) -> String { + self.name.to_camel_case() + } +} + +pub(crate) fn fragment_is_recursive(fragment_id: ResolvedFragmentId, query: &Query) -> bool { + let fragment = query.get_fragment(fragment_id); + + query + .walk_selection_set(&fragment.selection_set) + .any(|(_id, selection)| selection.contains_fragment(fragment_id, query)) +} diff --git a/graphql_client_codegen/src/query/operations.rs b/graphql_client_codegen/src/query/operations.rs new file mode 100644 index 000000000..f48a1bbf0 --- /dev/null +++ b/graphql_client_codegen/src/query/operations.rs @@ -0,0 +1,23 @@ +use super::SelectionId; +use crate::schema::ObjectId; +use heck::*; + +#[derive(Debug, Clone)] +pub(crate) enum OperationType { + Query, + Mutation, + Subscription, +} + +pub(crate) struct ResolvedOperation { + pub(crate) name: String, + pub(crate) _operation_type: OperationType, + pub(crate) selection_set: Vec, + pub(crate) object_id: ObjectId, +} + +impl ResolvedOperation { + pub(crate) fn to_path_segment(&self) -> String { + self.name.to_camel_case() + } +} diff --git a/graphql_client_codegen/src/query/selection.rs b/graphql_client_codegen/src/query/selection.rs new file mode 100644 index 000000000..2c08f5bb1 --- /dev/null +++ b/graphql_client_codegen/src/query/selection.rs @@ -0,0 +1,257 @@ +use super::{BoundQuery, OperationId, Query, ResolvedFragmentId, SelectionId, UsedTypes}; +use crate::schema::{Schema, StoredField, StoredFieldId, TypeId}; +use heck::CamelCase; + +/// This checks that the `on` clause on fragment spreads and inline fragments +/// are valid in their context. +pub(super) fn validate_type_conditions( + selection_id: SelectionId, + query: &BoundQuery<'_>, +) -> anyhow::Result<()> { + let selection = query.query.get_selection(selection_id); + + let selected_type = match selection { + Selection::FragmentSpread(fragment_id) => query.query.get_fragment(*fragment_id).on, + Selection::InlineFragment(inline_fragment) => inline_fragment.type_id, + _ => return Ok(()), + }; + + let parent_schema_type_id = query + .query + .selection_parent_idx + .get(&selection_id) + .expect("Could not find selection parent") + .schema_type_id(query); + + if parent_schema_type_id == selected_type { + return Ok(()); + } + + match parent_schema_type_id { + TypeId::Union(union_id) => { + let union = query.schema.get_union(union_id); + + anyhow::ensure!( + union + .variants + .iter() + .any(|variant| *variant == selected_type), + "The spread {}... on {} is not valid.", + union.name, + selected_type.name(query.schema), + ) + } + TypeId::Interface(interface_id) => { + let mut variants = query + .schema + .objects() + .filter(|(_, obj)| obj.implements_interfaces.contains(&interface_id)); + + anyhow::ensure!( + variants.any(|(id, _)| TypeId::Object(id) == selected_type), + "The spread {}... on {} is not valid.", + parent_schema_type_id.name(query.schema), + selected_type.name(query.schema), + ) + } + _ => (), + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy)] +pub(super) enum SelectionParent { + Field(SelectionId), + InlineFragment(SelectionId), + Fragment(ResolvedFragmentId), + Operation(OperationId), +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +impl SelectionParent { + fn schema_type_id(&self, query: &BoundQuery<'_>) -> TypeId { + match self { + SelectionParent::Fragment(fragment_id) => query.query.get_fragment(*fragment_id).on, + SelectionParent::Operation(operation_id) => { + TypeId::Object(query.query.get_operation(*operation_id).object_id) + } + SelectionParent::Field(id) => { + let field_id = query + .query + .get_selection(*id) + .as_selected_field() + .unwrap() + .field_id; + query.schema.get_field(field_id).r#type.id + } + SelectionParent::InlineFragment(id) => { + { query.query.get_selection(*id).as_inline_fragment().unwrap() }.type_id + } + } + } + + pub(super) fn add_to_selection_set(&self, q: &mut Query, selection_id: SelectionId) { + match self { + SelectionParent::Field(parent_selection_id) + | SelectionParent::InlineFragment(parent_selection_id) => { + let parent_selection = q + .selections + .get_mut(parent_selection_id.0 as usize) + .expect("get parent selection"); + + match parent_selection { + Selection::Field(f) => f.selection_set.push(selection_id), + Selection::InlineFragment(inline) => inline.selection_set.push(selection_id), + other => unreachable!("impossible parent selection: {:?}", other), + } + } + SelectionParent::Fragment(fragment_id) => { + let fragment = q + .fragments + .get_mut(fragment_id.0 as usize) + .expect("get fragment"); + + fragment.selection_set.push(selection_id); + } + SelectionParent::Operation(operation_id) => { + let operation = q + .operations + .get_mut(operation_id.0 as usize) + .expect("get operation"); + + operation.selection_set.push(selection_id); + } + } + } + + pub(crate) fn to_path_segment(&self, query: &BoundQuery<'_>) -> String { + match self { + SelectionParent::Field(id) | SelectionParent::InlineFragment(id) => { + query.query.get_selection(*id).to_path_segment(query) + } + SelectionParent::Operation(id) => query.query.get_operation(*id).to_path_segment(), + SelectionParent::Fragment(id) => query.query.get_fragment(*id).to_path_segment(), + } + } +} + +#[derive(Debug)] +pub(crate) enum Selection { + Field(SelectedField), + InlineFragment(InlineFragment), + FragmentSpread(ResolvedFragmentId), + Typename, +} + +impl Selection { + pub(crate) fn as_selected_field(&self) -> Option<&SelectedField> { + match self { + Selection::Field(f) => Some(f), + _ => None, + } + } + + pub(crate) fn as_inline_fragment(&self) -> Option<&InlineFragment> { + match self { + Selection::InlineFragment(f) => Some(f), + _ => None, + } + } + + pub(crate) fn collect_used_types(&self, used_types: &mut UsedTypes, query: &BoundQuery<'_>) { + match self { + Selection::Field(field) => { + let stored_field = query.schema.get_field(field.field_id); + used_types.types.insert(stored_field.r#type.id); + + for selection_id in self.subselection() { + let selection = query.query.get_selection(*selection_id); + selection.collect_used_types(used_types, query); + } + } + Selection::InlineFragment(inline_fragment) => { + used_types.types.insert(inline_fragment.type_id); + + for selection_id in self.subselection() { + let selection = query.query.get_selection(*selection_id); + selection.collect_used_types(used_types, query); + } + } + Selection::FragmentSpread(fragment_id) => { + // This is necessary to avoid infinite recursion. + if used_types.fragments.contains(fragment_id) { + return; + } + + used_types.fragments.insert(*fragment_id); + + let fragment = query.query.get_fragment(*fragment_id); + + for (_id, selection) in query.query.walk_selection_set(&fragment.selection_set) { + selection.collect_used_types(used_types, query); + } + } + Selection::Typename => (), + } + } + + pub(crate) fn contains_fragment(&self, fragment_id: ResolvedFragmentId, query: &Query) -> bool { + match self { + Selection::FragmentSpread(id) => *id == fragment_id, + _ => self.subselection().iter().any(|selection_id| { + query + .get_selection(*selection_id) + .contains_fragment(fragment_id, query) + }), + } + } + + pub(crate) fn subselection(&self) -> &[SelectionId] { + match self { + Selection::Field(field) => field.selection_set.as_slice(), + Selection::InlineFragment(inline_fragment) => &inline_fragment.selection_set, + _ => &[], + } + } + + pub(super) fn to_path_segment(&self, query: &BoundQuery<'_>) -> String { + match self { + Selection::Field(field) => field + .alias + .as_ref() + .map(|alias| alias.to_camel_case()) + .unwrap_or_else(move || { + query.schema.get_field(field.field_id).name.to_camel_case() + }), + Selection::InlineFragment(inline_fragment) => format!( + "On{}", + inline_fragment.type_id.name(query.schema).to_camel_case() + ), + other => unreachable!("{:?} in to_path_segment", other), + } + } +} + +#[derive(Debug)] +pub(crate) struct InlineFragment { + pub(crate) type_id: TypeId, + pub(crate) selection_set: Vec, +} + +#[derive(Debug)] +pub(crate) struct SelectedField { + pub(crate) alias: Option, + pub(crate) field_id: StoredFieldId, + pub(crate) selection_set: Vec, +} + +impl SelectedField { + pub(crate) fn alias(&self) -> Option<&str> { + self.alias.as_deref() + } + + pub(crate) fn schema_field<'a>(&self, schema: &'a Schema) -> &'a StoredField { + schema.get_field(self.field_id) + } +} diff --git a/graphql_client_codegen/src/query/validation.rs b/graphql_client_codegen/src/query/validation.rs new file mode 100644 index 000000000..1dafe512b --- /dev/null +++ b/graphql_client_codegen/src/query/validation.rs @@ -0,0 +1,70 @@ +use super::{full_path_prefix, BoundQuery, Query, Selection, SelectionId}; +use crate::schema::TypeId; + +pub(super) fn validate_typename_presence(query: &BoundQuery<'_>) -> anyhow::Result<()> { + for fragment in query.query.fragments.iter() { + let type_id = match fragment.on { + id @ TypeId::Interface(_) | id @ TypeId::Union(_) => id, + _ => continue, + }; + + if !selection_set_contains_type_name(fragment.on, &fragment.selection_set, query.query) { + anyhow::bail!( + "The `{}` fragment uses `{}` but does not select `__typename` on it. graphql-client cannot generate code for it. Please add `__typename` to the selection.", + &fragment.name, + type_id.name(query.schema), + ) + } + } + + let union_and_interface_field_selections = + query + .query + .selections() + .filter_map(|(selection_id, selection)| match selection { + Selection::Field(field) => match query.schema.get_field(field.field_id).r#type.id { + id @ TypeId::Interface(_) | id @ TypeId::Union(_) => { + Some((selection_id, id, &field.selection_set)) + } + _ => None, + }, + _ => None, + }); + + for selection in union_and_interface_field_selections { + if !selection_set_contains_type_name(selection.1, selection.2, query.query) { + anyhow::bail!( + "The query uses `{path}` at `{selected_type}` but does not select `__typename` on it. graphql-client cannot generate code for it. Please add `__typename` to the selection.", + path = full_path_prefix(selection.0, query), + selected_type = selection.1.name(query.schema) + ); + } + } + + Ok(()) +} + +fn selection_set_contains_type_name( + parent_type_id: TypeId, + selection_set: &[SelectionId], + query: &Query, +) -> bool { + for id in selection_set { + let selection = query.get_selection(*id); + + match selection { + Selection::Typename => return true, + Selection::FragmentSpread(fragment_id) => { + let fragment = query.get_fragment(*fragment_id); + if fragment.on == parent_type_id + && selection_set_contains_type_name(fragment.on, &fragment.selection_set, query) + { + return true; + } + } + _ => (), + } + } + + false +} diff --git a/graphql_client_codegen/src/scalars.rs b/graphql_client_codegen/src/scalars.rs deleted file mode 100644 index 85979ea94..000000000 --- a/graphql_client_codegen/src/scalars.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::normalization::Normalization; -use quote::quote; -use std::cell::Cell; - -#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] -pub struct Scalar<'schema> { - pub name: &'schema str, - pub description: Option<&'schema str>, - pub is_required: Cell, -} - -impl<'schema> Scalar<'schema> { - // TODO: do something smarter here - pub fn to_rust(&self, norm: Normalization) -> proc_macro2::TokenStream { - use proc_macro2::{Ident, Span}; - - let name = norm.scalar_name(self.name); - let ident = Ident::new(&name, Span::call_site()); - let description = &self.description.map(|d| quote!(#[doc = #d])); - - quote!(#description type #ident = super::#ident;) - } -} diff --git a/graphql_client_codegen/src/schema.rs b/graphql_client_codegen/src/schema.rs index 5b67a6ed4..96b99524e 100644 --- a/graphql_client_codegen/src/schema.rs +++ b/graphql_client_codegen/src/schema.rs @@ -1,435 +1,525 @@ -use crate::deprecation::DeprecationStatus; -use crate::enums::{EnumVariant, GqlEnum}; -use crate::field_type::FieldType; -use crate::inputs::GqlInput; -use crate::interfaces::GqlInterface; -use crate::objects::{GqlObject, GqlObjectField}; -use crate::scalars::Scalar; -use crate::unions::GqlUnion; -use failure::*; -use graphql_parser::{self, schema}; -use std::collections::{BTreeMap, BTreeSet}; +mod graphql_parser_conversion; +mod json_conversion; + +#[cfg(test)] +mod tests; + +use crate::query::UsedTypes; +use crate::type_qualifiers::GraphqlTypeQualifier; +use std::collections::HashMap; pub(crate) const DEFAULT_SCALARS: &[&str] = &["ID", "String", "Int", "Float", "Boolean"]; +#[derive(Debug, PartialEq, Clone)] +struct StoredObjectField { + name: String, + object: ObjectId, +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct StoredObject { + pub(crate) name: String, + pub(crate) fields: Vec, + pub(crate) implements_interfaces: Vec, +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct StoredField { + pub(crate) name: String, + pub(crate) r#type: StoredFieldType, + pub(crate) parent: StoredFieldParent, + /// `Some(None)` should be interpreted as "deprecated, without reason" + pub(crate) deprecation: Option>, +} + +impl StoredField { + pub(crate) fn deprecation(&self) -> Option> { + self.deprecation.as_ref().map(|inner| inner.as_deref()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) enum StoredFieldParent { + Object(ObjectId), + Interface(InterfaceId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct ObjectId(u32); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct ObjectFieldId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct InterfaceId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct ScalarId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct UnionId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct EnumId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) struct InputId(u32); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct StoredFieldId(usize); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct InputFieldId(usize); + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredInterface { + name: String, + fields: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredFieldType { + pub(crate) id: TypeId, + /// An ordered list of qualifiers, from outer to inner. + /// + /// e.g. `[Int]!` would have `vec![List, Optional]`, but `[Int!]` would have `vec![Optional, + /// List]`. + pub(crate) qualifiers: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredUnion { + pub(crate) name: String, + pub(crate) variants: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredScalar { + pub(crate) name: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] +pub(crate) enum TypeId { + Object(ObjectId), + Scalar(ScalarId), + Interface(InterfaceId), + Union(UnionId), + Enum(EnumId), + Input(InputId), +} + +impl TypeId { + fn r#enum(id: usize) -> Self { + TypeId::Enum(EnumId(id)) + } + + fn interface(id: usize) -> Self { + TypeId::Interface(InterfaceId(id)) + } + + fn union(id: usize) -> Self { + TypeId::Union(UnionId(id)) + } + + fn object(id: u32) -> Self { + TypeId::Object(ObjectId(id)) + } + + fn input(id: u32) -> Self { + TypeId::Input(InputId(id)) + } + + fn as_interface_id(&self) -> Option { + match self { + TypeId::Interface(id) => Some(*id), + _ => None, + } + } + + fn as_object_id(&self) -> Option { + match self { + TypeId::Object(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn as_input_id(&self) -> Option { + match self { + TypeId::Input(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn as_scalar_id(&self) -> Option { + match self { + TypeId::Scalar(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn as_enum_id(&self) -> Option { + match self { + TypeId::Enum(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn name<'a>(&self, schema: &'a Schema) -> &'a str { + match self { + TypeId::Object(obj) => schema.get_object(*obj).name.as_str(), + TypeId::Scalar(s) => schema.get_scalar(*s).name.as_str(), + TypeId::Interface(s) => schema.get_interface(*s).name.as_str(), + TypeId::Union(s) => schema.get_union(*s).name.as_str(), + TypeId::Enum(s) => schema.get_enum(*s).name.as_str(), + TypeId::Input(s) => schema.get_input(*s).name.as_str(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredEnum { + pub(crate) name: String, + pub(crate) variants: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredInputFieldType { + pub(crate) id: TypeId, + pub(crate) qualifiers: Vec, +} + +impl StoredInputFieldType { + /// A type is indirected if it is a (flat or nested) list type, optional or not. + /// + /// We use this to determine whether a type needs to be boxed for recursion. + pub(crate) fn is_indirected(&self) -> bool { + self.qualifiers + .iter() + .any(|qualifier| qualifier == &GraphqlTypeQualifier::List) + } + + pub(crate) fn is_optional(&self) -> bool { + self.qualifiers + .get(0) + .map(|qualifier| !qualifier.is_required()) + .unwrap_or(true) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredInputType { + pub(crate) name: String, + pub(crate) fields: Vec<(String, StoredInputFieldType)>, +} + /// Intermediate representation for a parsed GraphQL schema used during code generation. #[derive(Debug, Clone, PartialEq)] -pub(crate) struct Schema<'schema> { - pub(crate) enums: BTreeMap<&'schema str, GqlEnum<'schema>>, - pub(crate) inputs: BTreeMap<&'schema str, GqlInput<'schema>>, - pub(crate) interfaces: BTreeMap<&'schema str, GqlInterface<'schema>>, - pub(crate) objects: BTreeMap<&'schema str, GqlObject<'schema>>, - pub(crate) scalars: BTreeMap<&'schema str, Scalar<'schema>>, - pub(crate) unions: BTreeMap<&'schema str, GqlUnion<'schema>>, - pub(crate) query_type: Option<&'schema str>, - pub(crate) mutation_type: Option<&'schema str>, - pub(crate) subscription_type: Option<&'schema str>, +pub(crate) struct Schema { + stored_objects: Vec, + stored_fields: Vec, + stored_interfaces: Vec, + stored_unions: Vec, + stored_scalars: Vec, + stored_enums: Vec, + stored_inputs: Vec, + names: HashMap, + + pub(crate) query_type: Option, + pub(crate) mutation_type: Option, + pub(crate) subscription_type: Option, } -impl<'schema> Schema<'schema> { - pub(crate) fn new() -> Schema<'schema> { - Schema { - enums: BTreeMap::new(), - inputs: BTreeMap::new(), - interfaces: BTreeMap::new(), - objects: BTreeMap::new(), - scalars: BTreeMap::new(), - unions: BTreeMap::new(), +impl Schema { + pub(crate) fn new() -> Schema { + let mut schema = Schema { + stored_objects: Vec::new(), + stored_interfaces: Vec::new(), + stored_fields: Vec::new(), + stored_unions: Vec::new(), + stored_scalars: Vec::with_capacity(DEFAULT_SCALARS.len()), + stored_enums: Vec::new(), + stored_inputs: Vec::new(), + names: HashMap::new(), query_type: None, mutation_type: None, subscription_type: None, + }; + + schema.push_default_scalars(); + + schema + } + + fn push_default_scalars(&mut self) { + for scalar in DEFAULT_SCALARS { + let id = self.push_scalar(StoredScalar { + name: (*scalar).to_owned(), + }); + + self.names.insert((*scalar).to_owned(), TypeId::Scalar(id)); } } - pub(crate) fn ingest_interface_implementations( - &mut self, - impls: BTreeMap<&'schema str, Vec<&'schema str>>, - ) -> Result<(), failure::Error> { - impls - .into_iter() - .map(|(iface_name, implementors)| { - let iface = self - .interfaces - .get_mut(&iface_name) - .ok_or_else(|| format_err!("interface not found: {}", iface_name))?; - iface.implemented_by = implementors.iter().cloned().collect(); - Ok(()) - }) - .collect() - } - - pub(crate) fn require(&self, typename_: &str) { - DEFAULT_SCALARS + fn push_object(&mut self, object: StoredObject) -> ObjectId { + let id = ObjectId(self.stored_objects.len() as u32); + self.stored_objects.push(object); + + id + } + + fn push_interface(&mut self, interface: StoredInterface) -> InterfaceId { + let id = InterfaceId(self.stored_interfaces.len()); + + self.stored_interfaces.push(interface); + + id + } + + fn push_scalar(&mut self, scalar: StoredScalar) -> ScalarId { + let id = ScalarId(self.stored_scalars.len()); + + self.stored_scalars.push(scalar); + + id + } + + fn push_enum(&mut self, enm: StoredEnum) -> EnumId { + let id = EnumId(self.stored_enums.len()); + + self.stored_enums.push(enm); + + id + } + + fn push_field(&mut self, field: StoredField) -> StoredFieldId { + let id = StoredFieldId(self.stored_fields.len()); + + self.stored_fields.push(field); + + id + } + + pub(crate) fn query_type(&self) -> ObjectId { + self.query_type + .expect("Query operation type must be defined") + } + + pub(crate) fn mutation_type(&self) -> Option { + self.mutation_type + } + + pub(crate) fn subscription_type(&self) -> Option { + self.subscription_type + } + + pub(crate) fn get_interface(&self, interface_id: InterfaceId) -> &StoredInterface { + self.stored_interfaces.get(interface_id.0).unwrap() + } + + pub(crate) fn get_input(&self, input_id: InputId) -> &StoredInputType { + self.stored_inputs.get(input_id.0 as usize).unwrap() + } + + pub(crate) fn get_object(&self, object_id: ObjectId) -> &StoredObject { + self.stored_objects + .get(object_id.0 as usize) + .expect("Schema::get_object") + } + + pub(crate) fn get_field(&self, field_id: StoredFieldId) -> &StoredField { + self.stored_fields.get(field_id.0).unwrap() + } + + pub(crate) fn get_enum(&self, enum_id: EnumId) -> &StoredEnum { + self.stored_enums.get(enum_id.0).unwrap() + } + + pub(crate) fn get_scalar(&self, scalar_id: ScalarId) -> &StoredScalar { + self.stored_scalars.get(scalar_id.0).unwrap() + } + + pub(crate) fn get_union(&self, union_id: UnionId) -> &StoredUnion { + self.stored_unions + .get(union_id.0) + .expect("Schema::get_union") + } + + fn find_interface(&self, interface_name: &str) -> InterfaceId { + self.find_type_id(interface_name).as_interface_id().unwrap() + } + + pub(crate) fn find_type(&self, type_name: &str) -> Option { + self.names.get(type_name).copied() + } + + pub(crate) fn objects(&self) -> impl Iterator { + self.stored_objects .iter() - .find(|&&s| s == typename_) - .map(|_| ()) - .or_else(|| { - self.enums - .get(typename_) - .map(|enm| enm.is_required.set(true)) - }) - .or_else(|| self.inputs.get(typename_).map(|input| input.require(self))) - .or_else(|| { - self.objects - .get(typename_) - .map(|object| object.require(self)) - }) - .or_else(|| { - self.scalars - .get(typename_) - .map(|scalar| scalar.is_required.set(true)) - }); + .enumerate() + .map(|(idx, obj)| (ObjectId(idx as u32), obj)) } - pub(crate) fn contains_scalar(&self, type_name: &str) -> bool { - DEFAULT_SCALARS.iter().any(|s| s == &type_name) || self.scalars.contains_key(type_name) - } - - pub(crate) fn fragment_target( - &self, - target_name: &str, - ) -> Option> { - self.objects - .get(target_name) - .map(crate::fragments::FragmentTarget::Object) - .or_else(|| { - self.interfaces - .get(target_name) - .map(crate::fragments::FragmentTarget::Interface) - }) - .or_else(|| { - self.unions - .get(target_name) - .map(crate::fragments::FragmentTarget::Union) - }) + pub(crate) fn inputs(&self) -> impl Iterator { + self.stored_inputs + .iter() + .enumerate() + .map(|(idx, obj)| (InputId(idx as u32), obj)) + } + + fn find_type_id(&self, type_name: &str) -> TypeId { + match self.names.get(type_name) { + Some(id) => *id, + None => { + panic!( + "graphql-client-codegen internal error: failed to resolve TypeId for `{}°.", + type_name + ); + } + } } } -impl<'schema> std::convert::From<&'schema graphql_parser::schema::Document> for Schema<'schema> { - fn from(ast: &'schema graphql_parser::schema::Document) -> Schema<'schema> { - let mut schema = Schema::new(); - - // Holds which objects implement which interfaces so we can populate GqlInterface#implemented_by later. - // It maps interface names to a vec of implementation names. - let mut interface_implementations: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); - - for definition in &ast.definitions { - match definition { - schema::Definition::TypeDefinition(ty_definition) => match ty_definition { - schema::TypeDefinition::Object(obj) => { - for implementing in &obj.implements_interfaces { - let name = &obj.name; - interface_implementations - .entry(implementing) - .and_modify(|objects| objects.push(name)) - .or_insert_with(|| vec![name]); - } - - schema - .objects - .insert(&obj.name, GqlObject::from_graphql_parser_object(&obj)); - } - schema::TypeDefinition::Enum(enm) => { - schema.enums.insert( - &enm.name, - GqlEnum { - name: &enm.name, - description: enm.description.as_deref(), - variants: enm - .values - .iter() - .map(|v| EnumVariant { - description: v.description.as_deref(), - name: &v.name, - }) - .collect(), - is_required: false.into(), - }, - ); - } - schema::TypeDefinition::Scalar(scalar) => { - schema.scalars.insert( - &scalar.name, - Scalar { - name: &scalar.name, - description: scalar.description.as_deref(), - is_required: false.into(), - }, - ); +impl StoredInputType { + pub(crate) fn used_input_ids_recursive(&self, used_types: &mut UsedTypes, schema: &Schema) { + for type_id in self.fields.iter().map(|(_name, ty)| ty.id) { + match type_id { + TypeId::Input(input_id) => { + if used_types.types.contains(&type_id) { + continue; + } else { + used_types.types.insert(type_id); + let input = schema.get_input(input_id); + input.used_input_ids_recursive(used_types, schema); } - schema::TypeDefinition::Union(union) => { - let variants: BTreeSet<&str> = - union.types.iter().map(String::as_str).collect(); - schema.unions.insert( - &union.name, - GqlUnion { - name: &union.name, - variants, - description: union.description.as_deref(), - is_required: false.into(), - }, - ); - } - schema::TypeDefinition::Interface(interface) => { - let mut iface = - GqlInterface::new(&interface.name, interface.description.as_deref()); - iface - .fields - .extend(interface.fields.iter().map(|f| GqlObjectField { - description: f.description.as_deref(), - name: f.name.as_str(), - type_: FieldType::from(&f.field_type), - deprecation: DeprecationStatus::Current, - })); - schema.interfaces.insert(&interface.name, iface); - } - schema::TypeDefinition::InputObject(input) => { - schema.inputs.insert(&input.name, GqlInput::from(input)); - } - }, - schema::Definition::DirectiveDefinition(_) => (), - schema::Definition::TypeExtension(_extension) => (), - schema::Definition::SchemaDefinition(definition) => { - schema.query_type = definition.query.as_deref(); - schema.mutation_type = definition.mutation.as_deref(); - schema.subscription_type = definition.subscription.as_deref(); } + TypeId::Enum(_) | TypeId::Scalar(_) => { + used_types.types.insert(type_id); + } + _ => (), } } + } - schema - .ingest_interface_implementations(interface_implementations) - .expect("schema ingestion"); + fn contains_type_without_indirection(&self, input_id: InputId, schema: &Schema) -> bool { + // The input type is recursive if any of its members contains it, without indirection + self.fields.iter().any(|(_name, field_type)| { + // the field is indirected, so no boxing is needed + if field_type.is_indirected() { + return false; + } - schema + let field_input_id = field_type.id.as_input_id(); + + if let Some(field_input_id) = field_input_id { + if field_input_id == input_id { + return true; + } + + let input = schema.get_input(field_input_id); + + // we check if the other input contains this one (without indirection) + input.contains_type_without_indirection(input_id, schema) + } else { + // the field is not referring to an input type + false + } + }) + } +} + +pub(crate) fn input_is_recursive_without_indirection(input_id: InputId, schema: &Schema) -> bool { + let input = schema.get_input(input_id); + input.contains_type_without_indirection(input_id, schema) +} + +impl std::convert::From for Schema { + fn from(ast: graphql_parser::schema::Document) -> Schema { + graphql_parser_conversion::build_schema(ast) } } -impl<'schema> - std::convert::From< - &'schema graphql_introspection_query::introspection_response::IntrospectionResponse, - > for Schema<'schema> +impl std::convert::From + for Schema { fn from( - src: &'schema graphql_introspection_query::introspection_response::IntrospectionResponse, + src: graphql_introspection_query::introspection_response::IntrospectionResponse, ) -> Self { - use graphql_introspection_query::introspection_response::__TypeKind; - - let mut schema = Schema::new(); - let root = src - .as_schema() - .schema - .as_ref() - .expect("__schema is not null"); - - schema.query_type = root - .query_type - .as_ref() - .and_then(|ty| ty.name.as_ref()) - .map(String::as_str); - schema.mutation_type = root - .mutation_type - .as_ref() - .and_then(|ty| ty.name.as_ref()) - .map(String::as_str); - schema.subscription_type = root - .subscription_type - .as_ref() - .and_then(|ty| ty.name.as_ref()) - .map(String::as_str); - - // Holds which objects implement which interfaces so we can populate GqlInterface#implemented_by later. - // It maps interface names to a vec of implementation names. - let mut interface_implementations: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); - - for ty in root - .types - .as_ref() - .expect("types in schema") - .iter() - .filter_map(|t| t.as_ref().map(|t| &t.full_type)) - { - let name: &str = ty.name.as_deref().expect("type definition name"); - - match ty.kind { - Some(__TypeKind::ENUM) => { - let variants: Vec> = ty - .enum_values - .as_ref() - .expect("enum variants") - .iter() - .map(|t| { - t.as_ref().map(|t| EnumVariant { - description: t.description.as_deref(), - name: t.name.as_deref().expect("enum variant name"), - }) - }) - .filter_map(|t| t) - .collect(); - let enm = GqlEnum { - name, - description: ty.description.as_deref(), - variants, - is_required: false.into(), - }; - schema.enums.insert(name, enm); - } - Some(__TypeKind::SCALAR) => { - if DEFAULT_SCALARS.iter().find(|s| s == &&name).is_none() { - schema.scalars.insert( - name, - Scalar { - name, - description: ty.description.as_deref(), - is_required: false.into(), - }, - ); - } - } - Some(__TypeKind::UNION) => { - let variants: BTreeSet<&str> = ty - .possible_types - .as_ref() - .unwrap() - .iter() - .filter_map(|t| t.as_ref().and_then(|t| t.type_ref.name.as_deref())) - .collect(); - schema.unions.insert( - name, - GqlUnion { - name: ty.name.as_deref().expect("unnamed union"), - description: ty.description.as_deref(), - variants, - is_required: false.into(), - }, - ); - } - Some(__TypeKind::OBJECT) => { - for implementing in ty - .interfaces - .as_deref() - .unwrap_or_else(|| &[]) - .iter() - .filter_map(Option::as_ref) - .map(|t| &t.type_ref.name) - { - interface_implementations - .entry(implementing.as_deref().expect("interface name")) - .and_modify(|objects| objects.push(name)) - .or_insert_with(|| vec![name]); - } + json_conversion::build_schema(src) + } +} - schema - .objects - .insert(name, GqlObject::from_introspected_schema_json(ty)); - } - Some(__TypeKind::INTERFACE) => { - let mut iface = GqlInterface::new(name, ty.description.as_deref()); - iface.fields.extend( - ty.fields - .as_ref() - .expect("interface fields") - .iter() - .filter_map(Option::as_ref) - .map(|f| GqlObjectField { - description: f.description.as_deref(), - name: f.name.as_ref().expect("field name").as_str(), - type_: FieldType::from(f.type_.as_ref().expect("field type")), - deprecation: DeprecationStatus::Current, - }), - ); - schema.interfaces.insert(name, iface); - } - Some(__TypeKind::INPUT_OBJECT) => { - schema.inputs.insert(name, GqlInput::from(ty)); +pub(crate) fn resolve_field_type( + schema: &Schema, + inner: &graphql_parser::schema::Type, +) -> StoredFieldType { + use crate::type_qualifiers::graphql_parser_depth; + use graphql_parser::schema::Type::*; + + let qualifiers_depth = graphql_parser_depth(inner); + let mut qualifiers = Vec::with_capacity(qualifiers_depth); + + let mut inner = inner; + + loop { + match inner { + ListType(new_inner) => { + qualifiers.push(GraphqlTypeQualifier::List); + inner = new_inner; + } + NonNullType(new_inner) => { + qualifiers.push(GraphqlTypeQualifier::Required); + inner = new_inner; + } + NamedType(name) => { + return StoredFieldType { + id: schema.find_type_id(name), + qualifiers, } - _ => unimplemented!("unimplemented definition"), } } - - schema - .ingest_interface_implementations(interface_implementations) - .expect("schema ingestion"); - - schema } } -pub(crate) enum ParsedSchema { - GraphQLParser(graphql_parser::schema::Document), - Json(graphql_introspection_query::introspection_response::IntrospectionResponse), +pub(crate) trait ObjectLike { + fn name(&self) -> &str; + + fn get_field_by_name<'a>( + &'a self, + name: &str, + schema: &'a Schema, + ) -> Option<(StoredFieldId, &'a StoredField)>; } -impl<'schema> From<&'schema ParsedSchema> for Schema<'schema> { - fn from(parsed_schema: &'schema ParsedSchema) -> Schema<'schema> { - match parsed_schema { - ParsedSchema::GraphQLParser(s) => s.into(), - ParsedSchema::Json(s) => s.into(), - } +impl ObjectLike for StoredObject { + fn name(&self) -> &str { + &self.name + } + + fn get_field_by_name<'a>( + &'a self, + name: &str, + schema: &'a Schema, + ) -> Option<(StoredFieldId, &'a StoredField)> { + self.fields + .iter() + .map(|field_id| (*field_id, schema.get_field(*field_id))) + .find(|(_, f)| f.name == name) } } -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::*; - - #[test] - fn build_schema_works() { - let gql_schema = include_str!("tests/star_wars_schema.graphql"); - let gql_schema = graphql_parser::parse_schema(gql_schema).unwrap(); - let built = Schema::from(&gql_schema); - assert_eq!( - built.objects.get("Droid"), - Some(&GqlObject { - description: None, - name: "Droid", - fields: vec![ - GqlObjectField { - description: None, - name: TYPENAME_FIELD, - type_: FieldType::new(string_type()), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "id", - type_: FieldType::new("ID").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "name", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "friends", - type_: FieldType::new("Character").list(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "friendsConnection", - type_: FieldType::new("FriendsConnection").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "appearsIn", - type_: FieldType::new("Episode").list().nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "primaryFunction", - type_: FieldType::new("String"), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }) - ) +impl ObjectLike for StoredInterface { + fn name(&self) -> &str { + &self.name + } + + fn get_field_by_name<'a>( + &'a self, + name: &str, + schema: &'a Schema, + ) -> Option<(StoredFieldId, &'a StoredField)> { + self.fields + .iter() + .map(|field_id| (*field_id, schema.get_field(*field_id))) + .find(|(_, field)| field.name == name) } } diff --git a/graphql_client_codegen/src/schema/graphql_parser_conversion.rs b/graphql_client_codegen/src/schema/graphql_parser_conversion.rs new file mode 100644 index 000000000..a351aaeef --- /dev/null +++ b/graphql_client_codegen/src/schema/graphql_parser_conversion.rs @@ -0,0 +1,303 @@ +use super::{Schema, StoredInputFieldType, TypeId}; +use crate::schema::resolve_field_type; +use graphql_parser::schema::{self as parser, Definition, Document, TypeDefinition, UnionType}; + +pub(super) fn build_schema(mut src: graphql_parser::schema::Document) -> super::Schema { + let mut schema = Schema::new(); + convert(&mut src, &mut schema); + schema +} + +fn convert(src: &mut graphql_parser::schema::Document, schema: &mut Schema) { + populate_names_map(schema, &src.definitions); + + src.definitions + .iter_mut() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Scalar(scalar)) => Some(scalar), + _ => None, + }) + .for_each(|scalar| ingest_scalar(schema, scalar)); + + enums_mut(src).for_each(|enm| ingest_enum(schema, enm)); + + unions_mut(src).for_each(|union| ingest_union(schema, union)); + + interfaces_mut(src).for_each(|iface| ingest_interface(schema, iface)); + + objects_mut(src).for_each(|obj| ingest_object(schema, obj)); + + inputs_mut(src).for_each(|input| ingest_input(schema, input)); + + let schema_definition = src.definitions.iter_mut().find_map(|def| match def { + Definition::SchemaDefinition(definition) => Some(definition), + _ => None, + }); + + if let Some(schema_definition) = schema_definition { + schema.query_type = schema_definition + .query + .as_mut() + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + schema.mutation_type = schema_definition + .mutation + .as_mut() + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + schema.subscription_type = schema_definition + .subscription + .as_mut() + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + } else { + schema.query_type = schema.names.get("Query").and_then(|id| id.as_object_id()); + + schema.mutation_type = schema + .names + .get("Mutation") + .and_then(|id| id.as_object_id()); + + schema.subscription_type = schema + .names + .get("Subscription") + .and_then(|id| id.as_object_id()); + }; +} + +fn populate_names_map(schema: &mut Schema, definitions: &[Definition]) { + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Enum(enm)) => Some(enm.name.as_str()), + _ => None, + }) + .enumerate() + .for_each(|(idx, enum_name)| { + schema.names.insert(enum_name.into(), TypeId::r#enum(idx)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Object(object)) => { + Some(object.name.as_str()) + } + _ => None, + }) + .enumerate() + .for_each(|(idx, object_name)| { + schema + .names + .insert(object_name.into(), TypeId::r#object(idx as u32)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Interface(interface)) => { + Some(interface.name.as_str()) + } + _ => None, + }) + .enumerate() + .for_each(|(idx, interface_name)| { + schema + .names + .insert(interface_name.into(), TypeId::interface(idx)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Union(union)) => Some(union.name.as_str()), + _ => None, + }) + .enumerate() + .for_each(|(idx, union_name)| { + schema.names.insert(union_name.into(), TypeId::union(idx)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::InputObject(input)) => { + Some(input.name.as_str()) + } + _ => None, + }) + .enumerate() + .for_each(|(idx, input_name)| { + schema + .names + .insert(input_name.into(), TypeId::input(idx as u32)); + }); +} + +fn ingest_union(schema: &mut Schema, union: &mut UnionType) { + let stored_union = super::StoredUnion { + name: std::mem::take(&mut union.name), + variants: union + .types + .iter() + .map(|name| schema.find_type_id(name)) + .collect(), + }; + + schema.stored_unions.push(stored_union); +} + +fn ingest_object(schema: &mut Schema, obj: &mut graphql_parser::schema::ObjectType) { + let object_id = schema.find_type_id(&obj.name).as_object_id().unwrap(); + let mut field_ids = Vec::with_capacity(obj.fields.len()); + + for field in obj.fields.iter_mut() { + let field = super::StoredField { + name: std::mem::take(&mut field.name), + r#type: resolve_field_type(schema, &field.field_type), + parent: super::StoredFieldParent::Object(object_id), + deprecation: find_deprecation(&field.directives), + }; + + field_ids.push(schema.push_field(field)); + } + + // Ingest the object itself + let object = super::StoredObject { + name: std::mem::take(&mut obj.name), + fields: field_ids, + implements_interfaces: obj + .implements_interfaces + .iter() + .map(|iface_name| schema.find_interface(iface_name)) + .collect(), + }; + + schema.push_object(object); +} + +fn ingest_scalar(schema: &mut Schema, scalar: &mut graphql_parser::schema::ScalarType) { + let name = std::mem::take(&mut scalar.name); + let name_for_names = name.clone(); + + let scalar = super::StoredScalar { name }; + + let scalar_id = schema.push_scalar(scalar); + + schema + .names + .insert(name_for_names, TypeId::Scalar(scalar_id)); +} + +fn ingest_enum(schema: &mut Schema, enm: &mut graphql_parser::schema::EnumType) { + let enm = super::StoredEnum { + name: std::mem::take(&mut enm.name), + variants: enm + .values + .iter_mut() + .map(|value| std::mem::take(&mut value.name)) + .collect(), + }; + + schema.push_enum(enm); +} + +fn ingest_interface(schema: &mut Schema, interface: &mut graphql_parser::schema::InterfaceType) { + let interface_id = schema + .find_type_id(&interface.name) + .as_interface_id() + .unwrap(); + + let mut field_ids = Vec::with_capacity(interface.fields.len()); + + for field in interface.fields.iter_mut() { + let field = super::StoredField { + name: std::mem::take(&mut field.name), + r#type: resolve_field_type(schema, &field.field_type), + parent: super::StoredFieldParent::Interface(interface_id), + deprecation: find_deprecation(&field.directives), + }; + + field_ids.push(schema.push_field(field)); + } + + let new_interface = super::StoredInterface { + name: std::mem::take(&mut interface.name), + fields: field_ids, + }; + + schema.push_interface(new_interface); +} + +fn find_deprecation(directives: &[parser::Directive]) -> Option> { + directives + .iter() + .find(|directive| directive.name == "deprecated") + .map(|directive| { + directive + .arguments + .iter() + .find(|(name, _)| name == "reason") + .and_then(|(_, value)| match value { + graphql_parser::query::Value::String(s) => Some(s.clone()), + _ => None, + }) + }) +} + +fn ingest_input(schema: &mut Schema, input: &mut parser::InputObjectType) { + let input = super::StoredInputType { + name: std::mem::take(&mut input.name), + fields: input + .fields + .iter_mut() + .map(|val| { + let field_type = super::resolve_field_type(schema, &val.value_type); + ( + std::mem::take(&mut val.name), + StoredInputFieldType { + qualifiers: field_type.qualifiers, + id: field_type.id, + }, + ) + }) + .collect(), + }; + + schema.stored_inputs.push(input); +} + +fn objects_mut(doc: &mut Document) -> impl Iterator { + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Object(obj)) => Some(obj), + _ => None, + }) +} + +fn interfaces_mut(doc: &mut Document) -> impl Iterator { + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Interface(interface)) => Some(interface), + _ => None, + }) +} + +fn unions_mut(doc: &mut Document) -> impl Iterator { + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Union(union)) => Some(union), + _ => None, + }) +} + +fn enums_mut(doc: &mut Document) -> impl Iterator { + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Enum(r#enum)) => Some(r#enum), + _ => None, + }) +} + +fn inputs_mut(doc: &mut Document) -> impl Iterator { + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::InputObject(input)) => Some(input), + _ => None, + }) +} diff --git a/graphql_client_codegen/src/schema/json_conversion.rs b/graphql_client_codegen/src/schema/json_conversion.rs new file mode 100644 index 000000000..0eb2d0086 --- /dev/null +++ b/graphql_client_codegen/src/schema/json_conversion.rs @@ -0,0 +1,364 @@ +use super::{Schema, TypeId}; +use graphql_introspection_query::introspection_response::{ + FullType, IntrospectionResponse, Schema as JsonSchema, TypeRef, __TypeKind, +}; + +pub(super) fn build_schema(src: IntrospectionResponse) -> Schema { + let mut src = src.into_schema().schema.expect("could not find schema"); + let mut schema = Schema::new(); + build_names_map(&mut src, &mut schema); + convert(&mut src, &mut schema); + + schema +} + +fn build_names_map(src: &mut JsonSchema, schema: &mut Schema) { + let names = &mut schema.names; + names.reserve(types_mut(src).count()); + + unions_mut(src) + .map(|u| u.name.as_ref().expect("union name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::union(idx)); + }); + + interfaces_mut(src) + .map(|iface| iface.name.as_ref().expect("interface name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::interface(idx)); + }); + + objects_mut(src) + .map(|obj| obj.name.as_ref().expect("object name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::object(idx as u32)); + }); + + inputs_mut(src) + .map(|obj| obj.name.as_ref().expect("input name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::input(idx as u32)); + }); +} + +fn convert(src: &mut JsonSchema, schema: &mut Schema) { + for scalar in scalars_mut(src) { + ingest_scalar(schema, scalar); + } + + for enm in enums_mut(src) { + ingest_enum(schema, enm) + } + + for interface in interfaces_mut(src) { + ingest_interface(schema, interface); + } + + for object in objects_mut(src) { + ingest_object(schema, object); + } + + for unn in unions_mut(src) { + ingest_union(schema, unn) + } + + for input in inputs_mut(src) { + ingest_input(schema, input); + } + + // Define the root operations. + { + schema.query_type = src + .query_type + .as_mut() + .and_then(|n| n.name.as_mut()) + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + schema.mutation_type = src + .mutation_type + .as_mut() + .and_then(|n| n.name.as_mut()) + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + schema.subscription_type = src + .subscription_type + .as_mut() + .and_then(|n| n.name.as_mut()) + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + } +} + +fn types_mut(schema: &mut JsonSchema) -> impl Iterator { + schema + .types + .as_mut() + .expect("schema.types.as_mut()") + .iter_mut() + .filter_map(|t| -> Option<&mut FullType> { t.as_mut().map(|f| &mut f.full_type) }) +} + +fn objects_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::OBJECT)) +} + +fn enums_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::ENUM)) +} + +fn interfaces_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::INTERFACE)) +} + +fn unions_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::UNION)) +} + +fn inputs_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::INPUT_OBJECT)) +} + +fn scalars_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| { + t.kind == Some(__TypeKind::SCALAR) + && !super::DEFAULT_SCALARS.contains(&t.name.as_deref().expect("FullType.name")) + }) +} + +fn ingest_scalar(schema: &mut Schema, scalar: &mut FullType) { + let name: String = scalar.name.take().expect("scalar.name"); + let names_name = name.clone(); + + let id = schema.push_scalar(super::StoredScalar { name }); + + schema.names.insert(names_name, TypeId::Scalar(id)); +} + +fn ingest_enum(schema: &mut Schema, enm: &mut FullType) { + let name = enm.name.take().expect("enm.name"); + let names_name = name.clone(); + + let variants = enm + .enum_values + .as_mut() + .expect("enm.enum_values.as_mut()") + .iter_mut() + .map(|v| { + std::mem::take( + v.name + .as_mut() + .take() + .expect("variant.name.as_mut().take()"), + ) + }) + .collect(); + + let enm = super::StoredEnum { name, variants }; + + let id = schema.push_enum(enm); + + schema.names.insert(names_name, TypeId::Enum(id)); +} + +fn ingest_interface(schema: &mut Schema, iface: &mut FullType) { + let interface_id = schema + .find_type_id(iface.name.as_ref().expect("iface.name")) + .as_interface_id() + .expect("iface type id as interface id"); + let fields = iface.fields.as_mut().expect("interface.fields"); + let mut field_ids = Vec::with_capacity(fields.len()); + + for field in fields.iter_mut() { + let field = super::StoredField { + parent: super::StoredFieldParent::Interface(interface_id), + name: field.name.take().expect("take field name"), + r#type: resolve_field_type( + schema, + &mut field.type_.as_mut().expect("take field type").type_ref, + ), + deprecation: if let Some(true) = field.is_deprecated { + Some(field.deprecation_reason.clone()) + } else { + None + }, + }; + + field_ids.push(schema.push_field(field)); + } + + let interface = super::StoredInterface { + name: std::mem::take(iface.name.as_mut().expect("iface.name.as_mut")), + fields: field_ids, + }; + + schema.push_interface(interface); +} + +fn ingest_object(schema: &mut Schema, object: &mut FullType) { + let object_id = schema + .find_type_id(object.name.as_ref().expect("object.name")) + .as_object_id() + .expect("ingest_object > as_object_id"); + + let fields = object.fields.as_mut().expect("object.fields.as_mut()"); + let mut field_ids = Vec::with_capacity(fields.len()); + + for field in fields.iter_mut() { + let field = super::StoredField { + parent: super::StoredFieldParent::Object(object_id), + name: field.name.take().expect("take field name"), + r#type: resolve_field_type( + schema, + &mut field.type_.as_mut().expect("take field type").type_ref, + ), + deprecation: if let Some(true) = field.is_deprecated { + Some(field.deprecation_reason.clone()) + } else { + None + }, + }; + + field_ids.push(schema.push_field(field)); + } + + let object = super::StoredObject { + name: object.name.take().expect("take object name"), + implements_interfaces: object + .interfaces + .as_ref() + .map(|ifaces| { + ifaces + .iter() + .map(|iface| { + schema + .names + .get(iface.type_ref.name.as_ref().unwrap()) + .and_then(|type_id| type_id.as_interface_id()) + .ok_or_else(|| { + format!( + "Unknown interface: {}", + iface.type_ref.name.as_ref().unwrap() + ) + }) + .unwrap() + }) + .collect() + }) + .unwrap_or_else(Vec::new), + fields: field_ids, + }; + + schema.push_object(object); +} + +fn ingest_union(schema: &mut Schema, union: &mut FullType) { + let variants = union + .possible_types + .as_ref() + .expect("union.possible_types") + .iter() + .map(|variant| { + schema.find_type_id( + variant + .type_ref + .name + .as_ref() + .expect("variant.type_ref.name"), + ) + }) + .collect(); + let un = super::StoredUnion { + name: union.name.take().expect("union.name.take"), + variants, + }; + + schema.stored_unions.push(un); +} + +fn ingest_input(schema: &mut Schema, input: &mut FullType) { + let mut fields = Vec::new(); + + for field in input + .input_fields + .as_mut() + .expect("Missing input_fields on input") + .iter_mut() + { + fields.push(( + std::mem::take(&mut field.input_value.name), + resolve_input_field_type(schema, &mut field.input_value.type_), + )); + } + + let input = super::StoredInputType { + fields, + name: input.name.take().expect("Input without a name"), + }; + + schema.stored_inputs.push(input); +} + +fn resolve_field_type(schema: &mut Schema, typeref: &mut TypeRef) -> super::StoredFieldType { + from_json_type_inner(schema, typeref) +} + +fn resolve_input_field_type( + schema: &mut Schema, + typeref: &mut TypeRef, +) -> super::StoredInputFieldType { + let field_type = from_json_type_inner(schema, typeref); + + super::StoredInputFieldType { + id: field_type.id, + qualifiers: field_type.qualifiers, + } +} + +fn json_type_qualifiers_depth(typeref: &mut TypeRef) -> usize { + use graphql_introspection_query::introspection_response::*; + + match (typeref.kind.as_mut(), typeref.of_type.as_mut()) { + (Some(__TypeKind::NON_NULL), Some(inner)) => 1 + json_type_qualifiers_depth(inner), + (Some(__TypeKind::LIST), Some(inner)) => 1 + json_type_qualifiers_depth(inner), + (Some(_), None) => 0, + _ => panic!("Non-convertible type in JSON schema: {:?}", typeref), + } +} + +fn from_json_type_inner(schema: &mut Schema, inner: &mut TypeRef) -> super::StoredFieldType { + use crate::type_qualifiers::GraphqlTypeQualifier; + use graphql_introspection_query::introspection_response::*; + + let qualifiers_depth = json_type_qualifiers_depth(inner); + let mut qualifiers = Vec::with_capacity(qualifiers_depth); + + let mut inner = inner; + + loop { + match ( + inner.kind.as_mut(), + inner.of_type.as_mut(), + inner.name.as_mut(), + ) { + (Some(__TypeKind::NON_NULL), Some(new_inner), _) => { + qualifiers.push(GraphqlTypeQualifier::Required); + inner = new_inner.as_mut(); + } + (Some(__TypeKind::LIST), Some(new_inner), _) => { + qualifiers.push(GraphqlTypeQualifier::List); + inner = new_inner.as_mut(); + } + (Some(_), None, Some(name)) => { + return super::StoredFieldType { + id: *schema.names.get(name).expect("schema.names.get(name)"), + qualifiers, + } + } + _ => panic!("Non-convertible type in JSON schema"), + } + } +} diff --git a/graphql_client_codegen/src/schema/tests.rs b/graphql_client_codegen/src/schema/tests.rs new file mode 100644 index 000000000..6a5a51cbc --- /dev/null +++ b/graphql_client_codegen/src/schema/tests.rs @@ -0,0 +1 @@ +mod github; diff --git a/graphql_client_codegen/src/schema/tests/github.rs b/graphql_client_codegen/src/schema/tests/github.rs new file mode 100644 index 000000000..8957b775c --- /dev/null +++ b/graphql_client_codegen/src/schema/tests/github.rs @@ -0,0 +1,127 @@ +use crate::schema::Schema; + +const SCHEMA_JSON: &str = include_str!("github_schema.json"); +const SCHEMA_GRAPHQL: &str = include_str!("github_schema.graphql"); + +#[test] +fn ast_from_graphql_and_json_produce_the_same_schema() { + let json: graphql_introspection_query::introspection_response::IntrospectionResponse = + serde_json::from_str(SCHEMA_JSON).unwrap(); + let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL).unwrap(); + let mut json = Schema::from(json); + let mut gql = Schema::from(graphql_parser_schema); + + assert!(vecs_match(&json.stored_scalars, &gql.stored_scalars)); + + // Root objects + { + assert_eq!( + json.get_object(json.query_type()).name, + gql.get_object(gql.query_type()).name + ); + assert_eq!( + json.mutation_type().map(|t| &json.get_object(t).name), + gql.mutation_type().map(|t| &gql.get_object(t).name), + "Mutation types don't match." + ); + assert_eq!( + json.subscription_type().map(|t| &json.get_object(t).name), + gql.subscription_type().map(|t| &gql.get_object(t).name), + "Subscription types don't match." + ); + } + + // Objects + { + let mut json_stored_objects: Vec<_> = json + .stored_objects + .drain(..) + .filter(|obj| !obj.name.starts_with("__")) + .collect(); + + assert_eq!( + json_stored_objects.len(), + gql.stored_objects.len(), + "Objects count matches." + ); + + json_stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); + + for (j, g) in json_stored_objects + .iter_mut() + .filter(|obj| !obj.name.starts_with("__")) + .zip(gql.stored_objects.iter_mut()) + { + assert_eq!(j.name, g.name); + assert_eq!( + j.implements_interfaces.len(), + g.implements_interfaces.len(), + "{}", + j.name + ); + assert_eq!(j.fields.len(), g.fields.len(), "{}", j.name); + } + } + + // Unions + { + assert_eq!(json.stored_unions.len(), gql.stored_unions.len()); + + json.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_unions.iter().zip(gql.stored_unions.iter()) { + assert_eq!(json.variants.len(), gql.variants.len()); + } + } + + // Interfaces + { + assert_eq!(json.stored_interfaces.len(), gql.stored_interfaces.len()); + + json.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json + .stored_interfaces + .iter() + .zip(gql.stored_interfaces.iter()) + { + assert_eq!(json.fields.len(), gql.fields.len()); + } + } + + // Input objects + { + json.stored_enums = json + .stored_enums + .drain(..) + .filter(|enm| !enm.name.starts_with("__")) + .collect(); + assert_eq!(json.stored_inputs.len(), gql.stored_inputs.len()); + + json.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_inputs.iter().zip(gql.stored_inputs.iter()) { + assert_eq!(json.fields.len(), gql.fields.len()); + } + } + + // Enums + { + assert_eq!(json.stored_enums.len(), gql.stored_enums.len()); + + json.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_enums.iter().zip(gql.stored_enums.iter()) { + assert_eq!(json.variants.len(), gql.variants.len()); + } + } +} + +fn vecs_match(a: &[T], b: &[T]) -> bool { + a.len() == b.len() && a.iter().all(|a| b.iter().any(|b| a == b)) +} diff --git a/graphql_client_codegen/src/tests/github_schema.graphql b/graphql_client_codegen/src/schema/tests/github_schema.graphql similarity index 100% rename from graphql_client_codegen/src/tests/github_schema.graphql rename to graphql_client_codegen/src/schema/tests/github_schema.graphql diff --git a/graphql_client_codegen/src/tests/github_schema.json b/graphql_client_codegen/src/schema/tests/github_schema.json similarity index 100% rename from graphql_client_codegen/src/tests/github_schema.json rename to graphql_client_codegen/src/schema/tests/github_schema.json diff --git a/graphql_client_codegen/src/selection.rs b/graphql_client_codegen/src/selection.rs deleted file mode 100644 index 6a8dc88e6..000000000 --- a/graphql_client_codegen/src/selection.rs +++ /dev/null @@ -1,360 +0,0 @@ -use crate::constants::*; -use failure::*; -use graphql_parser::query::SelectionSet; -use std::collections::BTreeMap; - -/// A single object field as part of a selection. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SelectionField<'query> { - pub alias: Option<&'query str>, - pub name: &'query str, - pub fields: Selection<'query>, -} - -/// A spread fragment in a selection (e.g. `...MyFragment`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SelectionFragmentSpread<'query> { - pub fragment_name: &'query str, -} - -/// An inline fragment as part of a selection (e.g. `...on MyThing { name }`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SelectionInlineFragment<'query> { - pub on: &'query str, - pub fields: Selection<'query>, -} - -/// An element in a query selection. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SelectionItem<'query> { - Field(SelectionField<'query>), - FragmentSpread(SelectionFragmentSpread<'query>), - InlineFragment(SelectionInlineFragment<'query>), -} - -impl<'query> SelectionItem<'query> { - pub fn as_typename(&self) -> Option<&SelectionField<'_>> { - if let SelectionItem::Field(f) = self { - if f.name == TYPENAME_FIELD { - return Some(f); - } - } - None - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Selection<'query>(Vec>); - -impl<'query> Selection<'query> { - pub(crate) fn extract_typename<'s, 'context: 's>( - &'s self, - context: &'context crate::query::QueryContext<'_, '_>, - ) -> Option<&SelectionField<'_>> { - // __typename is selected directly - if let Some(field) = self.0.iter().filter_map(SelectionItem::as_typename).next() { - return Some(field); - }; - - // typename is selected through a fragment - (&self) - .into_iter() - .filter_map(|f| match f { - SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { - Some(fragment_name) - } - _ => None, - }) - .filter_map(|fragment_name| { - let fragment = context.fragments.get(fragment_name); - - fragment.and_then(|fragment| fragment.selection.extract_typename(context)) - }) - .next() - } - - // Implementation helper for `selected_variants_on_union`. - fn selected_variants_on_union_inner<'s>( - &'s self, - context: &'s crate::query::QueryContext<'_, '_>, - selected_variants: &mut BTreeMap<&'s str, Selection<'s>>, - // the name of the type the selection applies to - selection_on: &str, - ) -> Result<(), failure::Error> { - for item in self.0.iter() { - match item { - SelectionItem::Field(_) => (), - SelectionItem::InlineFragment(inline_fragment) => { - selected_variants - .entry(inline_fragment.on) - .and_modify(|entry| entry.0.extend(inline_fragment.fields.0.clone())) - .or_insert_with(|| { - let mut items = Vec::with_capacity(inline_fragment.fields.0.len()); - items.extend(inline_fragment.fields.0.clone()); - Selection(items) - }); - } - SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { - let fragment = context - .fragments - .get(fragment_name) - .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))?; - - // The fragment can either be on the union/interface itself, or on one of its variants (type-refining fragment). - if fragment.on.name() == selection_on { - // The fragment is on the union/interface itself. - fragment.selection.selected_variants_on_union_inner( - context, - selected_variants, - selection_on, - )?; - } else { - // Type-refining fragment - selected_variants - .entry(fragment.on.name()) - .and_modify(|entry| entry.0.extend(fragment.selection.0.clone())) - .or_insert_with(|| { - let mut items = Vec::with_capacity(fragment.selection.0.len()); - items.extend(fragment.selection.0.clone()); - Selection(items) - }); - } - } - } - } - - Ok(()) - } - - /// This method should only be invoked on selections on union and interface fields. It returns a map from the name of the selected variants to the corresponding selections. - /// - /// Importantly, it will "flatten" the fragments and handle multiple selections of the same variant. - /// - /// The `context` argument is required so we can expand the fragments. - pub(crate) fn selected_variants_on_union<'s>( - &'s self, - context: &'s crate::query::QueryContext<'_, '_>, - // the name of the type the selection applies to - selection_on: &str, - ) -> Result>, failure::Error> { - let mut selected_variants = BTreeMap::new(); - - self.selected_variants_on_union_inner(context, &mut selected_variants, selection_on)?; - - Ok(selected_variants) - } - - #[cfg(test)] - pub(crate) fn new_empty() -> Selection<'static> { - Selection(Vec::new()) - } - - #[cfg(test)] - pub(crate) fn from_vec(vec: Vec>) -> Self { - Selection(vec) - } - - pub(crate) fn contains_fragment(&self, fragment_name: &str) -> bool { - (&self).into_iter().any(|item| match item { - SelectionItem::Field(field) => field.fields.contains_fragment(fragment_name), - SelectionItem::InlineFragment(inline_fragment) => { - inline_fragment.fields.contains_fragment(fragment_name) - } - SelectionItem::FragmentSpread(fragment) => fragment.fragment_name == fragment_name, - }) - } - - pub(crate) fn len(&self) -> usize { - self.0.len() - } - - pub(crate) fn require_items<'s>(&self, context: &crate::query::QueryContext<'query, 's>) { - self.0.iter().for_each(|item| { - if let SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) = item { - context.require_fragment(fragment_name); - } - }) - } -} - -impl<'query> std::convert::From<&'query SelectionSet> for Selection<'query> { - fn from(selection_set: &SelectionSet) -> Selection<'_> { - use graphql_parser::query::Selection; - - let mut items = Vec::with_capacity(selection_set.items.len()); - - for item in &selection_set.items { - let converted = match item { - Selection::Field(f) => SelectionItem::Field(SelectionField { - alias: f.alias.as_deref(), - name: &f.name, - fields: (&f.selection_set).into(), - }), - Selection::FragmentSpread(spread) => { - SelectionItem::FragmentSpread(SelectionFragmentSpread { - fragment_name: &spread.fragment_name, - }) - } - Selection::InlineFragment(inline) => { - let graphql_parser::query::TypeCondition::On(ref name) = inline - .type_condition - .as_ref() - .expect("Missing `on` clause."); - SelectionItem::InlineFragment(SelectionInlineFragment { - on: &name, - fields: (&inline.selection_set).into(), - }) - } - }; - items.push(converted); - } - - Selection(items) - } -} - -impl<'a, 'query> std::iter::IntoIterator for &'a Selection<'query> { - type Item = &'a SelectionItem<'query>; - type IntoIter = std::slice::Iter<'a, SelectionItem<'query>>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -impl<'a> std::iter::FromIterator> for Selection<'a> { - fn from_iter>>(iter: T) -> Selection<'a> { - Selection(iter.into_iter().collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn selection_extract_typename_simple_case() { - let selection = Selection::new_empty(); - let schema = crate::schema::Schema::new(); - let context = crate::query::QueryContext::new_empty(&schema); - - assert!(selection.extract_typename(&context).is_none()); - } - - #[test] - fn selection_extract_typename_in_fragment() { - let mut selection = Selection::new_empty(); - selection - .0 - .push(SelectionItem::FragmentSpread(SelectionFragmentSpread { - fragment_name: "MyFragment", - })); - - let mut fragment_selection = Selection::new_empty(); - fragment_selection - .0 - .push(SelectionItem::Field(SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - })); - - let schema = crate::schema::Schema::new(); - let obj = crate::objects::GqlObject::new("MyObject", None); - let mut context = crate::query::QueryContext::new_empty(&schema); - context.fragments.insert( - "MyFragment", - crate::fragments::GqlFragment { - name: "MyFragment", - on: crate::fragments::FragmentTarget::Object(&obj), - selection: fragment_selection, - is_required: std::cell::Cell::new(false), - }, - ); - - assert!(selection.extract_typename(&context).is_some()); - } - - #[test] - fn selection_from_graphql_parser_selection_set() { - let query = r##" - query { - animal { - isCat - isHorse - ...Timestamps - barks - ...on Dog { - rating - } - pawsCount - aliased: sillyName - } - } - "##; - let parsed = graphql_parser::parse_query(query).unwrap(); - let selection_set: &graphql_parser::query::SelectionSet = parsed - .definitions - .iter() - .filter_map(|def| { - if let graphql_parser::query::Definition::Operation( - graphql_parser::query::OperationDefinition::Query(q), - ) = def - { - Some(&q.selection_set) - } else { - None - } - }) - .next() - .unwrap(); - - let selection: Selection<'_> = selection_set.into(); - - assert_eq!( - selection, - Selection(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "animal", - fields: Selection(vec![ - SelectionItem::Field(SelectionField { - alias: None, - name: "isCat", - fields: Selection(Vec::new()), - }), - SelectionItem::Field(SelectionField { - alias: None, - name: "isHorse", - fields: Selection(Vec::new()), - }), - SelectionItem::FragmentSpread(SelectionFragmentSpread { - fragment_name: "Timestamps", - }), - SelectionItem::Field(SelectionField { - alias: None, - name: "barks", - fields: Selection(Vec::new()), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "Dog", - fields: Selection(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "rating", - fields: Selection(Vec::new()), - })]), - }), - SelectionItem::Field(SelectionField { - alias: None, - name: "pawsCount", - fields: Selection(Vec::new()), - }), - SelectionItem::Field(SelectionField { - alias: Some("aliased"), - name: "sillyName", - fields: Selection(Vec::new()), - }), - ]), - })]) - ); - } -} diff --git a/graphql_client_codegen/src/shared.rs b/graphql_client_codegen/src/shared.rs deleted file mode 100644 index 62c940398..000000000 --- a/graphql_client_codegen/src/shared.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::deprecation::{DeprecationStatus, DeprecationStrategy}; -use crate::objects::GqlObjectField; -use crate::query::QueryContext; -use crate::selection::*; -use failure::*; -use heck::{CamelCase, SnakeCase}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; - -// List of keywords based on https://doc.rust-lang.org/grammar.html#keywords -const RUST_KEYWORDS: &[&str] = &[ - "abstract", - "alignof", - "as", - "async", - "await", - "become", - "box", - "break", - "const", - "continue", - "crate", - "do", - "else", - "enum", - "extern crate", - "extern", - "false", - "final", - "fn", - "for", - "for", - "if let", - "if", - "if", - "impl", - "impl", - "in", - "let", - "loop", - "macro", - "match", - "mod", - "move", - "mut", - "offsetof", - "override", - "priv", - "proc", - "pub", - "pure", - "ref", - "return", - "self", - "sizeof", - "static", - "struct", - "super", - "trait", - "true", - "type", - "typeof", - "unsafe", - "unsized", - "use", - "use", - "virtual", - "where", - "while", - "yield", -]; - -pub(crate) fn keyword_replace(needle: &str) -> String { - match RUST_KEYWORDS.binary_search(&needle) { - Ok(index) => [RUST_KEYWORDS[index], "_"].concat(), - Err(_) => needle.to_owned(), - } -} - -pub(crate) fn render_object_field( - field_name: &str, - field_type: &TokenStream, - description: Option<&str>, - status: &DeprecationStatus, - strategy: &DeprecationStrategy, -) -> Option { - #[allow(unused_assignments)] - let mut deprecation = quote!(); - match (status, strategy) { - // If the field is deprecated and we are denying usage, don't generate the - // field in rust at all and short-circuit. - (DeprecationStatus::Deprecated(_), DeprecationStrategy::Deny) => return None, - // Everything is allowed so there is nothing to do. - (_, DeprecationStrategy::Allow) => deprecation = quote!(), - // Current so there is nothing to do. - (DeprecationStatus::Current, _) => deprecation = quote!(), - // A reason was provided, translate it to a note. - (DeprecationStatus::Deprecated(Some(reason)), DeprecationStrategy::Warn) => { - deprecation = quote!(#[deprecated(note = #reason)]) - } - // No reason provided, just mark as deprecated. - (DeprecationStatus::Deprecated(None), DeprecationStrategy::Warn) => { - deprecation = quote!(#[deprecated]) - } - }; - - let description = description.map(|s| quote!(#[doc = #s])); - let rust_safe_field_name = keyword_replace(&field_name.to_snake_case()); - let name_ident = Ident::new(&rust_safe_field_name, Span::call_site()); - let rename = crate::shared::field_rename_annotation(&field_name, &rust_safe_field_name); - - Some(quote!(#description #deprecation #rename pub #name_ident: #field_type)) -} - -pub(crate) fn field_impls_for_selection( - fields: &[GqlObjectField<'_>], - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, -) -> Result, failure::Error> { - (&selection) - .into_iter() - .map(|selected| { - if let SelectionItem::Field(selected) = selected { - let name = &selected.name; - let alias = selected.alias.as_ref().unwrap_or(name); - - let ty = fields - .iter() - .find(|f| &f.name == name) - .ok_or_else(|| format_err!("could not find field `{}`", name))? - .type_ - .inner_name_str(); - let prefix = format!("{}{}", prefix.to_camel_case(), alias.to_camel_case()); - context.maybe_expand_field(&ty, &selected.fields, &prefix) - } else { - Ok(None) - } - }) - .filter_map(|i| i.transpose()) - .collect() -} - -pub(crate) fn response_fields_for_selection( - type_name: &str, - schema_fields: &[GqlObjectField<'_>], - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, -) -> Result, failure::Error> { - (&selection) - .into_iter() - .map(|item| match item { - SelectionItem::Field(f) => { - let name = &f.name; - let alias = f.alias.as_ref().unwrap_or(name); - - let schema_field = &schema_fields - .iter() - .find(|field| &field.name == name) - .ok_or_else(|| { - format_err!( - "Could not find field `{}` on `{}`. Available fields: `{}`.", - *name, - type_name, - schema_fields - .iter() - .map(|ref field| &field.name) - .fold(String::new(), |mut acc, item| { - acc.push_str(item); - acc.push_str(", "); - acc - }) - .trim_end_matches(", ") - ) - })?; - let ty = schema_field.type_.to_rust( - context, - &format!("{}{}", prefix.to_camel_case(), alias.to_camel_case()), - ); - - Ok(render_object_field( - alias, - &ty, - schema_field.description.as_ref().cloned(), - &schema_field.deprecation, - &context.deprecation_strategy, - )) - } - SelectionItem::FragmentSpread(fragment) => { - let field_name = - Ident::new(&fragment.fragment_name.to_snake_case(), Span::call_site()); - context.require_fragment(&fragment.fragment_name); - let fragment_from_context = context - .fragments - .get(&fragment.fragment_name) - .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment.fragment_name))?; - let type_name = Ident::new(&fragment.fragment_name, Span::call_site()); - let type_name = if fragment_from_context.is_recursive() { - quote!(Box<#type_name>) - } else { - quote!(#type_name) - }; - Ok(Some(quote! { - #[serde(flatten)] - pub #field_name: #type_name - })) - } - SelectionItem::InlineFragment(_) => Err(format_err!( - "unimplemented: inline fragment on object field" - )), - }) - .filter_map(|x| match x { - // Remove empty fields so callers always know a field has some - // tokens. - Ok(f) => f.map(Ok), - Err(err) => Some(Err(err)), - }) - .collect() -} - -/// Given the GraphQL schema name for an object/interface/input object field and -/// the equivalent rust name, produces a serde annotation to map them during -/// (de)serialization if it is necessary, otherwise an empty TokenStream. -pub(crate) fn field_rename_annotation(graphql_name: &str, rust_name: &str) -> Option { - if graphql_name != rust_name { - Some(quote!(#[serde(rename = #graphql_name)])) - } else { - None - } -} - -mod tests { - #[test] - fn keyword_replace() { - use super::keyword_replace; - assert_eq!("fora", keyword_replace("fora")); - assert_eq!("in_", keyword_replace("in")); - assert_eq!("fn_", keyword_replace("fn")); - assert_eq!("struct_", keyword_replace("struct")); - } -} diff --git a/graphql_client_codegen/src/tests/github.rs b/graphql_client_codegen/src/tests/github.rs deleted file mode 100644 index 7a72fa388..000000000 --- a/graphql_client_codegen/src/tests/github.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::schema::Schema; -use std::collections::HashSet; - -const SCHEMA_JSON: &str = include_str!("github_schema.json"); -const SCHEMA_GRAPHQL: &str = include_str!("github_schema.graphql"); - -#[test] -fn ast_from_graphql_and_json_produce_the_same_schema() { - use std::iter::FromIterator; - let json: graphql_introspection_query::introspection_response::IntrospectionResponse = - serde_json::from_str(SCHEMA_JSON).unwrap(); - let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL).unwrap(); - let json = Schema::from(&json); - let gql = Schema::from(&graphql_parser_schema); - - assert_eq!(json.scalars, gql.scalars); - for (json, gql) in json.objects.iter().zip(gql.objects.iter()) { - for (j, g) in json.1.fields.iter().zip(gql.1.fields.iter()) { - assert_eq!(j, g); - } - assert_eq!(json, gql) - } - for (json, gql) in json.unions.iter().zip(gql.unions.iter()) { - assert_eq!(json, gql) - } - for (json, gql) in json.interfaces.iter().zip(gql.interfaces.iter()) { - assert_eq!(json, gql) - } - assert_eq!(json.interfaces, gql.interfaces); - assert_eq!(json.query_type, gql.query_type); - assert_eq!(json.mutation_type, gql.mutation_type); - assert_eq!(json.subscription_type, gql.subscription_type); - for (json, gql) in json.inputs.iter().zip(gql.inputs.iter()) { - assert_eq!(json, gql); - } - assert_eq!(json.inputs, gql.inputs, "inputs differ"); - for ((json_name, json_value), (gql_name, gql_value)) in json.enums.iter().zip(gql.enums.iter()) - { - assert_eq!(json_name, gql_name); - assert_eq!( - HashSet::<&str>::from_iter(json_value.variants.iter().map(|v| v.name)), - HashSet::<&str>::from_iter(gql_value.variants.iter().map(|v| v.name)), - ); - } -} diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index 66728129d..1e24cfbb4 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -1,26 +1,22 @@ -mod github; - #[test] fn schema_with_keywords_works() { - use crate::{ - codegen, generated_module, schema::Schema, CodegenMode, GraphQLClientCodegenOptions, - }; - use graphql_parser; + use crate::{generated_module, schema::Schema, CodegenMode, GraphQLClientCodegenOptions}; let query_string = include_str!("keywords_query.graphql"); let query = graphql_parser::parse_query(query_string).expect("Parse keywords query"); let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql")) .expect("Parse keywords schema"); - let schema = Schema::from(&schema); + let schema = Schema::from(schema); let options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); - let operations = codegen::all_operations(&query); - for operation in &operations { + let query = crate::query::resolve(&schema, &query).unwrap(); + + for (_id, operation) in query.operations() { let generated_tokens = generated_module::GeneratedModule { query_string, schema: &schema, - query_document: &query, - operation, + operation: &operation.name, + resolved_query: &query, options: &options, } .to_token_stream() diff --git a/graphql_client_codegen/src/type_qualifiers.rs b/graphql_client_codegen/src/type_qualifiers.rs new file mode 100644 index 000000000..0803eb82e --- /dev/null +++ b/graphql_client_codegen/src/type_qualifiers.rs @@ -0,0 +1,19 @@ +#[derive(Clone, Debug, PartialEq, Hash)] +pub(crate) enum GraphqlTypeQualifier { + Required, + List, +} + +impl GraphqlTypeQualifier { + pub(crate) fn is_required(&self) -> bool { + *self == GraphqlTypeQualifier::Required + } +} + +pub fn graphql_parser_depth(schema_type: &graphql_parser::schema::Type) -> usize { + match schema_type { + graphql_parser::schema::Type::ListType(inner) => 1 + graphql_parser_depth(inner), + graphql_parser::schema::Type::NonNullType(inner) => 1 + graphql_parser_depth(inner), + graphql_parser::schema::Type::NamedType(_) => 0, + } +} diff --git a/graphql_client_codegen/src/unions.rs b/graphql_client_codegen/src/unions.rs deleted file mode 100644 index 784a8551b..000000000 --- a/graphql_client_codegen/src/unions.rs +++ /dev/null @@ -1,452 +0,0 @@ -use crate::query::QueryContext; -use crate::selection::Selection; -use failure::*; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; -use std::collections::BTreeSet; - -/// A GraphQL union (simplified schema representation). -/// -/// For code generation purposes, unions will "flatten" fragment spreads, so there is only one enum for the selection. See the tests in the graphql_client crate for examples. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct GqlUnion<'schema> { - pub name: &'schema str, - pub description: Option<&'schema str>, - pub variants: BTreeSet<&'schema str>, - pub is_required: Cell, -} - -#[derive(Debug, Fail)] -#[fail(display = "UnionError")] -enum UnionError { - #[fail(display = "Unknown type: {}", ty)] - UnknownType { ty: String }, - #[fail(display = "Unknown variant on union {}: {}", ty, var)] - UnknownVariant { var: String, ty: String }, - #[fail(display = "Missing __typename in selection for {}", union_name)] - MissingTypename { union_name: String }, -} - -type UnionVariantResult<'selection> = - Result<(Vec, Vec, Vec<&'selection str>), failure::Error>; - -/// Returns a triple. -/// -/// - The first element is the union variants to be inserted directly into the `enum` declaration. -/// - The second is the structs for each variant's sub-selection -/// - The last one contains which fields have been selected on the union, so we can make the enum exhaustive by complementing with those missing. -pub(crate) fn union_variants<'selection>( - selection: &'selection Selection<'_>, - context: &'selection QueryContext<'selection, 'selection>, - prefix: &str, - selection_on: &str, -) -> UnionVariantResult<'selection> { - let selection = selection.selected_variants_on_union(context, selection_on)?; - let mut used_variants: Vec<&str> = selection.keys().cloned().collect(); - let mut children_definitions = Vec::with_capacity(selection.len()); - let mut variants = Vec::with_capacity(selection.len()); - - for (on, fields) in selection.iter() { - let variant_name = Ident::new(&on, Span::call_site()); - used_variants.push(on); - - let new_prefix = format!("{}On{}", prefix, on); - - let variant_type = Ident::new(&new_prefix, Span::call_site()); - - let field_object_type = context - .schema - .objects - .get(on) - .map(|_f| context.maybe_expand_field(&on, fields, &new_prefix)); - let field_interface = context - .schema - .interfaces - .get(on) - .map(|_f| context.maybe_expand_field(&on, fields, &new_prefix)); - let field_union_type = context - .schema - .unions - .get(on) - .map(|_f| context.maybe_expand_field(&on, fields, &new_prefix)); - - match field_object_type.or(field_interface).or(field_union_type) { - Some(Ok(Some(tokens))) => children_definitions.push(tokens), - Some(Err(err)) => return Err(err), - Some(Ok(None)) => (), - None => { - return Err(UnionError::UnknownType { - ty: (*on).to_string(), - } - .into()) - } - }; - - variants.push(quote! { - #variant_name(#variant_type) - }) - } - - Ok((variants, children_definitions, used_variants)) -} - -impl<'schema> GqlUnion<'schema> { - /// Returns the code to deserialize this union in the response given the query selection. - pub(crate) fn response_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result { - let typename_field = selection.extract_typename(query_context); - - if typename_field.is_none() { - return Err(UnionError::MissingTypename { - union_name: prefix.into(), - } - .into()); - } - - let struct_name = Ident::new(prefix, Span::call_site()); - let derives = query_context.response_derives(); - - let (mut variants, children_definitions, used_variants) = - union_variants(selection, query_context, prefix, &self.name)?; - - for used_variant in used_variants.iter() { - if !self.variants.contains(used_variant) { - return Err(UnionError::UnknownVariant { - ty: self.name.into(), - var: (*used_variant).to_string(), - } - .into()); - } - } - - variants.extend( - self.variants - .iter() - .filter(|v| used_variants.iter().find(|a| a == v).is_none()) - .map(|v| { - let v = Ident::new(v, Span::call_site()); - quote!(#v) - }), - ); - - Ok(quote! { - #(#children_definitions)* - - #derives - #[serde(tag = "__typename")] - pub enum #struct_name { - #(#variants),* - } - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::*; - use crate::deprecation::DeprecationStatus; - use crate::field_type::FieldType; - use crate::objects::{GqlObject, GqlObjectField}; - use crate::selection::*; - - #[test] - fn union_response_for_selection_complains_if_typename_is_missing() { - let fields = vec![ - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "User", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "firstName", - fields: Selection::new_empty(), - })]), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "Organization", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "title", - fields: Selection::new_empty(), - })]), - }), - ]; - let selection = Selection::from_vec(fields); - let prefix = "Meow"; - let union = GqlUnion { - name: "MyUnion", - description: None, - variants: BTreeSet::new(), - is_required: false.into(), - }; - - let mut schema = crate::schema::Schema::new(); - - schema.objects.insert( - "User", - GqlObject { - description: None, - name: "User", - fields: vec![ - GqlObjectField { - description: None, - name: "firstName", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "lastName", - type_: FieldType::new("String").nonnull(), - - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "createdAt", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - - schema.objects.insert( - "Organization", - GqlObject { - description: None, - name: "Organization", - fields: vec![ - GqlObjectField { - description: None, - name: "title", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "created_at", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - let context = QueryContext::new_empty(&schema); - - let result = union.response_for_selection(&context, &selection, &prefix); - - assert!(result.is_err()); - - assert_eq!( - format!("{}", result.unwrap_err()), - "Missing __typename in selection for Meow" - ); - } - - #[test] - fn union_response_for_selection_works() { - let fields = vec![ - SelectionItem::Field(SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "User", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "firstName", - fields: Selection::new_empty(), - })]), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "Organization", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "title", - fields: Selection::new_empty(), - })]), - }), - ]; - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - let selection: Selection<'_> = fields.into_iter().collect(); - let prefix = "Meow"; - let mut union_variants = BTreeSet::new(); - union_variants.insert("User"); - union_variants.insert("Organization"); - let union = GqlUnion { - name: "MyUnion", - description: None, - variants: union_variants, - is_required: false.into(), - }; - - let result = union.response_for_selection(&context, &selection, &prefix); - - assert!(result.is_err()); - - let mut schema = crate::schema::Schema::new(); - schema.objects.insert( - "User", - GqlObject { - description: None, - name: "User", - fields: vec![ - GqlObjectField { - description: None, - name: "__typename", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "firstName", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "lastName", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "createdAt", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - - schema.objects.insert( - "Organization", - GqlObject { - description: None, - name: "Organization", - fields: vec![ - GqlObjectField { - description: None, - name: "__typename", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "title", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "createdAt", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - - let context = QueryContext::new_empty(&schema); - - let result = union.response_for_selection(&context, &selection, &prefix); - - println!("{:?}", result); - - assert!(result.is_ok()); - - assert_eq!( - result.unwrap().to_string(), - vec![ - "# [ derive ( Deserialize ) ] ", - "pub struct MeowOnOrganization { pub title : String , } ", - "# [ derive ( Deserialize ) ] ", - "pub struct MeowOnUser { # [ serde ( rename = \"firstName\" ) ] pub first_name : String , } ", - "# [ derive ( Deserialize ) ] ", - "# [ serde ( tag = \"__typename\" ) ] ", - "pub enum Meow { Organization ( MeowOnOrganization ) , User ( MeowOnUser ) }", - ].into_iter() - .collect::(), - ); - } - - #[test] - fn union_rejects_selection_on_non_member_type() { - let fields = vec![ - SelectionItem::Field(SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "SomeNonUnionType", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "field", - fields: Selection::new_empty(), - })]), - }), - ]; - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - let selection: Selection<'_> = fields.into_iter().collect(); - let prefix = "Meow"; - let mut union_variants = BTreeSet::new(); - union_variants.insert("Int"); - union_variants.insert("String"); - let union = GqlUnion { - name: "MyUnion", - description: None, - variants: union_variants, - is_required: false.into(), - }; - - let result = union.response_for_selection(&context, &selection, &prefix); - - assert!(result.is_err()); - - let mut schema = crate::schema::Schema::new(); - schema.unions.insert("MyUnion", union.clone()); - schema.objects.insert( - "SomeNonUnionType", - GqlObject { - description: None, - name: "SomeNonUnionType", - fields: vec![GqlObjectField { - description: None, - name: "field", - type_: FieldType::new(string_type()), - deprecation: DeprecationStatus::Current, - }], - is_required: false.into(), - }, - ); - - let context = QueryContext::new_empty(&schema); - - let result = union.response_for_selection(&context, &selection, &prefix); - - println!("{:?}", result); - - assert!(result.is_err()); - - match result.unwrap_err().downcast::() { - Ok(UnionError::UnknownVariant { var, ty }) => { - assert_eq!(var, "SomeNonUnionType"); - assert_eq!(ty, "MyUnion"); - } - err => panic!("Unexpected error type: {:?}", err), - } - } -} diff --git a/graphql_client_codegen/src/variables.rs b/graphql_client_codegen/src/variables.rs deleted file mode 100644 index 7865f6e28..000000000 --- a/graphql_client_codegen/src/variables.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::field_type::FieldType; -use crate::query::QueryContext; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::collections::BTreeMap; - -#[derive(Debug, Clone)] -pub struct Variable<'query> { - pub name: &'query str, - pub ty: FieldType<'query>, - pub default: Option<&'query graphql_parser::query::Value>, -} - -impl<'query> Variable<'query> { - pub(crate) fn generate_default_value_constructor( - &self, - context: &QueryContext<'_, '_>, - ) -> Option { - context.schema.require(&self.ty.inner_name_str()); - match &self.default { - Some(default) => { - let fn_name = Ident::new(&format!("default_{}", self.name), Span::call_site()); - let ty = self.ty.to_rust(context, ""); - let value = graphql_parser_value_to_literal( - default, - context, - &self.ty, - self.ty.is_optional(), - ); - Some(quote! { - pub fn #fn_name() -> #ty { - #value - } - - }) - } - None => None, - } - } -} - -impl<'query> std::convert::From<&'query graphql_parser::query::VariableDefinition> - for Variable<'query> -{ - fn from(def: &'query graphql_parser::query::VariableDefinition) -> Variable<'query> { - Variable { - name: &def.name, - ty: FieldType::from(&def.var_type), - default: def.default_value.as_ref(), - } - } -} - -fn graphql_parser_value_to_literal( - value: &graphql_parser::query::Value, - context: &QueryContext<'_, '_>, - ty: &FieldType<'_>, - is_optional: bool, -) -> TokenStream { - use graphql_parser::query::Value; - - let inner = match value { - Value::Boolean(b) => { - if *b { - quote!(true) - } else { - quote!(false) - } - } - Value::String(s) => quote!(#s.to_string()), - Value::Variable(_) => panic!("variable in variable"), - Value::Null => panic!("null as default value"), - Value::Float(f) => quote!(#f), - Value::Int(i) => { - let i = i.as_i64(); - quote!(#i) - } - Value::Enum(en) => quote!(#en), - Value::List(inner) => { - let elements = inner - .iter() - .map(|val| graphql_parser_value_to_literal(val, context, ty, false)); - quote! { - vec![ - #(#elements,)* - ] - } - } - Value::Object(obj) => render_object_literal(obj, ty, context), - }; - - if is_optional { - quote!(Some(#inner)) - } else { - inner - } -} - -fn render_object_literal( - object: &BTreeMap, - ty: &FieldType<'_>, - context: &QueryContext<'_, '_>, -) -> TokenStream { - let type_name = ty.inner_name_str(); - let constructor = Ident::new(&type_name, Span::call_site()); - let schema_type = context - .schema - .inputs - .get(type_name) - .expect("unknown input type"); - let fields: Vec = schema_type - .fields - .iter() - .map(|(name, field)| { - let field_name = Ident::new(&name, Span::call_site()); - let provided_value = object.get(name.to_owned()); - match provided_value { - Some(default_value) => { - let value = graphql_parser_value_to_literal( - default_value, - context, - &field.type_, - field.type_.is_optional(), - ); - quote!(#field_name: #value) - } - None => quote!(#field_name: None), - } - }) - .collect(); - - quote!(#constructor { - #(#fields,)* - }) -} diff --git a/graphql_client_web/.gitignore b/graphql_client_web/.gitignore deleted file mode 100644 index f32d710ca..000000000 --- a/graphql_client_web/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/wasm-pack.log -/bin diff --git a/graphql_query_derive/.gitignore b/graphql_query_derive/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/graphql_query_derive/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index 2084fa45b..634622ab9 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -11,7 +11,7 @@ fn path_to_match() -> syn::Path { } /// Extract an configuration parameter specified in the `graphql` attribute. -pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result { +pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result { let attributes = &ast.attrs; let graphql_path = path_to_match(); let attribute = attributes @@ -37,7 +37,9 @@ pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result { } /// Get the deprecation from a struct attribute in the derive case. -pub fn extract_deprecation_strategy(ast: &syn::DeriveInput) -> Result { +pub fn extract_deprecation_strategy( + ast: &syn::DeriveInput, +) -> Result { extract_attr(&ast, "deprecated")? .to_lowercase() .as_str() @@ -46,7 +48,7 @@ pub fn extract_deprecation_strategy(ast: &syn::DeriveInput) -> Result Result { +pub fn extract_normalization(ast: &syn::DeriveInput) -> Result { extract_attr(&ast, "normalization")? .to_lowercase() .as_str() diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index 2d3bdae01..d9374b84f 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -23,13 +23,13 @@ fn graphql_query_derive_inner( input: proc_macro::TokenStream, ) -> Result { let input = TokenStream::from(input); - let ast = syn::parse2(input).context("Derive input parsing.")?; + let ast = syn::parse2(input).expect("derive input parsing"); + // .context("Derive input parsing.")?; let (query_path, schema_path) = build_query_and_schema_path(&ast)?; - let options = build_graphql_client_derive_options(&ast, query_path.to_path_buf())?; + let options = build_graphql_client_derive_options(&ast, query_path.clone())?; Ok( generate_module_token_stream(query_path, &schema_path, options) .map(Into::into) - .map_err(|fail| fail.compat()) .context("Code generation failed.")?, ) }