graphql_query/schema/
build_client_schema.rs

1use self::private::BuildSchemaContext;
2
3use super::introspection::{IntrospectionQuery, IntrospectionSchema};
4use super::schema::Schema;
5use crate::ast::ASTContext;
6
7pub(crate) mod private {
8    use hashbrown::{hash_map::DefaultHashBuilder, HashMap};
9
10    use super::super::{introspection::*, schema::*};
11    use super::ASTContext;
12
13    fn from_output_type_ref<'a>(
14        ctx: &'a ASTContext,
15        output: &IntrospectionOutputTypeRef,
16    ) -> &'a TypeRef<'a> {
17        use std::ops::Deref;
18        let out = match output {
19            IntrospectionOutputTypeRef::List { of_type } => {
20                TypeRef::ListType(from_output_type_ref(ctx, of_type.deref()))
21            }
22            IntrospectionOutputTypeRef::NonNull { of_type } => {
23                TypeRef::NonNullType(from_output_type_ref(ctx, of_type.deref()))
24            }
25            IntrospectionOutputTypeRef::ScalarType { name }
26            | IntrospectionOutputTypeRef::EnumType { name }
27            | IntrospectionOutputTypeRef::ObjectType { name }
28            | IntrospectionOutputTypeRef::InterfaceType { name }
29            | IntrospectionOutputTypeRef::UnionType { name } => {
30                // TODO: Check whether type matches here
31                let name = ctx.alloc_str(name);
32                TypeRef::Type(name)
33            }
34        };
35
36        ctx.alloc(out)
37    }
38
39    #[derive(Clone)]
40    pub struct BuildSchemaContext<'arena> {
41        pub(crate) ctx: &'arena ASTContext,
42    }
43
44    impl<'arena> BuildSchemaContext<'arena> {
45        pub(crate) fn new(ctx: &'arena ASTContext) -> Self {
46            BuildSchemaContext { ctx }
47        }
48
49        pub fn build_schema(
50            &'arena self,
51            introspection: &IntrospectionSchema<'arena>,
52        ) -> &'arena Schema<'arena> {
53            // We have created our initial set of types, this ensures that every type is present
54            // before we start building them.
55            let mut schema_types: HashMap<
56                &str,
57                &'arena SchemaType,
58                DefaultHashBuilder,
59                &bumpalo::Bump,
60            > = HashMap::new_in(&self.ctx.arena);
61            for introspection_type in introspection.types.iter() {
62                let schema_type = BuildSchemaType::on_create(introspection_type, self);
63                schema_types.insert(
64                    self.ctx.alloc_str(introspection_type.name()),
65                    self.ctx.alloc(schema_type),
66                );
67            }
68
69            let query_type = introspection
70                .query_type
71                .as_ref()
72                .and_then(|type_ref| schema_types.get(&type_ref.name))
73                .and_then(|schema_type| schema_type.object());
74
75            let mutation_type = introspection
76                .mutation_type
77                .as_ref()
78                .and_then(|type_ref| schema_types.get(&type_ref.name))
79                .and_then(|schema_type| schema_type.object());
80
81            let subscription_type = introspection
82                .subscription_type
83                .as_ref()
84                .and_then(|type_ref| schema_types.get(&type_ref.name))
85                .and_then(|schema_type| schema_type.object());
86
87            self.ctx.alloc(Schema {
88                query_type,
89                mutation_type,
90                subscription_type,
91                types: schema_types,
92            })
93        }
94    }
95
96    pub trait BuildSchemaType<'arena, T>: Sized {
97        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> T;
98    }
99
100    impl<'arena> BuildSchemaType<'arena, SchemaType<'arena>> for IntrospectionType<'arena> {
101        #[inline]
102        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaType<'arena> {
103            match self {
104                IntrospectionType::Scalar(scalar) => {
105                    SchemaType::Scalar(ctx.ctx.alloc(scalar.on_create(ctx)))
106                }
107                IntrospectionType::Object(object) => {
108                    SchemaType::Object(ctx.ctx.alloc(object.on_create(ctx)))
109                }
110                IntrospectionType::Interface(interface) => {
111                    SchemaType::Interface(ctx.ctx.alloc(interface.on_create(ctx)))
112                }
113                IntrospectionType::Union(union_type) => {
114                    SchemaType::Union(ctx.ctx.alloc(union_type.on_create(ctx)))
115                }
116                IntrospectionType::Enum(enum_type) => {
117                    SchemaType::Enum(ctx.ctx.alloc(enum_type.on_create(ctx)))
118                }
119                IntrospectionType::InputObject(input_object) => {
120                    SchemaType::InputObject(ctx.ctx.alloc(input_object.on_create(ctx)))
121                }
122            }
123        }
124    }
125
126    impl<'arena> BuildSchemaType<'arena, SchemaScalar<'arena>> for IntrospectionScalarType<'arena> {
127        #[inline]
128        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaScalar<'arena> {
129            SchemaScalar::new(ctx.ctx.alloc_str(self.name))
130        }
131    }
132
133    impl<'arena> BuildSchemaType<'arena, SchemaEnum<'arena>> for IntrospectionEnumType<'arena> {
134        #[inline]
135        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaEnum<'arena> {
136            let name = ctx.ctx.alloc_str(self.name);
137            let mut enum_type = SchemaEnum::new(&ctx.ctx, name);
138            for value in self.enum_values.iter() {
139                let value_name = ctx.ctx.alloc_str(value.name);
140                enum_type.add_value(ctx.ctx, value_name);
141            }
142            enum_type
143        }
144    }
145
146    impl<'arena> BuildSchemaType<'arena, SchemaUnion<'arena>> for IntrospectionUnionType<'arena> {
147        #[inline]
148        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaUnion<'arena> {
149            let name = ctx.ctx.alloc_str(self.name);
150            let mut schema_union_type = SchemaUnion::new(ctx.ctx, name);
151            for introspection_type_ref in self.possible_types.possible_types.iter() {
152                let name = ctx.ctx.alloc_str(introspection_type_ref.name);
153                schema_union_type.add_possible_type(ctx.ctx, name);
154            }
155            schema_union_type
156        }
157    }
158
159    fn from_input_type_ref<'arena>(
160        ctx: &'arena ASTContext,
161        input: &IntrospectionInputTypeRef,
162    ) -> &'arena TypeRef<'arena> {
163        use std::ops::Deref;
164        let type_ref = match input {
165            IntrospectionInputTypeRef::List { of_type } => {
166                TypeRef::ListType(from_input_type_ref(ctx, of_type.deref()))
167            }
168            IntrospectionInputTypeRef::NonNull { of_type } => {
169                TypeRef::NonNullType(from_input_type_ref(ctx, of_type.deref()))
170            }
171            IntrospectionInputTypeRef::ScalarType { name }
172            | IntrospectionInputTypeRef::EnumType { name }
173            | IntrospectionInputTypeRef::InputObjectType { name } => {
174                // TODO: Check whether type matches here
175                let name = ctx.alloc_str(name);
176                TypeRef::Type(name)
177            }
178        };
179
180        ctx.alloc(type_ref)
181    }
182
183    impl<'arena> BuildSchemaType<'arena, SchemaObject<'arena>> for IntrospectionObjectType<'arena> {
184        #[inline]
185        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaObject<'arena> {
186            let name = ctx.ctx.alloc_str(self.name);
187            let mut schema_object_type = SchemaObject::new(ctx.ctx, name);
188            for field in self.implementation.fields.iter() {
189                let field_name = ctx.ctx.alloc_str(field.name);
190                let mut schema_field = SchemaField::new(
191                    &ctx.ctx,
192                    field_name,
193                    from_output_type_ref(ctx.ctx, &field.of_type),
194                );
195
196                for arg in field.args.iter() {
197                    let arg_name = ctx.ctx.alloc_str(arg.name);
198                    let input_field =
199                        SchemaInputField::new(arg_name, from_input_type_ref(ctx.ctx, &arg.of_type));
200                    schema_field.add_argument(ctx.ctx, input_field);
201                }
202
203                schema_object_type.add_field(ctx.ctx, schema_field);
204            }
205
206            if let Some(interfaces) = &self.implementation.interfaces {
207                for introspection_type_ref in interfaces.iter() {
208                    let name = ctx.ctx.alloc_str(introspection_type_ref.name);
209                    schema_object_type.add_interface(ctx.ctx, name);
210                }
211            }
212            schema_object_type
213        }
214    }
215
216    impl<'arena> BuildSchemaType<'arena, SchemaInterface<'arena>>
217        for IntrospectionInterfaceType<'arena>
218    {
219        #[inline]
220        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaInterface<'arena> {
221            let name = ctx.ctx.alloc_str(self.name);
222            let mut schema_interface_type = SchemaInterface::new(ctx.ctx, name);
223
224            for field in self.implementation.fields.iter() {
225                let field_name = ctx.ctx.alloc_str(field.name);
226                let mut schema_field = SchemaField::new(
227                    &ctx.ctx,
228                    field_name,
229                    from_output_type_ref(ctx.ctx, &field.of_type),
230                );
231                for arg in field.args.iter() {
232                    let arg_name = ctx.ctx.alloc_str(arg.name);
233                    let input_field =
234                        SchemaInputField::new(arg_name, from_input_type_ref(ctx.ctx, &arg.of_type));
235                    schema_field.add_argument(ctx.ctx, input_field);
236                }
237                schema_interface_type.add_field(ctx.ctx, schema_field);
238            }
239
240            if let Some(interfaces) = &self.implementation.interfaces {
241                for introspection_type_ref in interfaces.iter() {
242                    let name = ctx.ctx.alloc_str(introspection_type_ref.name);
243                    schema_interface_type.add_interface(ctx.ctx, name);
244                }
245            }
246
247            for introspection_type_ref in self.possible_types.possible_types.iter() {
248                let name = ctx.ctx.alloc_str(introspection_type_ref.name);
249                schema_interface_type.add_possible_type(ctx.ctx, name);
250            }
251
252            schema_interface_type
253        }
254    }
255
256    impl<'arena> BuildSchemaType<'arena, SchemaInputObject<'arena>>
257        for IntrospectionInputObjectType<'arena>
258    {
259        #[inline]
260        fn on_create(&self, ctx: &'arena BuildSchemaContext<'arena>) -> SchemaInputObject<'arena> {
261            let name = ctx.ctx.alloc_str(self.name);
262            let mut input = SchemaInputObject::new(ctx.ctx, name);
263            for field in self.input_fields.iter() {
264                let field_name = ctx.ctx.alloc_str(field.name);
265                let input_field =
266                    SchemaInputField::new(field_name, from_input_type_ref(ctx.ctx, &field.of_type));
267                input.add_field(ctx.ctx, input_field);
268            }
269
270            input
271        }
272    }
273}
274
275pub trait BuildClientSchema<'arena> {
276    /// Converts the introspected data to a [Schema].
277    fn build_client_schema(&self, ctx: &'arena ASTContext) -> &'arena Schema<'arena>;
278}
279
280impl<'arena> BuildClientSchema<'arena> for IntrospectionSchema<'arena> {
281    /// Converts the introspected data to a [Schema].
282    fn build_client_schema(&self, ctx: &'arena ASTContext) -> &'arena Schema<'arena> {
283        let builder_ctx: &mut BuildSchemaContext =
284            ctx.arena.alloc(private::BuildSchemaContext::new(ctx));
285        builder_ctx.build_schema(self)
286    }
287}
288
289impl<'arena> BuildClientSchema<'arena> for IntrospectionQuery<'arena> {
290    /// Converts the introspected data to a [Schema].
291    fn build_client_schema(&self, ctx: &'arena ASTContext) -> &'arena Schema<'arena> {
292        self.schema.build_client_schema(ctx)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::super::schema::{SchemaFields, SchemaPossibleTypes, SchemaSuperType};
299    use super::*;
300
301    #[test]
302    fn build_schema() {
303        let ctx = ASTContext::new();
304        let introspection_json = include_str!("../../fixture/introspection_query.json");
305        let introspection: IntrospectionQuery = serde_json::from_str(introspection_json).unwrap();
306        let schema = introspection.build_client_schema(&ctx);
307
308        let query_root_name = schema.query_type.map(|obj| obj.name).unwrap();
309        assert_eq!(query_root_name, "query_root");
310
311        assert!(std::ptr::eq(
312            schema
313                .get_type(query_root_name)
314                .and_then(|t| t.object())
315                .unwrap(),
316            schema.query_type.unwrap()
317        ));
318    }
319
320    #[test]
321    fn schema_fields() {
322        let ctx = ASTContext::new();
323        let introspection_json = include_str!("../../fixture/introspection_query.json");
324        let introspection: IntrospectionQuery = serde_json::from_str(introspection_json).unwrap();
325        let schema = introspection.build_client_schema(&ctx);
326
327        let todo_type = schema.get_type("Todo").and_then(|t| t.object()).unwrap();
328        let author_type = schema.get_type("Author").unwrap();
329
330        todo_type.get_field("id").unwrap();
331        todo_type.get_field("text").unwrap();
332
333        let author_field = todo_type.get_field("author").unwrap();
334        let maybe_author_type = author_field.output_type.of_type(schema);
335        assert!(schema.is_sub_type(*author_type, *maybe_author_type));
336        assert!(author_type == maybe_author_type);
337    }
338
339    #[test]
340    fn schema_abstract_relationships() {
341        let ctx = ASTContext::new();
342        let introspection_json = include_str!("../../fixture/introspection_query.json");
343        let introspection: IntrospectionQuery = serde_json::from_str(introspection_json).unwrap();
344        let schema = introspection.build_client_schema(&ctx);
345
346        let type_itodo = schema
347            .get_type("ITodo")
348            .and_then(|t| t.interface())
349            .unwrap();
350        let type_bigtodo = schema.get_type("BigTodo").and_then(|t| t.object()).unwrap();
351        let type_smalltodo = schema
352            .get_type("SmallTodo")
353            .and_then(|t| t.object())
354            .unwrap();
355        let type_search = schema
356            .get_type("Search")
357            .and_then(|t| t.union_type())
358            .unwrap();
359
360        assert!(type_search.get_possible_type("SmallTodo").is_some());
361        assert!(type_search.get_possible_type("BigTodo").is_some());
362
363        assert!(type_search.is_sub_type(type_smalltodo.into()));
364        assert!(type_search.is_sub_type(type_bigtodo.into()));
365
366        assert!(type_itodo.is_sub_type(type_smalltodo.into()));
367        assert!(type_itodo.is_sub_type(type_bigtodo.into()));
368
369        assert!(type_itodo.get_possible_type(type_bigtodo.name).is_some());
370        assert!(type_itodo.get_possible_type(type_smalltodo.name).is_some());
371    }
372}