1use crate::{
2 constants::*,
3 deprecation::DeprecationStatus,
4 field_type::FieldType,
5 query::QueryContext,
6 schema::Schema,
7 selection::*,
8 shared::{
9 field_impls_for_selection, response_fields_for_selection,
10 typescript_definitions_for_selection, typescript_fields_for_selection
11 },
12 CodegenError
13};
14use graphql_parser::schema;
15use proc_macro2::{Ident, Span, TokenStream};
16use quote::quote;
17use std::{cell::Cell, collections::HashSet};
18
19#[derive(Debug, Clone, PartialEq)]
20pub struct GqlObject<'schema> {
21 pub description: Option<&'schema str>,
22 pub fields: Vec<GqlObjectField<'schema>>,
23 pub name: &'schema str,
24 pub is_required: Cell<bool>
25}
26
27#[derive(Clone, Debug, PartialEq)]
28pub struct GqlObjectField<'schema> {
29 pub description: Option<&'schema str>,
30 pub name: &'schema str,
31 pub type_: FieldType<'schema>,
32 pub deprecation: DeprecationStatus
33}
34
35fn parse_deprecation_info(field: &schema::Field) -> DeprecationStatus {
36 let deprecated = field
37 .directives
38 .iter()
39 .find(|x| x.name.to_lowercase() == "deprecated");
40 let reason = if let Some(d) = deprecated {
41 if let Some((_, value)) = d.arguments.iter().find(|x| x.0.to_lowercase() == "reason") {
42 match value {
43 schema::Value::String(reason) => Some(reason.clone()),
44 schema::Value::Null => None,
45 _ => panic!("deprecation reason is not a string")
46 }
47 } else {
48 None
49 }
50 } else {
51 None
52 };
53 match deprecated {
54 Some(_) => DeprecationStatus::Deprecated(reason),
55 None => DeprecationStatus::Current
56 }
57}
58
59impl<'schema> GqlObject<'schema> {
60 pub fn new(name: &'schema str, description: Option<&'schema str>) -> GqlObject<'schema> {
61 GqlObject {
62 description,
63 name,
64 fields: vec![typename_field()],
65 is_required: false.into()
66 }
67 }
68
69 pub fn from_graphql_parser_object(obj: &'schema schema::ObjectType) -> Self {
70 let description = obj.description.as_deref();
71 let mut item = GqlObject::new(&obj.name, description);
72 item.fields.extend(obj.fields.iter().map(|f| {
73 let deprecation = parse_deprecation_info(&f);
74 GqlObjectField {
75 description: f.description.as_deref(),
76 name: &f.name,
77 type_: FieldType::from(&f.field_type),
78 deprecation
79 }
80 }));
81 item
82 }
83
84 pub fn from_introspected_schema_json(
85 obj: &'schema crate::introspection_response::FullType
86 ) -> Self {
87 let description = obj.description.as_deref();
88 let mut item = GqlObject::new(obj.name.as_ref().expect("missing object name"), description);
89 let fields = obj.fields.as_ref().unwrap().iter().filter_map(|t| {
90 t.as_ref().map(|t| {
91 let deprecation = if t.is_deprecated.unwrap_or(false) {
92 DeprecationStatus::Deprecated(t.deprecation_reason.clone())
93 } else {
94 DeprecationStatus::Current
95 };
96 GqlObjectField {
97 description: t.description.as_deref(),
98 name: t.name.as_ref().expect("field name"),
99 type_: FieldType::from(t.type_.as_ref().expect("field type")),
100 deprecation
101 }
102 })
103 });
104
105 item.fields.extend(fields);
106
107 item
108 }
109
110 pub(crate) fn require(&self, schema: &Schema<'_>) {
111 if self.is_required.get() {
112 return;
113 }
114 self.is_required.set(true);
115 self.fields.iter().for_each(|field| {
116 schema.require(&field.type_.inner_name_str());
117 })
118 }
119
120 pub(crate) fn typescript_for_selection(
121 &self,
122 query_context: &QueryContext<'_, '_>,
123 selection: &Selection<'_>,
124 prefix: &str,
125 include_typename: bool
126 ) -> Result<String, CodegenError> {
127 let fields = self.typescript_fields_for_selection(query_context, selection, prefix)?;
128 let field_impls = self.typescript_definitions_for_selection(query_context, selection)?;
129 let description = self
130 .description
131 .as_ref()
132 .map(|desc| format!("/** {} */", desc))
133 .unwrap_or_else(|| format!(""));
134 let typename = if include_typename {
135 format!("__typename: \"{}\",\n", prefix)
136 } else {
137 format!("")
138 };
139
140 let field_impls = if !field_impls.is_empty() {
141 format!(
142 r#"
143 export namespace {name} {{
144 {field_impls}
145 }}
146 "#,
147 name = prefix,
148 field_impls = field_impls.join(",\n")
149 )
150 } else {
151 format!("")
152 };
153
154 let tokens = format!(
155 r#"
156 {field_impls}
157
158 {description}
159 export interface {name} {{
160 {typename}{fields}
161 }}
162 "#,
163 field_impls = field_impls,
164 description = description,
165 name = prefix,
166 typename = typename,
167 fields = fields.join(",\n")
168 );
169
170 Ok(tokens)
171 }
172
173 pub(crate) fn response_for_selection(
174 &self,
175 query_context: &QueryContext<'_, '_>,
176 selection: &Selection<'_>,
177 prefix: &str
178 ) -> Result<(TokenStream, HashSet<String>), CodegenError> {
179 let derives = query_context.response_derives();
180 let wasm_derives = if query_context.wasm_bindgen {
181 let filtered: Vec<_> = vec!["Serialize"]
182 .into_iter()
183 .map(|def| syn::Ident::new(def, Span::call_site()))
184 .filter(|def| !query_context.response_derives.contains(def))
185 .collect();
186 if !filtered.is_empty() {
187 quote!(#[cfg_attr(target_arch = "wasm32", derive(#(#filtered),*))])
188 } else {
189 quote!()
190 }
191 } else {
192 quote!()
193 };
194 let name = Ident::new(prefix, Span::call_site());
195 let (field_infos, fields) =
196 self.response_fields_for_selection(query_context, selection, prefix)?;
197 let (field_impls, types) =
198 self.field_impls_for_selection(query_context, selection, &prefix)?;
199 let description = self.description.as_ref().map(|desc| quote!(#[doc = #desc]));
200
201 let tokens = quote! {
202 #(#field_impls)*
203
204 #derives
205 #wasm_derives
206 #description
207 pub struct #name {
208 #(#fields,)*
209 }
210
211 impl #name {
212 #[allow(unused_variables)]
213 fn selection(variables: &Variables) -> Vec<::artemis::codegen::FieldSelector> {
214 vec![
215 #(#field_infos),*
216 ]
217 }
218 }
219 };
220 Ok((tokens, types))
221 }
222
223 pub(crate) fn field_impls_for_selection(
224 &self,
225 query_context: &QueryContext<'_, '_>,
226 selection: &Selection<'_>,
227 prefix: &str
228 ) -> Result<(Vec<TokenStream>, HashSet<String>), CodegenError> {
229 field_impls_for_selection(&self.fields, query_context, selection, prefix)
230 }
231
232 pub(crate) fn typescript_definitions_for_selection(
233 &self,
234 query_context: &QueryContext<'_, '_>,
235 selection: &Selection<'_>
236 ) -> Result<Vec<String>, CodegenError> {
237 typescript_definitions_for_selection(&self.fields, query_context, selection)
238 }
239
240 pub(crate) fn typescript_fields_for_selection(
241 &self,
242 query_context: &QueryContext<'_, '_>,
243 selection: &Selection<'_>,
244 prefix: &str
245 ) -> Result<Vec<String>, CodegenError> {
246 typescript_fields_for_selection(&self.name, &self.fields, query_context, selection, prefix)
247 }
248
249 pub(crate) fn response_fields_for_selection(
250 &self,
251 query_context: &QueryContext<'_, '_>,
252 selection: &Selection<'_>,
253 prefix: &str
254 ) -> Result<(Vec<TokenStream>, Vec<TokenStream>), CodegenError> {
255 response_fields_for_selection(&self.name, &self.fields, query_context, selection, prefix)
256 }
257}
258
259#[cfg(test)]
260mod test {
261 use super::*;
262 use graphql_parser::{query, Pos};
263
264 fn mock_field(directives: Vec<schema::Directive>) -> schema::Field {
265 schema::Field {
266 position: Pos::default(),
267 description: None,
268 name: "foo".to_string(),
269 arguments: vec![],
270 field_type: schema::Type::NamedType("x".to_string()),
271 directives
272 }
273 }
274
275 #[test]
276 fn deprecation_no_reason() {
277 let directive = schema::Directive {
278 position: Pos::default(),
279 name: "deprecated".to_string(),
280 arguments: vec![]
281 };
282 let result = parse_deprecation_info(&mock_field(vec![directive]));
283 assert_eq!(DeprecationStatus::Deprecated(None), result);
284 }
285
286 #[test]
287 fn deprecation_with_reason() {
288 let directive = schema::Directive {
289 position: Pos::default(),
290 name: "deprecated".to_string(),
291 arguments: vec![(
292 "reason".to_string(),
293 query::Value::String("whatever".to_string())
294 )]
295 };
296 let result = parse_deprecation_info(&mock_field(vec![directive]));
297 assert_eq!(
298 DeprecationStatus::Deprecated(Some("whatever".to_string())),
299 result
300 );
301 }
302
303 #[test]
304 fn null_deprecation_reason() {
305 let directive = schema::Directive {
306 position: Pos::default(),
307 name: "deprecated".to_string(),
308 arguments: vec![("reason".to_string(), query::Value::Null)]
309 };
310 let result = parse_deprecation_info(&mock_field(vec![directive]));
311 assert_eq!(DeprecationStatus::Deprecated(None), result);
312 }
313
314 #[test]
315 #[should_panic]
316 fn invalid_deprecation_reason() {
317 let directive = schema::Directive {
318 position: Pos::default(),
319 name: "deprecated".to_string(),
320 arguments: vec![("reason".to_string(), query::Value::Boolean(true))]
321 };
322 let _ = parse_deprecation_info(&mock_field(vec![directive]));
323 }
324
325 #[test]
326 fn no_deprecation() {
327 let result = parse_deprecation_info(&mock_field(vec![]));
328 assert_eq!(DeprecationStatus::Current, result);
329 }
330}