artemis_codegen/
objects.rs

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}