shape/
graphql.rs

1//! Helper functions and impls for creating [`Shape`]s from [`apollo_compiler`] types.
2
3use apollo_compiler::ast::FieldDefinition;
4use apollo_compiler::ast::Type;
5use apollo_compiler::collections::IndexMap;
6use apollo_compiler::parser::SourceSpan;
7use apollo_compiler::schema::Component;
8use apollo_compiler::schema::EnumType;
9use apollo_compiler::schema::ExtendedType;
10use apollo_compiler::schema::InputObjectType;
11use apollo_compiler::schema::InterfaceType;
12use apollo_compiler::schema::ObjectType;
13use apollo_compiler::schema::UnionType;
14use apollo_compiler::Name;
15use apollo_compiler::Node;
16use apollo_compiler::Schema;
17use indexmap::IndexSet;
18
19use crate::location::Location;
20use crate::location::SourceId;
21use crate::name::Name as CrateName;
22use crate::name::Namespace;
23use crate::name::NotFinal;
24use crate::Shape;
25
26/// Computes a [`Namespace<NotFinal>`] from a GraphQL `&Schema`, allowing for
27/// mutual type references and recursive self-references.
28#[must_use]
29pub fn namespace_from_schema(schema: &Schema) -> Namespace<NotFinal> {
30    GraphQLSchemaWalker::new(schema).compute_namespace()
31}
32
33#[derive(Debug)]
34struct GraphQLSchemaWalker<'a> {
35    schema: &'a Schema,
36    visited: IndexSet<&'a str>,
37}
38
39impl<'a> GraphQLSchemaWalker<'a> {
40    fn new(schema: &'a Schema) -> Self {
41        Self {
42            schema,
43            visited: IndexSet::new(),
44        }
45    }
46
47    fn compute_namespace(mut self) -> Namespace<NotFinal> {
48        let mut namespace = Namespace::new();
49
50        // Predefine all GraphQL built-in types.
51        namespace.insert("String", Shape::string([]));
52        namespace.insert("Int", Shape::int([]));
53        namespace.insert("Float", Shape::float([]));
54        namespace.insert("Boolean", Shape::bool([]));
55        namespace.insert("ID", Shape::one([Shape::string([]), Shape::int([])], []));
56
57        for (name, extended) in &self.schema.types {
58            if namespace.has(name.as_str()) {
59                debug_assert!(extended.is_built_in());
60            } else if !extended.is_built_in() {
61                namespace.insert(name.as_str(), self.shape_from_extended_type(extended));
62            }
63        }
64
65        namespace
66    }
67
68    fn shape_from_type(&mut self, ty: &Type) -> Shape {
69        let inner_name = ty.inner_named_type();
70        let inner_shape = if let Some(inner_type) = self.schema.types.get(inner_name) {
71            self.shape_from_extended_type(inner_type)
72        } else {
73            Shape::unknown(span(ty.inner_named_type().location()))
74        };
75
76        match ty {
77            Type::Named(_) => Self::nullable(inner_shape),
78            Type::NonNullNamed(_) => inner_shape,
79            Type::List(_) => Self::nullable(Shape::list(inner_shape, [])),
80            Type::NonNullList(_) => Shape::list(inner_shape, []),
81        }
82    }
83
84    fn nullable(shape: Shape) -> Shape {
85        let locations = shape.locations().cloned().collect::<Vec<_>>();
86        Shape::one([shape, Shape::null([])], locations)
87    }
88
89    fn shape_from_extended_type(&mut self, extended: &'a ExtendedType) -> Shape {
90        self.shape_from_extended_type_with_context(extended, false)
91    }
92
93    fn shape_from_extended_type_with_context(
94        &mut self,
95        extended: &'a ExtendedType,
96        in_abstract_context: bool,
97    ) -> Shape {
98        let type_name = extended.name().as_str();
99
100        // Check for cycles - if we're already visiting this type, return a name reference
101        if self.visited.contains(type_name) {
102            return Shape::name(type_name, span(extended.location()));
103        }
104
105        // Mark this type as being visited
106        self.visited.insert(type_name);
107
108        let result = match extended {
109            ExtendedType::Scalar(node) => Shape::unknown(span(node.location())),
110
111            ExtendedType::Object(node) => {
112                // Add __typename field based on context
113                let typename_shape = if in_abstract_context {
114                    // In abstract context (interface/union), __typename is just the concrete type name
115                    Shape::string_value(type_name, span(node.location()))
116                } else {
117                    // In concrete context, __typename includes None
118                    Shape::one(
119                        [
120                            Shape::string_value(type_name, span(node.location())),
121                            Shape::none(),
122                        ],
123                        span(node.location()),
124                    )
125                };
126
127                let fields = node
128                    .fields
129                    .iter()
130                    .map(|(name, field)| (name.to_string(), self.shape_from_type(&field.ty)))
131                    .chain(std::iter::once(("__typename".to_string(), typename_shape)))
132                    .collect();
133
134                Shape::record(fields, span(node.location()))
135            }
136
137            ExtendedType::Interface(node) => {
138                // When processing an interface, we need to create a union of implementing types
139                // where each type knows it's in an abstract context for __typename generation
140                let implementing_types: Vec<Shape> = self
141                    .schema
142                    .types
143                    .iter()
144                    .filter_map(|(_name, extended)| {
145                        if let ExtendedType::Object(obj) = extended {
146                            if obj.implements_interfaces.contains(&node.name) {
147                                // Get the implementing type shape with abstract context
148                                let mut impl_shape =
149                                    self.shape_from_extended_type_with_context(extended, true);
150
151                                // Assign the concrete type name to preserve field aliasing
152                                let concrete_type_name = extended.name().as_str();
153                                let concrete_name =
154                                    CrateName::base(concrete_type_name, span(extended.location()));
155                                impl_shape = impl_shape.with_name(&concrete_name);
156
157                                Some(impl_shape)
158                            } else {
159                                None
160                            }
161                        } else {
162                            None
163                        }
164                    })
165                    .collect();
166
167                Shape::one(implementing_types, span(node.location()))
168            }
169
170            ExtendedType::Union(node) => Shape::one(
171                node.members.iter().filter_map(|member| {
172                    self.schema.types.get(&member.name).map(|extended| {
173                        // Get the member type shape with abstract context
174                        let mut member_shape =
175                            self.shape_from_extended_type_with_context(extended, true);
176
177                        // Assign the concrete type name to preserve field aliasing
178                        let concrete_type_name = extended.name().as_str();
179                        let concrete_name =
180                            CrateName::base(concrete_type_name, span(extended.location()));
181                        member_shape = member_shape.with_name(&concrete_name);
182
183                        member_shape
184                    })
185                }),
186                span(node.location()),
187            ),
188
189            ExtendedType::Enum(node) => Shape::one(
190                node.values
191                    .iter()
192                    .map(|(name, _)| Shape::string_value(name.as_str(), span(name.location()))),
193                span(node.location()),
194            ),
195
196            ExtendedType::InputObject(node) => Shape::record(
197                node.fields
198                    .iter()
199                    .map(|(name, field)| (name.to_string(), self.shape_from_type(&field.ty)))
200                    .collect(),
201                span(node.location()),
202            ),
203        };
204
205        // Remove this type from visited set now that we're done processing it
206        self.visited.shift_remove(type_name);
207
208        result
209    }
210}
211
212/// Get all the shapes for a GraphQL [`Schema`], keyed by their GraphQL [`Name`].
213///
214/// Locations will be automatically set with
215#[must_use]
216pub fn shapes_for_schema(schema: &Schema) -> IndexMap<String, Shape> {
217    namespace_from_schema(schema).finalize().iter().collect()
218}
219
220#[must_use]
221pub fn shape_for_arguments(field_definition: &Component<FieldDefinition>) -> Shape {
222    // Accumulate the locations of all the arguments so they can be highlighted together
223    let mut source_span = None;
224    for next_source_span in field_definition.arguments.iter().map(Node::location) {
225        source_span = SourceSpan::recompose(source_span, next_source_span);
226    }
227    Shape::record(
228        field_definition
229            .arguments
230            .iter()
231            .map(|arg| {
232                (
233                    arg.name.to_string(),
234                    from_type(arg.ty.as_ref(), span(arg.location())),
235                )
236            })
237            .collect(),
238        span(source_span),
239    )
240}
241
242impl From<&ExtendedType> for Shape {
243    fn from(ext: &ExtendedType) -> Self {
244        match ext {
245            ExtendedType::Scalar(scalar) => Shape::unknown(span(scalar.location())),
246            ExtendedType::Object(obj) => Self::from(obj),
247            ExtendedType::Interface(intf) => Self::from(intf),
248            ExtendedType::Union(union) => Self::from(union),
249            ExtendedType::Enum(enm) => Self::from(enm),
250            ExtendedType::InputObject(input) => Self::from(input),
251        }
252    }
253}
254
255impl From<&Name> for Shape {
256    fn from(name: &Name) -> Self {
257        let locations = span(name.location());
258        match name.as_str() {
259            "String" | "ID" => Self::string(locations),
260            "Int" => Self::int(locations),
261            "Float" => Self::float(locations),
262            "Boolean" => Self::bool(locations),
263            other => Self::name(other, locations),
264        }
265    }
266}
267
268impl From<&Node<ObjectType>> for Shape {
269    fn from(object_type: &Node<ObjectType>) -> Self {
270        record(&object_type.fields, span(object_type.location()))
271    }
272}
273
274fn record(fields: &IndexMap<Name, Component<FieldDefinition>>, locations: Vec<Location>) -> Shape {
275    let fields = fields
276        .iter()
277        .map(|(key, value)| (key.to_string(), Shape::from(value)))
278        .collect();
279    Shape::record(fields, locations)
280}
281
282fn nullable(shape: Shape, locations: Vec<Location>) -> Shape {
283    Shape::one([shape, Shape::null(locations.clone())], locations)
284}
285
286impl From<&Component<FieldDefinition>> for Shape {
287    fn from(field: &Component<FieldDefinition>) -> Self {
288        let locations = span(field.location());
289        from_type(&field.ty, locations)
290    }
291}
292
293fn from_type(ty: &Type, locations: Vec<Location>) -> Shape {
294    match ty {
295        Type::Named(name) => nullable(Shape::from(name), locations),
296        Type::NonNullNamed(name) => Shape::from(name),
297        Type::List(ty) => nullable(
298            Shape::list(from_type(ty.as_ref(), locations.clone()), locations.clone()),
299            locations,
300        ),
301        Type::NonNullList(ty) => Shape::list(from_type(ty.as_ref(), locations.clone()), locations),
302    }
303}
304
305impl From<&Node<InterfaceType>> for Shape {
306    fn from(interface_type: &Node<InterfaceType>) -> Self {
307        record(&interface_type.fields, span(interface_type.location()))
308    }
309}
310
311impl From<&Node<UnionType>> for Shape {
312    fn from(union_type: &Node<UnionType>) -> Self {
313        Self::one(
314            union_type
315                .members
316                .iter()
317                .map(|member| Self::from(&member.name)),
318            span(union_type.location()),
319        )
320    }
321}
322
323impl From<&Node<EnumType>> for Shape {
324    fn from(enum_type: &Node<EnumType>) -> Self {
325        Shape::one(
326            enum_type
327                .values
328                .iter()
329                .map(|(name, _)| Shape::string_value(name.as_str(), span(name.location()))),
330            span(enum_type.location()),
331        )
332    }
333}
334
335impl From<&Node<InputObjectType>> for Shape {
336    fn from(input_object_type: &Node<InputObjectType>) -> Self {
337        Self::record(
338            input_object_type
339                .fields
340                .iter()
341                .map(|(name, input)| {
342                    (
343                        name.to_string(),
344                        from_type(input.ty.as_ref(), span(input.location())),
345                    )
346                })
347                .collect(),
348            span(input_object_type.location()),
349        )
350    }
351}
352
353fn span(source_span: Option<SourceSpan>) -> Vec<Location> {
354    source_span.into_iter().map(Location::from).collect()
355}
356
357impl From<SourceSpan> for Location {
358    fn from(span: SourceSpan) -> Self {
359        Location {
360            source_id: SourceId::GraphQL(span.file_id()),
361            span: span.offset()..span.end_offset(),
362        }
363    }
364}