apollo_encoder/
object_def.rs

1use std::fmt;
2
3use crate::{Directive, FieldDefinition, StringValue};
4/// ObjectDefinition types represent concrete instantiations of sets of fields.
5///
6/// The introspection types (e.g. `__Type`, `__Field`, etc) are examples of
7/// objects.
8///
9/// *ObjectTypeDefinition*:
10///     Description? **type** Name ImplementsInterfaces? Directives? FieldsDefinition?
11///
12/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/October2021/#sec-Object).
13///
14/// ### Example
15/// ```rust
16/// use apollo_encoder::{Type_, FieldDefinition, ObjectDefinition};
17/// use indoc::indoc;
18///
19/// let ty_1 = Type_::NamedType {
20///     name: "DanglerPoleToys".to_string(),
21/// };
22///
23/// let ty_2 = Type_::List { ty: Box::new(ty_1) };
24/// let mut field = FieldDefinition::new("toys".to_string(), ty_2);
25/// let ty_3 = Type_::NamedType {
26///     name: "FoodType".to_string(),
27/// };
28/// let mut field_2 = FieldDefinition::new("food".to_string(), ty_3);
29/// field_2.description("Dry or wet food?".to_string());
30///
31/// let ty_4 = Type_::NamedType {
32///     name: "Boolean".to_string(),
33/// };
34/// let field_3 = FieldDefinition::new("catGrass".to_string(), ty_4);
35///
36/// let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
37/// object_def.field(field);
38/// object_def.field(field_2);
39/// object_def.field(field_3);
40/// object_def.interface("ShoppingTrip".to_string());
41///
42/// assert_eq!(
43///     object_def.to_string(),
44///     indoc! { r#"
45///         type PetStoreTrip implements ShoppingTrip {
46///           toys: [DanglerPoleToys]
47///           "Dry or wet food?"
48///           food: FoodType
49///           catGrass: Boolean
50///         }
51///     "#}
52/// );
53/// ```
54#[derive(Debug, Clone)]
55pub struct ObjectDefinition {
56    // Name must return a String.
57    name: String,
58    // Description may return a String or null.
59    description: Option<StringValue>,
60    // The vector of interfaces that an object implements.
61    interfaces: Vec<String>,
62    // The vector of fields query‐able on this type.
63    fields: Vec<FieldDefinition>,
64    /// The vector of directives for this object
65    directives: Vec<Directive>,
66    extend: bool,
67}
68
69impl ObjectDefinition {
70    /// Create a new instance of ObjectDef with a name.
71    pub fn new(name: String) -> Self {
72        Self {
73            name,
74            description: None,
75            interfaces: Vec::new(),
76            fields: Vec::new(),
77            directives: Vec::new(),
78            extend: false,
79        }
80    }
81
82    /// Set the ObjectDef's description field.
83    pub fn description(&mut self, description: String) {
84        self.description = Some(StringValue::Top {
85            source: description,
86        });
87    }
88
89    /// Add a directive on ObjectDef.
90    pub fn directive(&mut self, directive: Directive) {
91        self.directives.push(directive)
92    }
93
94    /// Set the object type as an extension
95    pub fn extend(&mut self) {
96        self.extend = true;
97    }
98
99    /// Push a Field to ObjectDef's fields vector.
100    pub fn field(&mut self, field: FieldDefinition) {
101        self.fields.push(field)
102    }
103
104    /// Add an interface ObjectDef implements.
105    pub fn interface(&mut self, interface: String) {
106        self.interfaces.push(interface)
107    }
108}
109
110impl fmt::Display for ObjectDefinition {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        if self.extend {
113            write!(f, "extend ")?;
114        // No description when it's a extension
115        } else if let Some(description) = &self.description {
116            writeln!(f, "{description}")?;
117        }
118
119        write!(f, "type {}", &self.name)?;
120        for (i, interface) in self.interfaces.iter().enumerate() {
121            match i {
122                0 => write!(f, " implements {interface}")?,
123                _ => write!(f, " & {interface}")?,
124            }
125        }
126        for directive in &self.directives {
127            write!(f, " {directive}")?;
128        }
129
130        write!(f, " {{")?;
131
132        for field in &self.fields {
133            write!(f, "\n{field}")?;
134        }
135        writeln!(f, "\n}}")
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::{Argument, FieldDefinition, InputValueDefinition, Type_, Value};
143    use indoc::indoc;
144    use pretty_assertions::assert_eq;
145
146    #[test]
147    fn it_encodes_object_with_description() {
148        let ty_1 = Type_::NamedType {
149            name: "DanglerPoleToys".to_string(),
150        };
151
152        let ty_2 = Type_::List { ty: Box::new(ty_1) };
153        let field = FieldDefinition::new("toys".to_string(), ty_2);
154
155        let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
156        object_def.field(field);
157        object_def.description("What to get at Fressnapf?".to_string());
158
159        assert_eq!(
160            object_def.to_string(),
161            indoc! { r#"
162                "What to get at Fressnapf?"
163                type PetStoreTrip {
164                  toys: [DanglerPoleToys]
165                }
166            "#}
167        );
168    }
169
170    #[test]
171    fn it_encodes_object_with_field_directives() {
172        let ty_1 = Type_::NamedType {
173            name: "DanglerPoleToys".to_string(),
174        };
175
176        let mut field = FieldDefinition::new("toys".to_string(), ty_1);
177        let mut deprecated_directive = Directive::new(String::from("deprecated"));
178        deprecated_directive.arg(Argument::new(
179            String::from("reason"),
180            Value::String(String::from(
181                "\"DanglerPoleToys\" are no longer interesting",
182            )),
183        ));
184        field.directive(deprecated_directive);
185
186        let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
187        object_def.field(field);
188        object_def.description("What to get at Fressnapf?".to_string());
189
190        assert_eq!(
191            object_def.to_string(),
192            indoc! { r#"
193                "What to get at Fressnapf?"
194                type PetStoreTrip {
195                  toys: DanglerPoleToys @deprecated(reason: """"DanglerPoleToys" are no longer interesting""")
196                }
197            "#}
198        );
199    }
200
201    #[test]
202    fn it_encodes_object_with_extend() {
203        let ty_1 = Type_::NamedType {
204            name: "DanglerPoleToys".to_string(),
205        };
206
207        let mut field = FieldDefinition::new("toys".to_string(), ty_1);
208        let mut deprecated_directive = Directive::new(String::from("deprecated"));
209        deprecated_directive.arg(Argument::new(
210            String::from("reason"),
211            Value::String(String::from("DanglerPoleToys are no longer interesting")),
212        ));
213        field.directive(deprecated_directive);
214
215        let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
216        object_def.field(field);
217        object_def.description("What to get at Fressnapf?".to_string());
218        object_def.extend();
219
220        assert_eq!(
221            object_def.to_string(),
222            indoc! { r#"
223                extend type PetStoreTrip {
224                  toys: DanglerPoleToys @deprecated(reason: "DanglerPoleToys are no longer interesting")
225                }
226            "#}
227        );
228    }
229
230    #[test]
231    fn it_encodes_object_with_interface() {
232        let ty_1 = Type_::NamedType {
233            name: "DanglerPoleToys".to_string(),
234        };
235
236        let ty_2 = Type_::List { ty: Box::new(ty_1) };
237        let mut field = FieldDefinition::new("toys".to_string(), ty_2);
238        let mut deprecated_directive = Directive::new(String::from("deprecated"));
239        deprecated_directive.arg(Argument::new(
240            String::from("reason"),
241            Value::String(String::from("Cats are too spoiled")),
242        ));
243        field.directive(deprecated_directive);
244        let ty_3 = Type_::NamedType {
245            name: "FoodType".to_string(),
246        };
247        let mut field_2 = FieldDefinition::new("food".to_string(), ty_3);
248        field_2.description("Dry or wet food?".to_string());
249
250        let ty_4 = Type_::NamedType {
251            name: "Boolean".to_string(),
252        };
253        let field_3 = FieldDefinition::new("catGrass".to_string(), ty_4);
254
255        let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
256        object_def.field(field);
257        object_def.field(field_2);
258        object_def.field(field_3);
259        object_def.description("Shopping list for cats at the pet store.".to_string());
260        object_def.interface("ShoppingTrip".to_string());
261
262        assert_eq!(
263            object_def.to_string(),
264            indoc! { r#"
265                "Shopping list for cats at the pet store."
266                type PetStoreTrip implements ShoppingTrip {
267                  toys: [DanglerPoleToys] @deprecated(reason: "Cats are too spoiled")
268                  "Dry or wet food?"
269                  food: FoodType
270                  catGrass: Boolean
271                }
272            "#}
273        );
274    }
275
276    #[test]
277    fn it_encodes_object_with_interface_and_directives() {
278        let ty_1 = Type_::NamedType {
279            name: "DanglerPoleToys".to_string(),
280        };
281
282        let mut directive = Directive::new(String::from("testDirective"));
283        directive.arg(Argument::new(
284            String::from("first"),
285            Value::String("one".to_string()),
286        ));
287        let ty_2 = Type_::List { ty: Box::new(ty_1) };
288        let mut field = FieldDefinition::new("toys".to_string(), ty_2);
289        let mut deprecated_directive = Directive::new(String::from("deprecated"));
290        deprecated_directive.arg(Argument::new(
291            String::from("reason"),
292            Value::String(String::from("Cats are too spoiled")),
293        ));
294        field.directive(deprecated_directive);
295        let ty_3 = Type_::NamedType {
296            name: "FoodType".to_string(),
297        };
298        let mut field_2 = FieldDefinition::new("food".to_string(), ty_3);
299        field_2.description("Dry or wet food?".to_string());
300
301        let ty_4 = Type_::NamedType {
302            name: "Boolean".to_string(),
303        };
304        let mut field_3 = FieldDefinition::new("catGrass".to_string(), ty_4);
305
306        let value = Type_::NamedType {
307            name: "Boolean".to_string(),
308        };
309
310        let mut arg = InputValueDefinition::new("fresh".to_string(), value);
311        arg.description("How fresh is this grass".to_string());
312        field_3.arg(arg);
313
314        let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
315        object_def.field(field);
316        object_def.field(field_2);
317        object_def.field(field_3);
318        object_def.description("Shopping list for cats at the pet store.".to_string());
319        object_def.interface("ShoppingTrip".to_string());
320        object_def.directive(directive);
321
322        assert_eq!(
323            object_def.to_string(),
324            indoc! { r#"
325                "Shopping list for cats at the pet store."
326                type PetStoreTrip implements ShoppingTrip @testDirective(first: "one") {
327                  toys: [DanglerPoleToys] @deprecated(reason: "Cats are too spoiled")
328                  "Dry or wet food?"
329                  food: FoodType
330                  catGrass(
331                    "How fresh is this grass"
332                    fresh: Boolean
333                  ): Boolean
334                }
335            "#}
336        );
337    }
338
339    #[test]
340    fn it_encodes_object_with_input_value_definition_fields() {
341        let ty_1 = Type_::NamedType {
342            name: "DanglerPoleToys".to_string(),
343        };
344
345        let ty_2 = Type_::List { ty: Box::new(ty_1) };
346        let field = FieldDefinition::new("toys".to_string(), ty_2);
347
348        let ty_4 = Type_::NamedType {
349            name: "Boolean".to_string(),
350        };
351        let mut field_3 = FieldDefinition::new("catGrass".to_string(), ty_4);
352
353        let value = Type_::NamedType {
354            name: "Boolean".to_string(),
355        };
356
357        let mut arg = InputValueDefinition::new("fresh".to_string(), value);
358        arg.description("How fresh is this grass".to_string());
359        field_3.arg(arg);
360
361        let value = Type_::NamedType {
362            name: "Int".to_string(),
363        };
364
365        let mut arg = InputValueDefinition::new("quantity".to_string(), value);
366        arg.description("Number of pots\ngrowing at a given time".to_string());
367        field_3.arg(arg);
368
369        let mut object_def = ObjectDefinition::new("PetStoreTrip".to_string());
370        object_def.field(field);
371        object_def.field(field_3);
372        object_def.description("Shopping list for cats at the pet store.".to_string());
373
374        assert_eq!(
375            object_def.to_string(),
376            indoc! { r#"
377                "Shopping list for cats at the pet store."
378                type PetStoreTrip {
379                  toys: [DanglerPoleToys]
380                  catGrass(
381                    "How fresh is this grass"
382                    fresh: Boolean,
383                    """
384                    Number of pots
385                    growing at a given time
386                    """
387                    quantity: Int
388                  ): Boolean
389                }
390            "#}
391        );
392    }
393
394    #[test]
395    fn it_encodes_object_with_block_string_description() {
396        let ty_1 = Type_::NamedType {
397            name: "String".to_string(),
398        };
399
400        let mut field = FieldDefinition::new("name".to_string(), ty_1);
401        field.description("multiline\ndescription".to_string());
402
403        let mut object_def = ObjectDefinition::new("Book".to_string());
404        object_def.field(field);
405        object_def.description("Book Object\nType".to_string());
406
407        assert_eq!(
408            object_def.to_string(),
409            indoc! { r#"
410                """
411                Book Object
412                Type
413                """
414                type Book {
415                  """
416                  multiline
417                  description
418                  """
419                  name: String
420                }
421            "#}
422        );
423    }
424}