grafbase_sdk_mock/
builder.rs

1#![allow(clippy::panic)]
2
3use std::{collections::HashMap, path::Path};
4
5use async_graphql::{
6    dynamic::{FieldValue, ResolverContext},
7    ServerError,
8};
9use cynic_parser::{common::WrappingType, type_system as parser};
10use serde::Deserialize;
11
12use crate::ExtensionOnlySubgraph;
13
14use super::{
15    entity_resolver::{EntityResolver, EntityResolverContext},
16    resolver::Resolver,
17    DynamicSchema, DynamicSubgraph,
18};
19
20type ResolverMap = HashMap<(String, String), Box<dyn Resolver>>;
21type EntityResolverMap = HashMap<String, Box<dyn EntityResolver>>;
22
23/// A builder for dynamic GraphQL schemas.
24pub struct DynamicSchemaBuilder {
25    sdl: String,
26    field_resolvers: ResolverMap,
27    entity_resolvers: EntityResolverMap,
28}
29
30impl DynamicSchemaBuilder {
31    pub(crate) fn new(sdl: &str) -> Self {
32        DynamicSchemaBuilder {
33            sdl: sdl.into(),
34            field_resolvers: Default::default(),
35            entity_resolvers: Default::default(),
36        }
37    }
38
39    /// Adds a field resolver to this schema.
40    ///
41    /// # Arguments
42    /// * `ty` - The name of the type that contains the field
43    /// * `field` - The name of the field to resolve
44    /// * `resolver` - A resolver implementation for the field
45    pub fn with_resolver(mut self, ty: &str, field: &str, resolver: impl Resolver + 'static) -> Self {
46        self.field_resolvers
47            .insert((ty.into(), field.into()), Box::new(resolver));
48        self
49    }
50
51    /// Adds an entity resolver to this schema.
52    ///
53    /// # Arguments
54    /// * `entity` - The name of the entity type to resolve
55    /// * `resolver` - A resolver implementation for the entity
56    pub fn with_entity_resolver(mut self, entity: &str, resolver: impl EntityResolver + 'static) -> Self {
57        self.entity_resolvers.insert(entity.into(), Box::new(resolver));
58        self
59    }
60
61    /// Converts this schema builder into a dynamic subgraph with the given name.
62    ///
63    /// # Arguments
64    /// * `name` - The name to give this subgraph
65    ///
66    /// # Returns
67    /// A `DynamicSubgraph` that can be used in a federated schema
68    pub fn into_subgraph(self, name: &str) -> anyhow::Result<DynamicSubgraph> {
69        Ok(DynamicSubgraph {
70            name: name.into(),
71            schema: self.finish(),
72        })
73    }
74
75    /// Converts this schema builder into an extension-only subgraph.
76    ///
77    /// # Arguments
78    /// * `name` - The name to give this subgraph
79    /// * `extension_path` - Path to the extension directory with wasm and manifest files
80    ///
81    /// # Returns
82    /// An `ExtensionOnlySubgraph` that can be used in a federated schema
83    pub fn into_extension_only_subgraph(
84        self,
85        name: &str,
86        extension_path: &Path,
87    ) -> anyhow::Result<ExtensionOnlySubgraph> {
88        Ok(ExtensionOnlySubgraph {
89            name: name.into(),
90            schema: self.finish(),
91            extension_path: extension_path.to_path_buf(),
92        })
93    }
94
95    fn finish(self) -> DynamicSchema {
96        let Self {
97            sdl,
98            mut field_resolvers,
99            entity_resolvers,
100        } = self;
101
102        let schema = cynic_parser::parse_type_system_document(&sdl)
103            .map_err(|e| e.to_report(&sdl))
104            .expect("a valid document");
105
106        let (query_type, ..) = root_types(&schema);
107
108        // Note: don't enable federation on this because we want to provide all that stuff ourselves
109        let mut builder = schema_builder(&schema);
110        builder = builder.register(service_type(&sdl));
111        let entities = find_entities(&schema);
112
113        if !entities.is_empty() {
114            builder = builder.register(any_type());
115            builder = builder.register(entity_type(&entities));
116        }
117
118        let mut entity_resolvers = Some(entity_resolvers);
119        for definition in schema.definitions() {
120            match definition {
121                parser::Definition::Type(def) => {
122                    let mut ty = convert_type(def, &mut field_resolvers);
123                    if def.name() == query_type {
124                        if let Some(entity_resolvers) = entity_resolvers.take() {
125                            ty = add_federation_fields(ty, &entities, entity_resolvers);
126                        }
127                    }
128                    builder = builder.register(ty);
129                }
130                parser::Definition::TypeExtension(_) => {
131                    unimplemented!("this is just for tests, extensions aren't supported")
132                }
133                _ => {}
134            }
135        }
136
137        if entity_resolvers.is_some() && !entities.is_empty() {
138            let entity_resolvers = entity_resolvers.unwrap();
139            builder = builder.register(add_federation_fields(
140                async_graphql::dynamic::Object::new("Query").into(),
141                &entities,
142                entity_resolvers,
143            ));
144        }
145
146        let schema = builder.finish().unwrap();
147
148        DynamicSchema { schema, sdl }
149    }
150}
151
152fn find_entities(schema: &parser::TypeSystemDocument) -> Vec<&str> {
153    schema
154        .definitions()
155        .filter_map(|def| match def {
156            parser::Definition::Type(def) => Some(def),
157            parser::Definition::TypeExtension(def) => Some(def),
158            _ => None,
159        })
160        .filter(|def| def.directives().any(|directive| directive.name() == "key"))
161        .map(|def| def.name())
162        .collect()
163}
164
165fn convert_type(def: parser::TypeDefinition<'_>, resolvers: &mut ResolverMap) -> async_graphql::dynamic::Type {
166    match def {
167        parser::TypeDefinition::Scalar(def) => async_graphql::dynamic::Scalar::new(def.name()).into(),
168        parser::TypeDefinition::Object(def) => convert_object(def, resolvers),
169        parser::TypeDefinition::Interface(def) => convert_iface(def),
170        parser::TypeDefinition::Union(def) => convert_union(def),
171        parser::TypeDefinition::Enum(def) => convert_enum(def),
172        parser::TypeDefinition::InputObject(def) => convert_input_object(def),
173    }
174}
175
176fn convert_object(def: parser::ObjectDefinition<'_>, resolvers: &mut ResolverMap) -> async_graphql::dynamic::Type {
177    use async_graphql::dynamic::*;
178
179    let mut object = Object::new(def.name());
180
181    for name in def.implements_interfaces() {
182        object = object.implement(name);
183    }
184
185    for field_def in def.fields() {
186        let type_ref = convert_type_ref(field_def.ty());
187        let resolver = std::sync::Mutex::new(
188            resolvers
189                .remove(&(def.name().into(), field_def.name().into()))
190                .unwrap_or_else(|| Box::new(default_field_resolver(field_def.name()))),
191        );
192
193        let mut field = Field::new(field_def.name(), type_ref, move |context| {
194            let mut resolver = resolver.lock().expect("mutex to be unpoisoned");
195            FieldFuture::Value(resolver.resolve(context).map(|value| {
196                let value = async_graphql::Value::deserialize(value).unwrap();
197                transform_into_field_value(value)
198            }))
199        });
200
201        for argument in field_def.arguments() {
202            field = field.argument(convert_input_value(argument));
203        }
204
205        object = object.field(field);
206    }
207
208    object.into()
209}
210
211fn transform_into_field_value(mut value: async_graphql::Value) -> FieldValue<'static> {
212    match value {
213        async_graphql::Value::Object(ref mut fields) => {
214            if let Some(async_graphql::Value::String(ty)) = fields.swap_remove("__typename") {
215                FieldValue::from(value).with_type(ty)
216            } else {
217                FieldValue::from(value)
218            }
219        }
220        async_graphql::Value::List(values) => FieldValue::list(values.into_iter().map(transform_into_field_value)),
221        value => FieldValue::from(value),
222    }
223}
224
225fn convert_iface(def: parser::InterfaceDefinition<'_>) -> async_graphql::dynamic::Type {
226    use async_graphql::dynamic::*;
227    let mut interface = Interface::new(def.name());
228
229    for field_def in def.fields() {
230        let type_ref = convert_type_ref(field_def.ty());
231
232        let mut field = InterfaceField::new(field_def.name(), type_ref);
233
234        for argument in field_def.arguments() {
235            field = field.argument(convert_input_value(argument));
236        }
237
238        interface = interface.field(field);
239    }
240
241    interface.into()
242}
243
244fn convert_union(def: parser::UnionDefinition<'_>) -> async_graphql::dynamic::Type {
245    use async_graphql::dynamic::*;
246
247    let mut output = Union::new(def.name());
248
249    for member in def.members() {
250        output = output.possible_type(member.name());
251    }
252
253    output.into()
254}
255
256fn convert_enum(def: parser::EnumDefinition<'_>) -> async_graphql::dynamic::Type {
257    use async_graphql::dynamic::*;
258
259    Enum::new(def.name())
260        .items(def.values().map(|value| EnumItem::new(value.value())))
261        .into()
262}
263
264fn convert_input_object(def: parser::InputObjectDefinition<'_>) -> async_graphql::dynamic::Type {
265    use async_graphql::dynamic::*;
266
267    let mut object = InputObject::new(def.name());
268
269    for field_def in def.fields() {
270        object = object.field(convert_input_value(field_def))
271    }
272
273    object.into()
274}
275
276fn convert_type_ref(ty: parser::Type<'_>) -> async_graphql::dynamic::TypeRef {
277    use async_graphql::dynamic::TypeRef;
278
279    let mut output = TypeRef::named(ty.name());
280
281    for wrapper in ty.wrappers() {
282        match wrapper {
283            WrappingType::NonNull => {
284                output = TypeRef::NonNull(Box::new(output));
285            }
286            WrappingType::List => {
287                output = TypeRef::List(Box::new(output));
288            }
289        }
290    }
291
292    output
293}
294
295fn convert_input_value(value_def: parser::InputValueDefinition<'_>) -> async_graphql::dynamic::InputValue {
296    use async_graphql::dynamic::InputValue;
297
298    let mut value = InputValue::new(value_def.name(), convert_type_ref(value_def.ty()));
299
300    if let Some(default) = value_def.default_value() {
301        value = value.default_value(convert_value(default))
302    }
303
304    value
305}
306
307fn convert_value(value: cynic_parser::ConstValue<'_>) -> async_graphql::Value {
308    match value {
309        cynic_parser::ConstValue::Int(inner) => async_graphql::Value::Number(inner.as_i64().into()),
310        cynic_parser::ConstValue::Float(inner) => {
311            async_graphql::Value::Number(serde_json::Number::from_f64(inner.as_f64()).unwrap())
312        }
313        cynic_parser::ConstValue::String(inner) => async_graphql::Value::String(inner.as_str().into()),
314        cynic_parser::ConstValue::Boolean(inner) => async_graphql::Value::Boolean(inner.as_bool()),
315        cynic_parser::ConstValue::Null(_) => async_graphql::Value::Null,
316        cynic_parser::ConstValue::Enum(inner) => async_graphql::Value::Enum(async_graphql::Name::new(inner.name())),
317        cynic_parser::ConstValue::List(inner) => async_graphql::Value::List(inner.items().map(convert_value).collect()),
318        cynic_parser::ConstValue::Object(inner) => async_graphql::Value::Object(
319            inner
320                .fields()
321                .map(|field| (async_graphql::Name::new(field.name()), convert_value(field.value())))
322                .collect(),
323        ),
324    }
325}
326
327fn schema_builder(schema: &cynic_parser::TypeSystemDocument) -> async_graphql::dynamic::SchemaBuilder {
328    let (query_name, mutation_name, subscription_name) = root_types(schema);
329    async_graphql::dynamic::Schema::build(query_name, mutation_name, subscription_name)
330}
331
332fn root_types(schema: &cynic_parser::TypeSystemDocument) -> (&str, Option<&str>, Option<&str>) {
333    use parser::Definition;
334
335    let mut query_name = "Query";
336    let mut mutation_name = None;
337    let mut subscription_name = None;
338    let mut found_schema_def = false;
339    let mut mutation_present = false;
340    let mut subscription_present = false;
341    for definition in schema.definitions() {
342        if let Definition::Schema(_) = definition {
343            found_schema_def = true;
344        }
345        match definition {
346            Definition::Schema(schema) | Definition::SchemaExtension(schema) => {
347                if let Some(def) = schema.query_type() {
348                    query_name = def.named_type();
349                }
350                if let Some(def) = schema.mutation_type() {
351                    mutation_name = Some(def.named_type());
352                }
353                if let Some(def) = schema.subscription_type() {
354                    subscription_name = Some(def.named_type());
355                }
356            }
357            Definition::Type(ty) | Definition::TypeExtension(ty) if ty.name() == "Mutation" => mutation_present = true,
358            Definition::Type(ty) | Definition::TypeExtension(ty) if ty.name() == "Subscription" => {
359                subscription_present = true
360            }
361            _ => {}
362        }
363    }
364    if !found_schema_def {
365        if mutation_present {
366            mutation_name = Some("Mutation");
367        }
368        if subscription_present {
369            mutation_name = Some("Subscription");
370        }
371    }
372
373    (query_name, mutation_name, subscription_name)
374}
375
376fn default_field_resolver(field_name: &str) -> impl Resolver {
377    let field_name = async_graphql::Name::new(field_name);
378
379    move |context: ResolverContext<'_>| {
380        if let Some(value) = context.parent_value.as_value() {
381            return match value {
382                async_graphql::Value::Object(map) => {
383                    map.get(&field_name).map(|value| value.clone().into_json().unwrap())
384                }
385                _ => None,
386            };
387        }
388        panic!("Unexpected parent value for tests: {:?}", context.parent_value)
389    }
390}
391
392fn service_type(sdl: &str) -> async_graphql::dynamic::Type {
393    use async_graphql::dynamic::*;
394    let mut object = Object::new("_Service");
395
396    let sdl = sdl.to_string();
397
398    object = object.field(Field::new("sdl", TypeRef::named_nn("String"), move |_| {
399        FieldFuture::from_value(Some(async_graphql::Value::String(sdl.clone())))
400    }));
401
402    object.into()
403}
404
405fn entity_type(entities: &[&str]) -> async_graphql::dynamic::Type {
406    use async_graphql::dynamic::*;
407
408    let mut ty = Union::new("_Entity");
409
410    for entity in entities {
411        ty = ty.possible_type(*entity);
412    }
413
414    ty.into()
415}
416
417fn any_type() -> async_graphql::dynamic::Type {
418    use async_graphql::dynamic::*;
419
420    Scalar::new("_Any").into()
421}
422
423fn add_federation_fields(
424    query_ty: async_graphql::dynamic::Type,
425    entities: &[&str],
426    entity_resolvers: EntityResolverMap,
427) -> async_graphql::dynamic::Type {
428    use async_graphql::dynamic::*;
429
430    let async_graphql::dynamic::Type::Object(mut obj) = query_ty else {
431        panic!("this shouldn't happen probably")
432    };
433    obj = obj.field(Field::new("_service", TypeRef::named_nn("_Service"), |_| {
434        // Doesnt matter what we return here hopefully?
435        FieldFuture::from_value(Some(async_graphql::Value::Object([].into())))
436    }));
437
438    for entity in entity_resolvers.keys() {
439        if !entities.contains(&entity.as_str()) {
440            panic!("Tried to add an resolver for {entity}, but this entity doesnt exist");
441        }
442    }
443
444    if !entities.is_empty() {
445        let resolvers = std::sync::Mutex::new(entity_resolvers);
446
447        let entity_field = Field::new("_entities", TypeRef::named_list_nn("_Entity"), move |context| {
448            let mut resolvers = resolvers.lock().expect("mutex to be unpoisoned");
449            let representations = context
450                .args
451                .get("representations")
452                .expect("_entities needs representations");
453
454            let reprs = representations
455                .deserialize::<Vec<serde_json::Value>>()
456                .expect("representations to be a list of objects");
457
458            let entities = reprs.into_iter().map(|repr| {
459                let context = EntityResolverContext::new(&context, repr);
460
461                let Some(resolver) = resolvers.get_mut(&context.typename) else {
462                    context.add_error(ServerError::new(format!("{} has no resolver", context.typename), None));
463                    return FieldValue::value(async_graphql::Value::Null);
464                };
465
466                let json_value = resolver.resolve(context).unwrap_or_default();
467
468                transform_into_field_value(async_graphql::Value::deserialize(json_value).unwrap())
469            });
470
471            FieldFuture::Value(Some(FieldValue::list(entities)))
472        })
473        .argument(InputValue::new("representations", TypeRef::named_nn_list_nn("_Any")));
474
475        obj = obj.field(entity_field);
476    }
477
478    obj.into()
479}