Skip to main content

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