graphql_query/schema/
schema_reference.rs

1use crate::{
2    ast::OperationKind,
3    error::{Error, Result},
4};
5
6use super::{
7    OutputType, Schema, SchemaField, SchemaFields, SchemaInterfaces, SchemaObject,
8    SchemaPossibleTypes,
9};
10
11/// Schema Reference
12///
13/// A stateful traverser that may be used to traverse a schema as a query is traversed. It supports
14/// diving into types by specifying fields and fragment conditions. As a query is traversed it
15/// keeps a stack of previous types, hence, as fields and fragments are traversed it can keep track
16/// of the current type that a selection set is operating on.
17#[derive(Clone)]
18pub struct SchemaReference<'a> {
19    output_stack: Vec<OutputType<'a>>,
20    pointer: OutputType<'a>,
21    schema: &'a Schema<'a>,
22}
23
24impl<'a> SchemaReference<'a> {
25    /// Create a schema reference pointer from a given [SchemaObject] type to start from.
26    #[inline]
27    pub fn from_object_type(schema: &'a Schema, object: &'a SchemaObject<'a>) -> Self {
28        SchemaReference {
29            schema,
30            output_stack: Vec::with_capacity(32),
31            pointer: OutputType::Object(object),
32        }
33    }
34
35    /// Create a schema reference pointer from a schema and selected fragment type-condition
36    #[inline]
37    pub fn from_fragment(schema: &'a Schema<'a>, type_condition: &'a str) -> Result<Self> {
38        let typename = schema.get_type(type_condition);
39        match typename {
40            Some(typename) => Ok(SchemaReference {
41                schema,
42                output_stack: Vec::with_capacity(32),
43                pointer: typename
44                    .output_type()
45                    .expect("This should be an object-output-type."),
46            }),
47            None => Err(Error::new(
48                "Schema does not support given type-condition.",
49                None,
50            )),
51        }
52    }
53
54    /// Create a schema reference pointer from a schema and selected root operation kind.
55    #[inline]
56    pub fn from_schema(schema: &'a Schema<'a>, operation_kind: OperationKind) -> Result<Self> {
57        Ok(SchemaReference {
58            schema,
59            output_stack: Vec::with_capacity(32),
60            pointer: OutputType::Object(schema.get_root_type(operation_kind).ok_or_else(|| {
61                Error::new(
62                    "Schema does not support selected root operation type.",
63                    None,
64                )
65            })?),
66        })
67    }
68
69    #[inline(always)]
70    fn push_pointer(&mut self, pointer: OutputType<'a>) {
71        self.output_stack.push(self.pointer);
72        self.pointer = pointer;
73    }
74
75    /// Leave the current type and return to the previously pointed at output type.
76    #[inline]
77    pub fn leave_type(&mut self) -> Result<OutputType<'a>> {
78        if let Some(pointer) = self.output_stack.pop() {
79            self.pointer = pointer;
80            Ok(pointer)
81        } else {
82            Err(Error::new("Cannot leave root type.", None))
83        }
84    }
85
86    /// Returns the current pointer's referenced [OutputType].
87    #[inline]
88    pub fn output_type(&self) -> OutputType<'a> {
89        self.pointer
90    }
91
92    /// Returns a field, if possible, on the current [OutputType].
93    #[inline]
94    pub fn get_field(&self, field_name: &'a str) -> Option<&SchemaField<'a>> {
95        match self.pointer {
96            OutputType::Object(object) => object.get_field(field_name),
97            OutputType::Interface(interface) => interface.get_field(field_name),
98            _ => None,
99        }
100    }
101
102    /// Traverse deeper by selecting a field on the current [OutputType] and return the next
103    /// [OutputType].
104    #[inline]
105    pub fn select_field(&mut self, field_name: &'a str) -> Result<OutputType<'a>> {
106        let fields = match self.pointer {
107            OutputType::Object(object) => Ok(object.get_fields()),
108            OutputType::Interface(interface) => Ok(interface.get_fields()),
109            _ => Err(Error::new(
110                "Cannot select fields on non-object/interface type.",
111                None,
112            )),
113        }?;
114        if let Some(field) = fields.get(field_name) {
115            let output_type = field
116                .output_type
117                .of_type(self.schema)
118                .output_type()
119                .expect("This to be an output type.");
120            self.push_pointer(output_type);
121            Ok(output_type)
122        } else {
123            Err(Error::new("Cannot select unknown fields.", None))
124        }
125    }
126
127    /// Traverse deeper by applying a fragment condition on the current [OutputType] and return the next
128    /// [OutputType].
129    pub fn select_condition(&mut self, type_name: &'a str) -> Result<OutputType<'a>> {
130        match self.pointer {
131            OutputType::Object(schema_object) if schema_object.name == type_name => {
132                self.push_pointer(self.pointer);
133                Ok(self.pointer)
134            }
135            OutputType::Object(schema_object) => {
136                let maybe_interface = schema_object
137                    .get_interfaces()
138                    .into_iter()
139                    .find(|interface| *interface == type_name)
140                    .map(|x| {
141                        self.schema
142                            .get_type(x)
143                            .expect("The type to exist")
144                            .output_type()
145                            .expect("and it to be an output type.")
146                    });
147                if let Some(output_type) = maybe_interface {
148                    self.push_pointer(output_type);
149                    Ok(output_type)
150                } else {
151                    Err(Error::new(
152                        "No possible interface type found for spread name.",
153                        None,
154                    ))
155                }
156            }
157            OutputType::Interface(interface) => {
158                let maybe_interface = interface
159                    .get_interfaces()
160                    .into_iter()
161                    .chain(interface.get_possible_interfaces())
162                    .find(|interface| *interface == type_name)
163                    .map(|x| {
164                        self.schema
165                            .get_type(x)
166                            .expect("The type to exist")
167                            .output_type()
168                            .expect("and it to be an output type.")
169                    });
170                if let Some(output_type) = maybe_interface {
171                    self.push_pointer(output_type);
172                    return Ok(output_type);
173                };
174                let maybe_object = interface
175                    .get_possible_types()
176                    .into_iter()
177                    .find(|object| *object == type_name)
178                    .map(|x| {
179                        self.schema
180                            .get_type(x)
181                            .expect("The type to exist")
182                            .output_type()
183                            .expect("and it to be an output type.")
184                    });
185                if let Some(output_type) = maybe_object {
186                    self.push_pointer(output_type);
187                    Ok(output_type)
188                } else {
189                    Err(Error::new(
190                        "No possible interface/object type found for spread name.",
191                        None,
192                    ))
193                }
194            }
195            OutputType::Union(schema_union) => {
196                let maybe_object = schema_union.get_possible_type(type_name).map(|x| {
197                    self.schema
198                        .get_type(x)
199                        .expect("The type to exist")
200                        .output_type()
201                        .expect("and it to be an output type.")
202                });
203                if let Some(output_type) = maybe_object {
204                    self.push_pointer(output_type);
205                    Ok(output_type)
206                } else {
207                    Err(Error::new(
208                        "No possible object type found for spread name.",
209                        None,
210                    ))
211                }
212            }
213            OutputType::Scalar(_) | OutputType::Enum(_) => Err(Error::new(
214                "Cannot spread fragment on non-abstract type.",
215                None,
216            )),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::super::{BuildClientSchema, IntrospectionQuery};
224    use super::*;
225    use crate::ast::ASTContext;
226
227    #[test]
228    fn walk_schema() {
229        let ctx = ASTContext::new();
230        let introspection_json = include_str!("../../fixture/introspection_query.json");
231        let introspection: IntrospectionQuery = serde_json::from_str(introspection_json).unwrap();
232        let schema = introspection.build_client_schema(&ctx);
233
234        let mut schema_ref = SchemaReference::from_schema(schema, OperationKind::Query).unwrap();
235
236        assert_eq!(schema_ref.output_type().name(), "query_root");
237
238        let todo_type = schema_ref.select_field("todos").unwrap();
239        assert_eq!(todo_type.name(), "Todo");
240
241        let author_type = schema_ref.select_field("author").unwrap();
242        assert_eq!(author_type.name(), "Author");
243
244        assert_eq!(schema_ref.leave_type().unwrap().name(), "Todo");
245        assert_eq!(schema_ref.leave_type().unwrap().name(), "query_root");
246    }
247
248    #[test]
249    fn selecting_type_repeatedly() {
250        let ctx = ASTContext::new();
251        let introspection_json = include_str!("../../fixture/introspection_query.json");
252        let introspection: IntrospectionQuery = serde_json::from_str(introspection_json).unwrap();
253        let schema = introspection.build_client_schema(&ctx);
254
255        let mut schema_ref = SchemaReference::from_schema(schema, OperationKind::Mutation).unwrap();
256
257        assert_eq!(schema_ref.output_type().name(), "mutation_root");
258
259        let mutation_type = schema_ref.select_condition("mutation_root").unwrap();
260        assert_eq!(mutation_type.name(), "mutation_root");
261
262        // We can leave once since we selected once...
263        assert_eq!(schema_ref.leave_type().unwrap().name(), "mutation_root");
264        // ..but not a second time
265        assert_eq!(
266            schema_ref.leave_type().unwrap_err().message,
267            "Cannot leave root type."
268        );
269    }
270}