1#![deny(missing_docs)]
2#![warn(rust_2018_idioms)]
3#![allow(clippy::option_option)]
4
5use gurkle_parser::schema::parse_schema;
10use introspection_response::IntrospectionResponse;
11use proc_macro2::TokenStream;
12use quote::*;
13use schema::Schema;
14
15mod codegen;
16mod codegen_options;
17pub mod deprecation;
19pub mod schema;
21
22mod constants;
23mod generated_module;
24mod introspection_response;
25pub 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
44pub 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 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 let query_string = query_path
74 .iter()
75 .map(|x| read_file(x))
76 .collect::<Result<Vec<_>, _>>()?
77 .join("\n\n");
78
79 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 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 let mut modules = Vec::with_capacity(operations.len());
99
100 for operation in &operations {
101 let generated = generated_module::GeneratedModule {
102 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}