diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f6f2f8f..81f6304ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,8 @@ jobs: override: true - name: Execute cargo test run: cargo test --all --tests --examples - wasm_test: - name: Cargo test for wasm + wasm_build: + name: Cargo build for wasm runs-on: ubuntu-18.04 steps: - name: Checkout sources @@ -41,14 +41,10 @@ jobs: profile: minimal toolchain: stable override: true - - name: Setup wasm-bindgen + target: wasm32-unknown-unknown + - name: Execute cargo build run: | - rustup target add wasm32-unknown-unknown && - sudo apt update && sudo apt install -y firefox-geckodriver && - cargo install wasm-bindgen-cli - - name: Execute cargo test - run: | - xvfb-run cargo test --manifest-path=./graphql_client/Cargo.toml --features="web" --target wasm32-unknown-unknown + cargo build --manifest-path=./graphql_client/Cargo.toml --features="reqwest" --target wasm32-unknown-unknown lint: name: Rustfmt and Clippy runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index ec9050f79..f207ee2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Cargo.lock .idea scripts/* !scripts/*.sh +/.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index aa406739b..ead47b3d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased -- [to be documnented] — A CLI and derive option to specify a module to import - custom scalar from. Thanks @miterst! ([PR](https://github.com/graphql-rust/graphql-client/pull/354)) +- The `web` feature is dropped. You can now use a `reqwest::Client` instead of the custom HTTP client. +- Allow specifying externally defined enums (thanks @jakmeier) +- Make the derive feature optional (but enabled by default) +- `--no-ssl` param in CLI (thanks @danielharbor!) +- The shape of some generated response types changed to be flatter and more ergonomic. +- Many dependencies were dropped +— A CLI and derive option to specify a module to import custom scalar from. + Thanks @miterst! ([PR](https://github.com/graphql-rust/graphql-client/pull/354)) ## 0.9.0 - 2020-03-13 diff --git a/Cargo.toml b/Cargo.toml index fff800a54..413f34383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [workspace] +resolver = "2" members = [ "graphql_client", "graphql_client_cli", "graphql_client_codegen", - "graphql_client_web", "graphql-introspection-query", "graphql_query_derive", diff --git a/README.md b/README.md index 3948ec884..9ebbd4df6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Github actions Status](https://github.com/graphql-rust/graphql-client/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/graphql-rust/graphql-client/actions) [![docs](https://docs.rs/graphql_client/badge.svg)](https://docs.rs/graphql_client/latest/graphql_client/) [![crates.io](https://img.shields.io/crates/v/graphql_client.svg)](https://crates.io/crates/graphql_client) -[![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/juniper-graphql/graphql-client) A typed GraphQL client library for Rust. @@ -19,7 +18,7 @@ A typed GraphQL client library for Rust. - Supports multiple operations per query document. - Supports setting GraphQL fields as deprecated and having the Rust compiler check their use. -- [web client](./graphql_client_web) for boilerplate-free API calls from browsers. +- Optional reqwest-based client for boilerplate-free API calls from browsers. ## Getting started @@ -84,6 +83,15 @@ A typed GraphQL client library for Rust. [A complete example using the GitHub GraphQL API is available](https://github.com/graphql-rust/graphql-client/tree/master/examples/github), as well as sample [rustdoc output](https://www.tomhoule.com/docs/example_module/). +## Alternative workflow using the CLI + +You can introspect GraphQL APIs and generate module from a command line interface to the library: + +```bash +$ cargo install graphql_client_cli +$ graphql-client --help +``` + ## Deriving specific traits on the response The generated response types always derive `serde::Deserialize` but you may want to print them (`Debug`), compare them (`PartialEq`) or derive any other trait on it. You can achieve this with the `response_derives` option of the `graphql` attribute. Example: diff --git a/examples/github/Cargo.toml b/examples/github/Cargo.toml index 2a7fb394b..1093c4de2 100644 --- a/examples/github/Cargo.toml +++ b/examples/github/Cargo.toml @@ -6,12 +6,10 @@ edition = "2018" [dev-dependencies] anyhow = "1.0" -graphql_client = { path = "../../graphql_client" } +graphql_client = { path = "../../graphql_client", features = ["reqwest-blocking"] } serde = "^1.0" -reqwest = { version = "^0.10", features = ["json", "blocking"] } +reqwest = { version = "^0.11", features = ["json", "blocking"] } prettytable-rs = "^0.7" structopt = "^0.3" -dotenv = "^0.13" -envy = "^0.3" log = "^0.4" env_logger = "^0.5" diff --git a/examples/github/examples/github.rs b/examples/github/examples/github.rs index 833ee757b..96076a18b 100644 --- a/examples/github/examples/github.rs +++ b/examples/github/examples/github.rs @@ -1,10 +1,11 @@ +use ::reqwest::blocking::Client; use anyhow::*; -use graphql_client::*; +use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; use log::*; use prettytable::*; -use serde::*; use structopt::StructOpt; +#[allow(clippy::upper_case_acronyms)] type URI = String; #[derive(GraphQLQuery)] @@ -22,11 +23,6 @@ struct Command { repo: String, } -#[derive(Deserialize, Debug)] -struct Env { - github_api_token: String, -} - fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), anyhow::Error> { let mut parts = repo_name.split('/'); match (parts.next(), parts.next()) { @@ -36,34 +32,36 @@ fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), anyhow::Error> { } fn main() -> Result<(), anyhow::Error> { - dotenv::dotenv().ok(); env_logger::init(); - let config: Env = envy::from_env().context("while reading from environment")?; + let github_api_token = + std::env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN env var"); let args = Command::from_args(); let repo = args.repo; let (owner, name) = parse_repo_name(&repo).unwrap_or(("tomhoule", "graphql-client")); - let q = RepoView::build_query(repo_view::Variables { + let variables = repo_view::Variables { owner: owner.to_string(), name: name.to_string(), - }); + }; - let client = reqwest::blocking::Client::builder() + let client = Client::builder() .user_agent("graphql-rust/0.9.0") + .default_headers( + std::iter::once(( + reqwest::header::AUTHORIZATION, + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", github_api_token)) + .unwrap(), + )) + .collect(), + ) .build()?; - let res = client - .post("https://api.github.com/graphql") - .bearer_auth(config.github_api_token) - .json(&q) - .send()?; - - res.error_for_status_ref()?; + let response_body = + post_graphql::(&client, "https://api.github.com/graphql", variables).unwrap(); - let response_body: Response = res.json()?; info!("{:?}", response_body); let response_data: repo_view::ResponseData = response_body.data.expect("missing response data"); @@ -79,16 +77,16 @@ fn main() -> Result<(), anyhow::Error> { table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); table.set_titles(row!(b => "issue", "comments")); - for issue in &response_data + for issue in response_data .repository .expect("missing repository") .issues .nodes .expect("issue nodes is null") + .iter() + .flatten() { - if let Some(issue) = issue { - table.add_row(row!(issue.title, issue.comments.total_count)); - } + table.add_row(row!(issue.title, issue.comments.total_count)); } table.printstd(); diff --git a/examples/hasura/Cargo.toml b/examples/hasura/Cargo.toml index 7307f5a7b..56ca334ce 100644 --- a/examples/hasura/Cargo.toml +++ b/examples/hasura/Cargo.toml @@ -6,12 +6,10 @@ edition = "2018" [dev-dependencies] anyhow = "1.0" -graphql_client = { path = "../../graphql_client" } -serde = "1.0" -serde_derive = "1.0" +graphql_client = { path = "../../graphql_client", features = ["reqwest-blocking"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -reqwest = { version = "^0.10", features = ["json", "blocking"] } +reqwest = { version = "^0.11", features = ["json", "blocking"] } prettytable-rs = "0.7.0" -dotenv = "0.13.0" log = "0.4.3" env_logger = "0.5.10" diff --git a/examples/hasura/examples/hasura.rs b/examples/hasura/examples/hasura.rs index e2bf7a4b1..0fe9c4672 100644 --- a/examples/hasura/examples/hasura.rs +++ b/examples/hasura/examples/hasura.rs @@ -1,4 +1,5 @@ -use graphql_client::*; +use ::reqwest::blocking::Client; +use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; use log::*; use prettytable::*; @@ -16,10 +17,9 @@ struct UpsertIssue; fn main() -> Result<(), anyhow::Error> { use upsert_issue::{IssuesUpdateColumn::*, *}; - dotenv::dotenv().ok(); env_logger::init(); - let q = UpsertIssue::build_query(Variables { + let v = Variables { issues: vec![IssuesInsertInput { id: Some("001000000000000".to_string()), name: Some("Name".to_string()), @@ -27,16 +27,12 @@ fn main() -> Result<(), anyhow::Error> { salesforce_updated_at: Some("2019-06-11T08:14:28Z".to_string()), }], update_columns: vec![Name, Status, SalesforceUpdatedAt], - }); + }; - let client = reqwest::blocking::Client::new(); + let client = Client::new(); - let res = client - .post("https://localhost:8080/v1/graphql") - .json(&q) - .send()?; - - let response_body: Response = res.json()?; + let response_body = + post_graphql::(&client, "https://localhost:8080/v1/graphql", v)?; info!("{:?}", response_body); if let Some(errors) = response_body.errors { diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index a6ebeb72e..76a0a292a 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -4,21 +4,19 @@ version = "0.1.0" authors = ["Tom Houlé "] edition = "2018" -# https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#profile-overrides -#[profile.release] -#lto = "thin" +[lib] +crate-type = ["cdylib", "rlib"] -[dev-dependencies] -graphql_client = { path = "../../graphql_client" } -graphql_client_web = { path = "../../graphql_client_web" } +[dependencies] +graphql_client = { path = "../../graphql_client", features = ["reqwest"] } wasm-bindgen = "^0.2" serde = { version = "1.0.67", features = ["derive"] } lazy_static = "1.0.1" js-sys = "0.3.6" -futures-util = "0.3.8" wasm-bindgen-futures = "0.4.18" +reqwest = "0.11.3" -[dev-dependencies.web-sys] +[dependencies.web-sys] version = "0.3.6" features = [ "console", @@ -29,8 +27,5 @@ features = [ "HtmlBodyElement", "HtmlDocument", "HtmlElement", + "Window", ] - -[[example]] -name = "web" -crate-type = ["cdylib"] diff --git a/examples/web/examples/web.rs b/examples/web/src/lib.rs similarity index 80% rename from examples/web/examples/web.rs rename to examples/web/src/lib.rs index f27c6f059..2e9b0be2c 100644 --- a/examples/web/examples/web.rs +++ b/examples/web/src/lib.rs @@ -1,4 +1,4 @@ -use graphql_client::GraphQLQuery; +use graphql_client::{reqwest::post_graphql, GraphQLQuery}; use lazy_static::*; use std::cell::RefCell; use std::sync::Mutex; @@ -9,7 +9,7 @@ use wasm_bindgen_futures::future_to_promise; #[derive(GraphQLQuery)] #[graphql( schema_path = "schema.json", - query_path = "examples/puppy_smiles.graphql", + query_path = "src/puppy_smiles.graphql", response_derives = "Debug" )] struct PuppySmiles; @@ -23,20 +23,22 @@ lazy_static! { } async fn load_more() -> Result { - let client = graphql_client::web::Client::new("https://www.graphqlhub.com/graphql"); + let url = "https://www.graphqlhub.com/graphql"; let variables = puppy_smiles::Variables { after: LAST_ENTRY .lock() .ok() .and_then(|opt| opt.borrow().to_owned()), }; - let response = client.call(PuppySmiles, variables).await.map_err(|err| { - log(&format!( - "Could not fetch puppies. graphql_client_web error: {:?}", - err - )); - JsValue::NULL - })?; + + let client = reqwest::Client::new(); + + let response = post_graphql::(&client, url, variables) + .await + .map_err(|err| { + log(&format!("Could not fetch puppies. error: {:?}", err)); + JsValue::NULL + })?; render_response(response); Ok(JsValue::NULL) } @@ -72,14 +74,14 @@ fn add_load_more_button() { on_click.forget(); } -fn render_response(response: graphql_client_web::Response) { +fn render_response(response: graphql_client::Response) { use std::fmt::Write; log(&format!("response body\n\n{:?}", response)); let parent = document().body().expect_throw("no body"); - let json: graphql_client_web::Response = response; + let json: graphql_client::Response = response; let response = document() .create_element("div") .expect_throw("could not create div"); @@ -98,11 +100,10 @@ fn render_response(response: graphql_client_web::Response {}
@@ -110,10 +111,9 @@ fn render_response(response: graphql_client_web::Response
"#, - puppy.title, puppy.url, puppy.title - ) - .expect_throw("write to string"); - } + puppy.title, puppy.url, puppy.title + ) + .expect_throw("write to string"); } response.set_inner_html(&format!( "

response:

{}
", diff --git a/examples/web/examples/puppy_smiles.graphql b/examples/web/src/puppy_smiles.graphql similarity index 100% rename from examples/web/examples/puppy_smiles.graphql rename to examples/web/src/puppy_smiles.graphql diff --git a/graphql-introspection-query/src/introspection_response.rs b/graphql-introspection-query/src/introspection_response.rs index d3daa1814..2ed287b87 100644 --- a/graphql-introspection-query/src/introspection_response.rs +++ b/graphql-introspection-query/src/introspection_response.rs @@ -286,7 +286,7 @@ impl IntrospectionResponse { pub fn as_schema(&self) -> &SchemaContainer { match self { IntrospectionResponse::FullResponse(full_response) => &full_response.data, - IntrospectionResponse::Schema(schema) => &schema, + IntrospectionResponse::Schema(schema) => schema, } } diff --git a/graphql_client/Cargo.toml b/graphql_client/Cargo.toml index c3fae68ee..0b610739e 100644 --- a/graphql_client/Cargo.toml +++ b/graphql_client/Cargo.toml @@ -10,47 +10,13 @@ categories = ["network-programming", "web-programming", "wasm"] edition = "2018" [dependencies] -doc-comment = "^0.3" -anyhow = { version = "1.0", optional = true } -thiserror = { version = "1.0", optional = true } -graphql_query_derive = { path = "../graphql_query_derive", version = "0.9.0", optional = true } -serde_json = "1.0" serde = { version = "^1.0.78", features = ["derive"] } +serde_json = "1.0" -[dependencies.js-sys] -version = "^0.3" -optional = true - -[dependencies.log] -version = "^0.4" -optional = true - -[dependencies.web-sys] -version = "^0.3" -optional = true -features = ["Headers", "Request", "RequestInit", "Response", "Window"] - -[dependencies.wasm-bindgen] -version = "^0.2" -optional = true - -[dependencies.wasm-bindgen-futures] -version = "^0.4" -optional = true - -[dev-dependencies] -# Note: If we bumpup wasm-bindge-test version, we should change CI setting. -wasm-bindgen-test = "^0.3" -reqwest = { version = "^0.10", features = ["json", "blocking"] } +# Optional dependencies +graphql_query_derive = { path = "../graphql_query_derive", version = "0.9.0", optional = true } +reqwest = { version = "^0.11", features = ["json"], optional = true } [features] default = ["graphql_query_derive"] -web = [ - "anyhow", - "thiserror", - "js-sys", - "log", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] +reqwest-blocking = ["reqwest/blocking"] diff --git a/graphql_client/src/lib.rs b/graphql_client/src/lib.rs index f737b079e..17f1e170d 100644 --- a/graphql_client/src/lib.rs +++ b/graphql_client/src/lib.rs @@ -1,6 +1,15 @@ -//! The top-level documentation resides on the [project README](https://github.com/graphql-rust/graphql-client) at the moment. +//! The top-level documentation resides on the [project +//! README](https://github.com/graphql-rust/graphql-client) at the moment. //! -//! The main interface to this library is the custom derive that generates modules from a GraphQL query and schema. See the docs for the [`GraphQLQuery`] trait for a full example. +//! The main interface to this library is the custom derive that generates +//! modules from a GraphQL query and schema. See the docs for the +//! [`GraphQLQuery`] trait for a full example. +//! +//! ## Cargo features +//! +//! - `graphql_query_derive` (default: on): enables the `#[derive(GraphqlQuery)]` custom derive. +//! - `reqwest` (default: off): exposes the `graphql_client::reqwest::post_graphql()` function. +//! - `reqwest-blocking` (default: off): exposes the blocking version, `graphql_client::reqwest::post_graphql_blocking()`. #![deny(missing_docs)] #![warn(rust_2018_idioms)] @@ -14,16 +23,13 @@ extern crate graphql_query_derive; #[doc(hidden)] pub use graphql_query_derive::*; -use serde::*; - -#[cfg(feature = "web")] -pub mod web; +#[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] +pub mod reqwest; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{self, Display}; -doc_comment::doctest!("../../README.md"); - /// A convenience trait that can be used to build a GraphQL request body. /// /// This will be implemented for you by codegen in the normal case. It is implemented on the struct you place the derive on. diff --git a/graphql_client/src/reqwest.rs b/graphql_client/src/reqwest.rs new file mode 100644 index 000000000..6fecf9ba6 --- /dev/null +++ b/graphql_client/src/reqwest.rs @@ -0,0 +1,29 @@ +//! A concrete client implementation over HTTP with reqwest. + +use crate::GraphQLQuery; + +/// Use the provided reqwest::Client to post a GraphQL request. +#[cfg(feature = "reqwest")] +pub async fn post_graphql( + client: &reqwest::Client, + url: U, + variables: Q::Variables, +) -> Result, reqwest::Error> { + let body = Q::build_query(variables); + let reqwest_response = client.post(url).json(&body).send().await?; + + Ok(reqwest_response.json().await?) +} + +/// Use the provided reqwest::Client to post a GraphQL request. +#[cfg(feature = "reqwest-blocking")] +pub fn post_graphql_blocking( + client: &reqwest::blocking::Client, + url: U, + variables: Q::Variables, +) -> Result, reqwest::Error> { + let body = Q::build_query(variables); + let reqwest_response = client.post(url).json(&body).send()?; + + reqwest_response.json() +} diff --git a/graphql_client/src/web.rs b/graphql_client/src/web.rs deleted file mode 100644 index 7242c7331..000000000 --- a/graphql_client/src/web.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Use graphql_client inside browsers with -//! [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen). - -use crate::*; -use log::*; -use std::collections::HashMap; -use thiserror::*; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; - -/// The main interface to the library. -/// -/// The workflow is the following: -/// -/// - create a client -/// - (optionally) configure it -/// - use it to perform queries with the [call] method -pub struct Client { - endpoint: String, - headers: HashMap, -} - -/// All the ways a request can go wrong. -/// -/// not exhaustive -#[derive(Debug, Error, PartialEq)] -pub enum ClientError { - /// The body couldn't be built - #[error("Request body is not a valid string")] - Body, - /// An error caused by window.fetch - #[error("Network error")] - Network(String), - /// Error in a dynamic JS cast that should have worked - #[error("JS casting error")] - Cast, - /// No window object could be retrieved - #[error( - "No Window object available - the client works only in a browser (non-worker) context" - )] - NoWindow, - /// Response shape does not match the generated code - #[error("Response shape error")] - ResponseShape, - /// Response could not be converted to text - #[error("Response conversion to text failed (Response.text threw)")] - ResponseText, - /// Exception thrown when building the request - #[error("Error building the request")] - RequestError, - /// Other JS exception - #[error("Unexpected JS exception")] - JsException, -} - -impl Client { - /// Initialize a client. The `endpoint` parameter is the URI of the GraphQL API. - pub fn new(endpoint: Endpoint) -> Client - where - Endpoint: Into, - { - Client { - endpoint: endpoint.into(), - headers: HashMap::new(), - } - } - - /// Add a header to those sent with the requests. Can be used for things like authorization. - pub fn add_header(&mut self, name: &str, value: &str) { - self.headers.insert(name.into(), value.into()); - } - - /// Perform a query. - /// - // Lint disabled: We can pass by value because it's always an empty struct. - #[allow(clippy::needless_pass_by_value)] - pub async fn call( - &self, - _query: Q, - variables: Q::Variables, - ) -> Result, ClientError> { - let window = web_sys::window().ok_or(ClientError::NoWindow)?; - let body = - serde_json::to_string(&Q::build_query(variables)).map_err(|_| ClientError::Body)?; - - let mut request_init = web_sys::RequestInit::new(); - request_init - .method("POST") - .body(Some(&JsValue::from_str(&body))); - - let request = web_sys::Request::new_with_str_and_init(&self.endpoint, &request_init) - .map_err(|_| ClientError::JsException)?; - - let headers = request.headers(); - headers - .set("Content-Type", "application/json") - .map_err(|_| ClientError::RequestError)?; - headers - .set("Accept", "application/json") - .map_err(|_| ClientError::RequestError)?; - for (header_name, header_value) in self.headers.iter() { - headers - .set(header_name, header_value) - .map_err(|_| ClientError::RequestError)?; - } - - let res = JsFuture::from(window.fetch_with_request(&request)) - .await - .map_err(|err| ClientError::Network(js_sys::Error::from(err).message().into()))?; - debug!("response: {:?}", res); - let cast_response = res - .dyn_into::() - .map_err(|_| ClientError::Cast)?; - - let text_promise = cast_response - .text() - .map_err(|_| ClientError::ResponseText)?; - let text = JsFuture::from(text_promise) - .await - .map_err(|_| ClientError::ResponseText)?; - - let response_text = text.as_string().unwrap_or_default(); - debug!("response text as string: {:?}", response_text); - let response_data = - serde_json::from_str(&response_text).map_err(|_| ClientError::ResponseShape)?; - Ok(response_data) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn client_new() { - Client::new("https://example.com/graphql"); - Client::new("/graphql"); - } -} diff --git a/graphql_client/tests/web.rs b/graphql_client/tests/web.rs deleted file mode 100644 index 6cb57b868..000000000 --- a/graphql_client/tests/web.rs +++ /dev/null @@ -1,88 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use graphql_client::{web::Client, GraphQLQuery}; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn build_client() { - // just to test it doesn't crash - Client::new("https://example.com/graphql"); - Client::new("/graphql"); -} - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "tests/countries_schema.json", - query_path = "tests/Germany.graphql", - response_derives = "Debug" -)] -struct Germany; - -#[wasm_bindgen_test] -async fn test_germany() { - let response = Client::new("https://countries.trevorblades.com/") - .call(Germany, germany::Variables) - .await - .expect("successful response"); - let continent_name = response - .data - .expect("response data is not null") - .country - .expect("country is not null") - .continent - .expect("continent is not null") - .name - .expect("germany is on a continent"); - assert_eq!(continent_name, "Europe"); -} - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "tests/countries_schema.json", - query_path = "tests/Germany.graphql" -)] -struct Country; - -#[wasm_bindgen_test] -async fn test_country() { - let response = Client::new("https://countries.trevorblades.com/") - .call( - Country, - country::Variables { - country_code: "CN".to_owned(), - }, - ) - .await - .expect("successful response"); - let continent_name = response - .data - .expect("response data is not null") - .country - .expect("country is not null") - .continent - .expect("continent is not null") - .name - .expect("country is on a continent"); - assert_eq!(continent_name, "Asia"); -} - -#[wasm_bindgen_test] -async fn test_bad_url() { - let result = Client::new("https://example.com/non-existent/graphql/endpoint") - .call( - Country, - country::Variables { - country_code: "CN".to_owned(), - }, - ) - .await; - match result { - Ok(_response) => panic!("The API endpoint does not exist, this should not be called."), - Err(graphql_client::web::ClientError::Network(msg)) => { - assert_eq!(msg, "NetworkError when attempting to fetch resource.") - } - Err(err) => panic!("unexpected error: {}", err), - } -} diff --git a/graphql_client_cli/Cargo.toml b/graphql_client_cli/Cargo.toml index 5a42871bd..575fe39e0 100644 --- a/graphql_client_cli/Cargo.toml +++ b/graphql_client_cli/Cargo.toml @@ -12,19 +12,15 @@ name = "graphql-client" path = "src/main.rs" [dependencies] -anyhow = "1.0" -reqwest = { version = "^0.10", features = ["json", "blocking"] } -graphql_client = { version = "0.9.0", path = "../graphql_client", features = [] } +reqwest = { version = "^0.11", features = ["json", "blocking"] } +graphql_client = { version = "0.9.0", path = "../graphql_client", default-features = false, features = ["graphql_query_derive", "reqwest-blocking"] } graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.9.0" } structopt = "0.3" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" -syn = "^1.0" log = "^0.4" env_logger = "^0.6" - -rustfmt-nightly = { version = "1.4.5", optional = true } +syn = "1.0" [features] default = [] -rustfmt = ["rustfmt-nightly"] diff --git a/graphql_client_cli/README.md b/graphql_client_cli/README.md index 1843c789e..370772d7a 100644 --- a/graphql_client_cli/README.md +++ b/graphql_client_cli/README.md @@ -40,7 +40,7 @@ USAGE: FLAGS: -h, --help Prints help information --no-formatting If you don't want to execute rustfmt to generated code, set this option. Default value is - false. Formating feature is disabled as default installation. + false. -V, --version Prints version information OPTIONS: @@ -66,5 +66,5 @@ ARGS: If you want to use formatting feature, you should install like this. ```bash -cargo install graphql_client_cli --features rustfmt --force +cargo install graphql_client_cli ``` diff --git a/graphql_client_cli/src/error.rs b/graphql_client_cli/src/error.rs new file mode 100644 index 000000000..feb682c2f --- /dev/null +++ b/graphql_client_cli/src/error.rs @@ -0,0 +1,65 @@ +use std::fmt::{Debug, Display}; + +pub struct Error { + source: Option>, + message: Option, + location: &'static std::panic::Location<'static>, +} + +impl Error { + #[track_caller] + pub fn message(msg: String) -> Self { + Error { + source: None, + message: Some(msg), + location: std::panic::Location::caller(), + } + } + + #[track_caller] + pub fn source_with_message( + source: impl std::error::Error + Send + Sync + 'static, + message: String, + ) -> Self { + let mut err = Error::message(message); + err.source = Some(Box::new(source)); + err + } +} + +// This is the impl that shows up when the error bubbles up to `main()`. +impl Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(msg) = &self.message { + f.write_str(msg)?; + f.write_str("\n")?; + } + + if self.source.is_some() && self.message.is_some() { + f.write_str("Cause: ")?; + } + + if let Some(source) = self.source.as_ref() { + Display::fmt(source, f)?; + } + + f.write_str("\nLocation: ")?; + Display::fmt(self.location, f)?; + + Ok(()) + } +} + +impl From for Error +where + T: std::error::Error + Send + Sync + 'static, +{ + #[track_caller] + fn from(err: T) -> Self { + Error { + message: None, + source: Some(Box::new(err)), + location: std::panic::Location::caller(), + } + } +} diff --git a/graphql_client_cli/src/generate.rs b/graphql_client_cli/src/generate.rs index dd4e2ebd2..b767ba020 100644 --- a/graphql_client_cli/src/generate.rs +++ b/graphql_client_cli/src/generate.rs @@ -1,10 +1,13 @@ -use anyhow::*; +use crate::error::Error; +use crate::CliResult; use graphql_client_codegen::{ generate_module_token_stream, CodegenMode, GraphQLClientCodegenOptions, }; +use std::ffi::OsString; use std::fs::File; use std::io::Write as _; use std::path::PathBuf; +use std::process::Stdio; use syn::Token; pub(crate) struct CliCodegenParams { @@ -20,7 +23,7 @@ pub(crate) struct CliCodegenParams { pub custom_scalars_module: Option, } -pub(crate) fn generate_code(params: CliCodegenParams) -> Result<()> { +pub(crate) fn generate_code(params: CliCodegenParams) -> CliResult<()> { let CliCodegenParams { variables_derives, response_derives, @@ -62,59 +65,66 @@ pub(crate) fn generate_code(params: CliCodegenParams) -> Result<()> { } if let Some(custom_scalars_module) = custom_scalars_module { - let custom_scalars_module = syn::parse_str(&custom_scalars_module).with_context(|| { - format!( - "Invalid custom scalars module path: {}", - custom_scalars_module - ) - })?; + let custom_scalars_module = syn::parse_str(&custom_scalars_module) + .map_err(|_| Error::message("Invalid custom scalar module path".to_owned()))?; options.set_custom_scalars_module(custom_scalars_module); } - let gen = generate_module_token_stream(query_path.clone(), &schema_path, options).unwrap(); + let gen = generate_module_token_stream(query_path.clone(), &schema_path, options) + .map_err(|err| Error::message(format!("Error generating module code: {}", err)))?; let generated_code = gen.to_string(); - let generated_code = if cfg!(feature = "rustfmt") && !no_formatting { - format(&generated_code) + let generated_code = if !no_formatting { + format(&generated_code)? } else { generated_code }; - let query_file_name: ::std::ffi::OsString = query_path - .file_name() - .map(ToOwned::to_owned) - .ok_or_else(|| format_err!("Failed to find a file name in the provided query path."))?; + let query_file_name: OsString = + query_path + .file_name() + .map(ToOwned::to_owned) + .ok_or_else(|| { + Error::message("Failed to find a file name in the provided query path.".to_owned()) + })?; let dest_file_path: PathBuf = output_directory .map(|output_dir| output_dir.join(query_file_name).with_extension("rs")) .unwrap_or_else(move || query_path.with_extension("rs")); - let mut file = File::create(dest_file_path)?; + log::info!("Writing generated query to {:?}", dest_file_path); + + let mut file = File::create(&dest_file_path).map_err(|err| { + Error::source_with_message( + err, + format!("Creating file at {}", dest_file_path.display()), + ) + })?; write!(file, "{}", generated_code)?; Ok(()) } -#[allow(unused_variables)] -fn format(codes: &str) -> String { - #[cfg(feature = "rustfmt")] - { - use rustfmt::{Config, Input, Session}; - - let mut config = Config::default(); +fn format(code: &str) -> CliResult { + let binary = "rustfmt"; - config.set().emit_mode(rustfmt_nightly::EmitMode::Stdout); - config.set().verbose(rustfmt_nightly::Verbosity::Quiet); + let mut child = std::process::Command::new(binary) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(|err| Error::source_with_message(err, "Error spawning rustfmt".to_owned()))?; + let child_stdin = child.stdin.as_mut().unwrap(); + write!(child_stdin, "{}", code)?; - let mut out = Vec::with_capacity(codes.len() * 2); + let output = child.wait_with_output()?; - Session::new(config, Some(&mut out)) - .format(Input::Text(codes.to_string())) - .unwrap_or_else(|err| panic!("rustfmt error: {}", err)); - - return String::from_utf8(out).unwrap(); + if !output.status.success() { + panic!( + "rustfmt error\n\n{}", + String::from_utf8_lossy(&output.stderr) + ); } - #[cfg(not(feature = "rustfmt"))] - unreachable!("called format() without the rustfmt feature") + + Ok(String::from_utf8(output.stdout)?) } diff --git a/graphql_client_cli/src/introspect_schema.rs b/graphql_client_cli/src/introspect_schema.rs index cf2b6d360..0791ee8a1 100644 --- a/graphql_client_cli/src/introspect_schema.rs +++ b/graphql_client_cli/src/introspect_schema.rs @@ -1,4 +1,4 @@ -use anyhow::*; +use crate::CliResult; use graphql_client::GraphQLQuery; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE}; use std::path::PathBuf; @@ -20,7 +20,7 @@ pub fn introspect_schema( authorization: Option, headers: Vec
, no_ssl: bool, -) -> anyhow::Result<()> { +) -> CliResult<()> { use std::io::Write; let out: Box = match output { @@ -60,6 +60,7 @@ pub fn introspect_schema( let json: serde_json::Value = res.json()?; serde_json::to_writer_pretty(out, &json)?; + Ok(()) } @@ -77,12 +78,12 @@ pub struct Header { } impl FromStr for Header { - type Err = anyhow::Error; + type Err = String; fn from_str(input: &str) -> Result { // error: colon required for name/value pair if !input.contains(':') { - return Err(format_err!( + return Err(format!( "Invalid header input. A colon is required to separate the name and value. [{}]", input )); @@ -95,7 +96,7 @@ impl FromStr for Header { // error: field name must be if name.is_empty() { - return Err(format_err!( + return Err(format!( "Invalid header input. Field name is required before colon. [{}]", input )); @@ -103,7 +104,7 @@ impl FromStr for Header { // error: no whitespace in field name if name.split_whitespace().count() > 1 { - return Err(format_err!( + return Err(format!( "Invalid header input. Whitespace not allowed in field name. [{}]", input )); diff --git a/graphql_client_cli/src/main.rs b/graphql_client_cli/src/main.rs index ab1f94ef0..82d9dc1df 100644 --- a/graphql_client_cli/src/main.rs +++ b/graphql_client_cli/src/main.rs @@ -1,14 +1,17 @@ -use env_logger::fmt::{Color, Style, StyledValue}; -use log::Level; - -#[cfg(feature = "rustfmt")] -extern crate rustfmt_nightly as rustfmt; +#![allow(clippy::redundant_clone)] // in structopt generated code +mod error; mod generate; mod introspect_schema; + +use env_logger::fmt::{Color, Style, StyledValue}; +use error::Error; +use log::Level; use std::path::PathBuf; use structopt::StructOpt; +type CliResult = Result; + #[derive(StructOpt)] #[structopt(author, about)] enum Cli { @@ -57,7 +60,6 @@ enum Cli { deprecation_strategy: Option, /// If you don't want to execute rustfmt to generated code, set this option. /// Default value is false. - /// Formating feature is disabled as default installation. #[structopt(long = "no-formatting")] no_formatting: bool, /// You can choose module and target struct visibility from pub and private. @@ -77,7 +79,7 @@ enum Cli { }, } -fn main() -> anyhow::Result<()> { +fn main() -> CliResult<()> { set_env_logger(); let cli = Cli::from_args(); @@ -107,15 +109,15 @@ fn main() -> anyhow::Result<()> { selected_operation, custom_scalars_module, } => generate::generate_code(generate::CliCodegenParams { + query_path, + schema_path, + selected_operation, variables_derives, response_derives, deprecation_strategy, - module_visibility, no_formatting, + module_visibility, output_directory, - query_path, - schema_path, - selected_operation, custom_scalars_module, }), } diff --git a/graphql_client_codegen/Cargo.toml b/graphql_client_codegen/Cargo.toml index 2f25b8352..512c8c932 100644 --- a/graphql_client_codegen/Cargo.toml +++ b/graphql_client_codegen/Cargo.toml @@ -17,4 +17,3 @@ 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 88bffd20d..34587e699 100644 --- a/graphql_client_codegen/src/codegen.rs +++ b/graphql_client_codegen/src/codegen.rs @@ -298,7 +298,7 @@ fn render_object_literal( .fields .iter() .map(|(name, r#type)| { - let field_name = Ident::new(&name, Span::call_site()); + let field_name = Ident::new(name, Span::call_site()); let provided_value = object_map.get(name); match provided_value { Some(default_value) => { diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs index 1a6d1bea3..8e96b31c1 100644 --- a/graphql_client_codegen/src/codegen/selection.rs +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -168,7 +168,7 @@ fn calculate_selection<'a>( .iter() .map(|id| (id, context.query.query.get_selection(*id))) .filter_map(|(id, selection)| { - VariantSelection::from_selection(&selection, type_id, context.query) + VariantSelection::from_selection(selection, type_id, context.query) .map(|variant_selection| (*id, selection, variant_selection)) }) .collect(); @@ -188,7 +188,7 @@ fn calculate_selection<'a>( if let Some((selection_id, selection, _variant)) = variant_selections.get(0) { let mut variant_struct_name_str = - full_path_prefix(*selection_id, &context.query); + 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); @@ -258,7 +258,7 @@ fn calculate_selection<'a>( match selection { Selection::Field(field) => { - let (graphql_name, rust_name) = context.field_name(&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; @@ -295,7 +295,7 @@ fn calculate_selection<'a>( }); } TypeId::Object(_) | TypeId::Interface(_) | TypeId::Union(_) => { - let struct_name_string = full_path_prefix(*id, &context.query); + let struct_name_string = full_path_prefix(*id, context.query); context.push_field(ExpandedField { struct_id, @@ -436,7 +436,7 @@ 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()); + let ident = Ident::new(variant_type, Span::call_site()); quote!((#ident)) }); @@ -507,7 +507,7 @@ impl<'a> ExpandedSelection<'a> { // 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 = Ident::new(alias.name, Span::call_site()); let fragment_name = if alias.boxed { quote!(Box<#fragment_name>) } else { diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index 04b9cec18..36c41a117 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -6,17 +6,23 @@ use crate::{ use heck::*; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use thiserror::Error; +use std::{error::Error, fmt::Display}; -#[derive(Debug, Error)] -#[error( - "Could not find an operation named {} in the query document.", - operation_name -)] +#[derive(Debug)] struct OperationNotFound { operation_name: String, } +impl Display for OperationNotFound { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Could not find an operation named ")?; + f.write_str(&self.operation_name)?; + f.write_str(" in the query document.") + } +} + +impl Error for OperationNotFound {} + /// This struct contains the parameters necessary to generate code for a given operation. pub(crate) struct GeneratedModule<'a> { pub operation: &'a str, @@ -31,7 +37,7 @@ impl<'a> GeneratedModule<'a> { fn build_impls(&self) -> Result { Ok(crate::codegen::response_for_query( self.root()?, - &self.options, + self.options, BoundQuery { query: self.resolved_query, schema: self.schema, diff --git a/graphql_client_codegen/src/lib.rs b/graphql_client_codegen/src/lib.rs index 6cf869943..7037eaaba 100644 --- a/graphql_client_codegen/src/lib.rs +++ b/graphql_client_codegen/src/lib.rs @@ -29,13 +29,19 @@ mod tests; pub use crate::codegen_options::{CodegenMode, GraphQLClientCodegenOptions}; -use std::{collections::HashMap, io}; -use thiserror::Error; +use std::{collections::HashMap, fmt::Display, io}; -#[derive(Debug, Error)] -#[error("{0}")] +#[derive(Debug)] struct GeneralError(String); +impl Display for GeneralError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for GeneralError {} + type BoxError = Box; type CacheMap = std::sync::Mutex>; @@ -137,24 +143,34 @@ pub fn generate_module_token_stream( Ok(modules) } -#[derive(Debug, Error)] +#[derive(Debug)] enum ReadFileError { - #[error( - "Could not find file with path: {}\ - Hint: file paths in the GraphQLQuery attribute are relative to the project root (location of the Cargo.toml). Example: query_path = \"src/my_query.graphql\".", - path - )] - FileNotFound { - path: String, - #[source] - io_error: io::Error, - }, - #[error("Error reading file at: {}", path)] - ReadError { - path: String, - #[source] - io_error: io::Error, - }, + FileNotFound { path: String, io_error: io::Error }, + ReadError { path: String, io_error: io::Error }, +} + +impl Display for ReadFileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReadFileError::FileNotFound { path, .. } => { + write!(f, "Could not find file with path: {}\n + Hint: file paths in the GraphQLQuery attribute are relative to the project root (location of the Cargo.toml). Example: query_path = \"src/my_query.graphql\".", path) + } + ReadFileError::ReadError { path, .. } => { + f.write_str("Error reading file at: ")?; + f.write_str(path) + } + } + } +} + +impl std::error::Error for ReadFileError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ReadFileError::FileNotFound { io_error, .. } + | ReadFileError::ReadError { io_error, .. } => Some(io_error), + } + } } fn read_file(path: &std::path::Path) -> Result { diff --git a/graphql_client_codegen/src/query.rs b/graphql_client_codegen/src/query.rs index e20b74f81..d8ff35bb3 100644 --- a/graphql_client_codegen/src/query.rs +++ b/graphql_client_codegen/src/query.rs @@ -18,15 +18,24 @@ use crate::{ StoredInputType, StoredScalar, TypeId, UnionId, }, }; -use std::collections::{HashMap, HashSet}; -use thiserror::Error; +use std::{ + collections::{HashMap, HashSet}, + fmt::Display, +}; -#[derive(Debug, Error)] -#[error("{}", message)] +#[derive(Debug)] pub(crate) struct QueryValidationError { message: String, } +impl Display for QueryValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for QueryValidationError {} + impl QueryValidationError { pub(crate) fn new(message: String) -> Self { QueryValidationError { message } @@ -188,7 +197,7 @@ fn resolve_fragment( fragment_definition: &graphql_parser::query::FragmentDefinition, ) -> Result<(), QueryValidationError> { let graphql_parser::query::TypeCondition::On(on) = &fragment_definition.type_condition; - let on = schema.find_type(&on).ok_or_else(|| { + let on = schema.find_type(on).ok_or_else(|| { QueryValidationError::new(format!( "Could not find type `{}` referenced by fragment `{}`", on, fragment_definition.name diff --git a/graphql_client_codegen/src/query/selection.rs b/graphql_client_codegen/src/query/selection.rs index bf3ba30be..a241c1b85 100644 --- a/graphql_client_codegen/src/query/selection.rs +++ b/graphql_client_codegen/src/query/selection.rs @@ -131,13 +131,13 @@ impl SelectionParent { } } - pub(crate) fn to_path_segment(&self, query: &BoundQuery<'_>) -> String { + 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) + 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(), + SelectionParent::Operation(id) => query.query.get_operation(id).to_path_segment(), + SelectionParent::Fragment(id) => query.query.get_fragment(id).to_path_segment(), } } } diff --git a/graphql_client_web/Cargo.toml b/graphql_client_web/Cargo.toml deleted file mode 100644 index a2ae4bf8a..000000000 --- a/graphql_client_web/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "graphql_client_web" -version = "0.9.0" -authors = ["Tom Houlé "] -edition = "2018" -description = "Typed GraphQL requests and responses (web integration)" -license = "Apache-2.0 OR MIT" -keywords = ["graphql", "api", "web", "webassembly", "wasm"] -categories = ["network-programming", "web-programming", "wasm"] -repository = "https://github.com/graphql-rust/graphql-client" - -[dependencies.graphql_client] -version = "0.9.0" -path = "../graphql_client" -features = ["web"] - -[dev-dependencies] -serde = { version = "^1.0", features = ["derive"] } -wasm-bindgen-test = "^0.3" diff --git a/graphql_client_web/README.md b/graphql_client_web/README.md deleted file mode 100644 index 6b1b601a0..000000000 --- a/graphql_client_web/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# graphql_client_web - -Make boilerplate-free GraphQL API calls from web browsers using [graphql-client](../README.md) and [wasm-bindgen](https://github.com/alexcrichton/wasm-bindgen). - -For usage details, see the [API docs](https://docs.rs/graphql_client_web/latest), the [example](../examples/web) and the [tests](../graphql_client/tests/web.rs). diff --git a/graphql_client_web/src/lib.rs b/graphql_client_web/src/lib.rs deleted file mode 100644 index 02a066d73..000000000 --- a/graphql_client_web/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![deprecated( - note = "graphql_client_web is deprecated. The web client is now part of the graphql_client crate, with the \"web\" feature." -)] - -pub use graphql_client::web::*; -pub use graphql_client::{self, *}; diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index 7003d8222..ff6ccb3f1 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -1,4 +1,3 @@ -use crate::{BoxError, GeneralError}; use graphql_client_codegen::deprecation::DeprecationStrategy; use graphql_client_codegen::normalization::Normalization; @@ -33,7 +32,10 @@ pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result Result Result { - extract_attr(&ast, "deprecated")? +) -> Result { + extract_attr(ast, "deprecated")? .to_lowercase() .as_str() .parse() - .map_err(|_| GeneralError(DEPRECATION_ERROR.to_owned()).into()) + .map_err(|_| syn::Error::new_spanned(ast, DEPRECATION_ERROR.to_owned())) } /// Get the deprecation from a struct attribute in the derive case. -pub fn extract_normalization(ast: &syn::DeriveInput) -> Result { - extract_attr(&ast, "normalization")? +pub fn extract_normalization(ast: &syn::DeriveInput) -> Result { + extract_attr(ast, "normalization")? .to_lowercase() .as_str() .parse() - .map_err(|_| GeneralError(NORMALIZATION_ERROR.to_owned()).into()) + .map_err(|_| syn::Error::new_spanned(ast, NORMALIZATION_ERROR)) } #[cfg(test)] diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index aec49d58d..56de45bf1 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -8,58 +8,49 @@ use graphql_client_codegen::{ }; use std::{ env, - fmt::Display, path::{Path, PathBuf}, }; use proc_macro2::TokenStream; -type BoxError = Box; - -#[derive(Debug)] -struct GeneralError(String); - -impl Display for GeneralError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } -} - -impl std::error::Error for GeneralError {} - #[proc_macro_derive(GraphQLQuery, attributes(graphql))] pub fn derive_graphql_query(input: proc_macro::TokenStream) -> proc_macro::TokenStream { match graphql_query_derive_inner(input) { Ok(ts) => ts, - Err(err) => panic!("{:?}", err), + Err(err) => err.to_compile_error().into(), } } fn graphql_query_derive_inner( input: proc_macro::TokenStream, -) -> Result { +) -> Result { let input = TokenStream::from(input); let ast = syn::parse2(input)?; let (query_path, schema_path) = build_query_and_schema_path(&ast)?; 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(|err| GeneralError(format!("Code generation failed: {}", err)))?, - ) + + generate_module_token_stream(query_path, &schema_path, options) + .map(Into::into) + .map_err(|err| { + syn::Error::new_spanned( + ast, + format!("Failed to generate GraphQLQuery impl: {}", err), + ) + }) } -fn build_query_and_schema_path(input: &syn::DeriveInput) -> Result<(PathBuf, PathBuf), BoxError> { +fn build_query_and_schema_path(input: &syn::DeriveInput) -> Result<(PathBuf, PathBuf), syn::Error> { let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_err| { - GeneralError("Checking that the CARGO_MANIFEST_DIR env variable is defined.".into()) + syn::Error::new_spanned( + input, + "Error checking that the CARGO_MANIFEST_DIR env variable is defined.", + ) })?; - let query_path = attributes::extract_attr(input, "query_path") - .map_err(|err| GeneralError(format!("Error extracting query path. {}", err)))?; + let query_path = attributes::extract_attr(input, "query_path")?; let query_path = format!("{}/{}", cargo_manifest_dir, query_path); let query_path = Path::new(&query_path).to_path_buf(); - let schema_path = attributes::extract_attr(input, "schema_path") - .map_err(|err| GeneralError(format!("Error extracting schema path. {}", err)))?; + let schema_path = attributes::extract_attr(input, "schema_path")?; let schema_path = Path::new(&cargo_manifest_dir).join(schema_path); Ok((query_path, schema_path)) } @@ -67,7 +58,7 @@ fn build_query_and_schema_path(input: &syn::DeriveInput) -> Result<(PathBuf, Pat fn build_graphql_client_derive_options( input: &syn::DeriveInput, query_path: PathBuf, -) -> Result { +) -> Result { let variables_derives = attributes::extract_attr(input, "variables_derives").ok(); let response_derives = attributes::extract_attr(input, "response_derives").ok(); let custom_scalars_module = attributes::extract_attr(input, "custom_scalars_module").ok(); @@ -96,12 +87,7 @@ fn build_graphql_client_derive_options( // The user can give a path to a module that provides definitions for the custom scalars. if let Some(custom_scalars_module) = custom_scalars_module { - let custom_scalars_module = syn::parse_str(&custom_scalars_module).map_err(|err| { - GeneralError(format!( - "Invalid custom scalars module path: {}. {}", - custom_scalars_module, err - )) - })?; + let custom_scalars_module = syn::parse_str(&custom_scalars_module)?; options.set_custom_scalars_module(custom_scalars_module); }