gurkle_codegen/
lib.rs

1#![deny(missing_docs)]
2#![warn(rust_2018_idioms)]
3#![allow(clippy::option_option)]
4
5//! Crate for internal use by other graphql-client crates, for code generation.
6//!
7//! It is not meant to be used directly by users of the library.
8
9use gurkle_parser::schema::parse_schema;
10use introspection_response::IntrospectionResponse;
11use proc_macro2::TokenStream;
12use quote::*;
13use schema::Schema;
14
15mod codegen;
16mod codegen_options;
17/// Deprecation-related code
18pub mod deprecation;
19/// Contains the [Schema] type and its implementation.
20pub mod schema;
21
22mod constants;
23mod generated_module;
24mod introspection_response;
25/// Normalization-related code
26pub mod normalization;
27mod query;
28mod type_qualifiers;
29
30#[cfg(test)]
31mod tests;
32
33pub use crate::codegen_options::GraphQLClientCodegenOptions;
34
35use std::{io, path::Path};
36use thiserror::Error;
37
38#[derive(Debug, Error)]
39#[error("{0}")]
40struct GeneralError(String);
41
42type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
43
44/// Generates Rust code given a query document, a schema and options.
45pub fn generate_module_token_stream(
46    query_path: Vec<std::path::PathBuf>,
47    schema_path: &Path,
48    options: GraphQLClientCodegenOptions,
49) -> Result<TokenStream, BoxError> {
50    let schema_extension = schema_path
51        .extension()
52        .and_then(std::ffi::OsStr::to_str)
53        .unwrap_or("INVALID");
54
55    // Check the schema cache.
56    let schema: Schema = {
57        let schema_string = read_file(schema_path)?;
58        let schema = match schema_extension {
59            "graphql" | "gql" => {
60                let s = parse_schema(&schema_string).map_err(|parser_error| GeneralError(format!("Parser error: {}", parser_error)))?;
61                Schema::from(s)
62            }
63            "json" => {
64                let parsed: IntrospectionResponse = serde_json::from_str(&schema_string)?;
65                Schema::from(parsed)
66            }
67            extension => return Err(GeneralError(format!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension)).into())
68        };
69        schema
70    };
71
72    // Load and concatenative all the query files.
73    let query_string = query_path
74        .iter()
75        .map(|x| read_file(x))
76        .collect::<Result<Vec<_>, _>>()?
77        .join("\n\n");
78
79    // We need to qualify the query with the path to the crate it is part of
80    let query = gurkle_parser::parse_query(&query_string)
81        .map_err(|err| GeneralError(format!("Query parser error: {}", err)))?;
82
83    let query = crate::query::resolve(&schema, &query)?;
84
85    // Determine which operation we are generating code for. This will be used in operationName.
86    let operations = options
87        .operation_name
88        .as_ref()
89        .and_then(|operation_name| query.select_operation(operation_name, *options.normalization()))
90        .map(|op| vec![op]);
91
92    let operations = match operations {
93        Some(ops) => ops,
94        None => query.operations().collect(),
95    };
96
97    // The generated modules.
98    let mut modules = Vec::with_capacity(operations.len());
99
100    for operation in &operations {
101        let generated = generated_module::GeneratedModule {
102            // query_string: format!("{}", operation.query),
103            schema: &schema,
104            resolved_query: &query,
105            operation: &operation.1.name,
106            options: &options,
107        }
108        .to_token_stream()?;
109        modules.push(generated);
110    }
111
112    let modules = quote! { #(#modules)* };
113
114    Ok(modules)
115}
116
117#[derive(Debug, Error)]
118enum ReadFileError {
119    #[error(
120        "Could not find file with path: {}\
121        Hint: file paths in the GraphQLRequest attribute are relative to the project root (location of the Cargo.toml). Example: query_path = \"src/my_query.graphql\".",
122        path
123    )]
124    FileNotFound {
125        path: String,
126        #[source]
127        io_error: io::Error,
128    },
129    #[error("Error reading file at: {}", path)]
130    ReadError {
131        path: String,
132        #[source]
133        io_error: io::Error,
134    },
135}
136
137fn read_file(path: &Path) -> Result<String, ReadFileError> {
138    use std::fs;
139    use std::io::prelude::*;
140
141    let mut out = String::new();
142    let mut file = fs::File::open(path).map_err(|io_error| ReadFileError::FileNotFound {
143        io_error,
144        path: path.display().to_string(),
145    })?;
146
147    file.read_to_string(&mut out)
148        .map_err(|io_error| ReadFileError::ReadError {
149            io_error,
150            path: path.display().to_string(),
151        })?;
152    Ok(out)
153}