1use std::{collections::HashMap, sync::Arc};
7
8use super::{
9 super::{CompiledSchema, MutationDefinition, QueryDefinition, SubscriptionDefinition},
10 directive_builder::{build_custom_directives, builtin_directives},
11 field_resolver::{build_arg_input_value, type_ref},
12 type_resolver::{
13 build_enum_type, build_input_object_type, build_interface_type, build_object_type,
14 build_union_type, builtin_scalars,
15 },
16 types::{
17 IntrospectionField, IntrospectionInputValue, IntrospectionSchema, IntrospectionType,
18 IntrospectionTypeRef, TypeKind,
19 },
20};
21
22#[must_use = "call .build() to construct the final value"]
28pub struct IntrospectionBuilder;
29
30impl IntrospectionBuilder {
31 #[must_use]
33 pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
34 let mut types = Vec::new();
35
36 types.extend(builtin_scalars());
38
39 for type_def in &schema.types {
41 types.push(build_object_type(type_def));
42 }
43
44 for enum_def in &schema.enums {
46 types.push(build_enum_type(enum_def));
47 }
48
49 for input_def in &schema.input_types {
51 types.push(build_input_object_type(input_def));
52 }
53
54 for interface_def in &schema.interfaces {
56 types.push(build_interface_type(interface_def, schema));
57 }
58
59 for union_def in &schema.unions {
61 types.push(build_union_type(union_def));
62 }
63
64 types.push(build_query_type(schema));
66
67 if !schema.mutations.is_empty() {
69 types.push(build_mutation_type(schema));
70 }
71
72 if !schema.subscriptions.is_empty() {
74 types.push(build_subscription_type(schema));
75 }
76
77 let mut directives = builtin_directives();
79 directives.extend(build_custom_directives(&schema.directives));
80
81 IntrospectionSchema {
82 description: Some("FraiseQL GraphQL Schema".to_string()),
83 types,
84 query_type: IntrospectionTypeRef {
85 name: "Query".to_string(),
86 },
87 mutation_type: if schema.mutations.is_empty() {
88 None
89 } else {
90 Some(IntrospectionTypeRef {
91 name: "Mutation".to_string(),
92 })
93 },
94 subscription_type: if schema.subscriptions.is_empty() {
95 None
96 } else {
97 Some(IntrospectionTypeRef {
98 name: "Subscription".to_string(),
99 })
100 },
101 directives,
102 }
103 }
104
105 #[must_use]
107 pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
108 let mut map = HashMap::new();
109 for t in &schema.types {
110 if let Some(ref name) = t.name {
111 map.insert(name.clone(), t.clone());
112 }
113 }
114 map
115 }
116
117 #[must_use]
119 pub fn type_ref(name: &str) -> IntrospectionType {
120 type_ref(name)
121 }
122}
123
124fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
130 let mut fields: Vec<IntrospectionField> =
131 schema.queries.iter().map(|q| build_query_field(q, schema)).collect();
132
133 let has_relay_types =
135 schema.types.iter().any(|t| t.relay) || schema.interfaces.iter().any(|i| i.name == "Node");
136 if has_relay_types && !fields.iter().any(|f| f.name == "node") {
137 fields.push(build_node_query_field());
138 }
139
140 IntrospectionType {
141 kind: TypeKind::Object,
142 name: Some("Query".to_string()),
143 description: Some("Root query type".to_string()),
144 fields: Some(fields),
145 interfaces: Some(vec![]),
146 possible_types: None,
147 enum_values: None,
148 input_fields: None,
149 of_type: None,
150 specified_by_u_r_l: None,
151 }
152}
153
154fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
156 let fields: Vec<IntrospectionField> =
157 schema.mutations.iter().map(|m| build_mutation_field(m, schema)).collect();
158
159 IntrospectionType {
160 kind: TypeKind::Object,
161 name: Some("Mutation".to_string()),
162 description: Some("Root mutation type".to_string()),
163 fields: Some(fields),
164 interfaces: Some(vec![]),
165 possible_types: None,
166 enum_values: None,
167 input_fields: None,
168 of_type: None,
169 specified_by_u_r_l: None,
170 }
171}
172
173fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
175 let fields: Vec<IntrospectionField> = schema
176 .subscriptions
177 .iter()
178 .map(|s| build_subscription_field(s, schema))
179 .collect();
180
181 IntrospectionType {
182 kind: TypeKind::Object,
183 name: Some("Subscription".to_string()),
184 description: Some("Root subscription type".to_string()),
185 fields: Some(fields),
186 interfaces: Some(vec![]),
187 possible_types: None,
188 enum_values: None,
189 input_fields: None,
190 of_type: None,
191 specified_by_u_r_l: None,
192 }
193}
194
195fn build_query_field(query: &QueryDefinition, schema: &CompiledSchema) -> IntrospectionField {
201 if query.relay {
204 return build_relay_query_field(query, schema);
205 }
206
207 let return_type = type_ref(&query.return_type);
208 let return_type = if query.returns_list {
209 IntrospectionType {
210 kind: TypeKind::List,
211 name: None,
212 description: None,
213 fields: None,
214 interfaces: None,
215 possible_types: None,
216 enum_values: None,
217 input_fields: None,
218 of_type: Some(Box::new(return_type)),
219 specified_by_u_r_l: None,
220 }
221 } else {
222 return_type
223 };
224
225 let return_type = if query.nullable {
226 return_type
227 } else {
228 IntrospectionType {
229 kind: TypeKind::NonNull,
230 name: None,
231 description: None,
232 fields: None,
233 interfaces: None,
234 possible_types: None,
235 enum_values: None,
236 input_fields: None,
237 of_type: Some(Box::new(return_type)),
238 specified_by_u_r_l: None,
239 }
240 };
241
242 let args: Vec<IntrospectionInputValue> =
244 query.arguments.iter().map(build_arg_input_value).collect();
245
246 IntrospectionField {
247 name: schema.display_name(&query.name),
248 description: query.description.clone(),
249 args,
250 field_type: return_type,
251 is_deprecated: query.is_deprecated(),
252 deprecation_reason: query.deprecation_reason().map(ToString::to_string),
253 }
254}
255
256fn build_relay_query_field(query: &QueryDefinition, schema: &CompiledSchema) -> IntrospectionField {
263 let connection_type = format!("{}Connection", query.return_type);
264
265 let return_type = IntrospectionType {
267 kind: TypeKind::NonNull,
268 name: None,
269 description: None,
270 fields: None,
271 interfaces: None,
272 possible_types: None,
273 enum_values: None,
274 input_fields: None,
275 of_type: Some(Box::new(type_ref(&connection_type))),
276 specified_by_u_r_l: None,
277 };
278
279 let nullable_int = || IntrospectionType {
281 kind: TypeKind::Scalar,
282 name: Some("Int".to_string()),
283 description: None,
284 fields: None,
285 interfaces: None,
286 possible_types: None,
287 enum_values: None,
288 input_fields: None,
289 of_type: None,
290 specified_by_u_r_l: None,
291 };
292 let nullable_string = || IntrospectionType {
293 kind: TypeKind::Scalar,
294 name: Some("String".to_string()),
295 description: None,
296 fields: None,
297 interfaces: None,
298 possible_types: None,
299 enum_values: None,
300 input_fields: None,
301 of_type: None,
302 specified_by_u_r_l: None,
303 };
304 let relay_args = vec![
305 IntrospectionInputValue {
306 name: "first".to_string(),
307 description: Some("Return the first N items.".to_string()),
308 input_type: nullable_int(),
309 default_value: None,
310 is_deprecated: false,
311 deprecation_reason: None,
312 validation_rules: vec![],
313 },
314 IntrospectionInputValue {
315 name: "after".to_string(),
316 description: Some("Cursor: return items after this position.".to_string()),
317 input_type: nullable_string(),
318 default_value: None,
319 is_deprecated: false,
320 deprecation_reason: None,
321 validation_rules: vec![],
322 },
323 IntrospectionInputValue {
324 name: "last".to_string(),
325 description: Some("Return the last N items.".to_string()),
326 input_type: nullable_int(),
327 default_value: None,
328 is_deprecated: false,
329 deprecation_reason: None,
330 validation_rules: vec![],
331 },
332 IntrospectionInputValue {
333 name: "before".to_string(),
334 description: Some("Cursor: return items before this position.".to_string()),
335 input_type: nullable_string(),
336 default_value: None,
337 is_deprecated: false,
338 deprecation_reason: None,
339 validation_rules: vec![],
340 },
341 ];
342
343 IntrospectionField {
344 name: schema.display_name(&query.name),
345 description: query.description.clone(),
346 args: relay_args,
347 field_type: return_type,
348 is_deprecated: query.is_deprecated(),
349 deprecation_reason: query.deprecation_reason().map(ToString::to_string),
350 }
351}
352
353fn build_node_query_field() -> IntrospectionField {
357 let return_type = IntrospectionType {
361 kind: TypeKind::Interface,
362 name: Some("Node".to_string()),
363 description: None,
364 fields: None,
365 interfaces: None,
366 possible_types: None,
367 enum_values: None,
368 input_fields: None,
369 of_type: None,
370 specified_by_u_r_l: None,
371 };
372
373 let id_arg = IntrospectionInputValue {
375 name: "id".to_string(),
376 description: Some("Globally unique opaque identifier.".to_string()),
377 input_type: IntrospectionType {
378 kind: TypeKind::NonNull,
379 name: None,
380 description: None,
381 fields: None,
382 interfaces: None,
383 possible_types: None,
384 enum_values: None,
385 input_fields: None,
386 of_type: Some(Box::new(type_ref("ID"))),
387 specified_by_u_r_l: None,
388 },
389 default_value: None,
390 is_deprecated: false,
391 deprecation_reason: None,
392 validation_rules: vec![],
393 };
394
395 IntrospectionField {
396 name: "node".to_string(),
397 description: Some(
398 "Fetch any object that implements the Node interface by its global ID.".to_string(),
399 ),
400 args: vec![id_arg],
401 field_type: return_type,
402 is_deprecated: false,
403 deprecation_reason: None,
404 }
405}
406
407fn build_mutation_field(
409 mutation: &MutationDefinition,
410 schema: &CompiledSchema,
411) -> IntrospectionField {
412 let return_type = type_ref(&mutation.return_type);
414
415 let args: Vec<IntrospectionInputValue> =
417 mutation.arguments.iter().map(build_arg_input_value).collect();
418
419 IntrospectionField {
420 name: schema.display_name(&mutation.name),
421 description: mutation.description.clone(),
422 args,
423 field_type: return_type,
424 is_deprecated: mutation.is_deprecated(),
425 deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
426 }
427}
428
429fn build_subscription_field(
431 subscription: &SubscriptionDefinition,
432 schema: &CompiledSchema,
433) -> IntrospectionField {
434 let return_type = type_ref(&subscription.return_type);
436
437 let args: Vec<IntrospectionInputValue> =
439 subscription.arguments.iter().map(build_arg_input_value).collect();
440
441 IntrospectionField {
442 name: schema.display_name(&subscription.name),
443 description: subscription.description.clone(),
444 args,
445 field_type: return_type,
446 is_deprecated: subscription.is_deprecated(),
447 deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
448 }
449}
450
451#[derive(Debug, Clone)]
460pub struct IntrospectionResponses {
461 pub schema_response: Arc<serde_json::Value>,
463 pub type_responses: HashMap<String, Arc<serde_json::Value>>,
465}
466
467impl IntrospectionResponses {
468 pub fn build(schema: &CompiledSchema) -> Self {
472 let introspection = IntrospectionBuilder::build(schema);
473 let type_map = IntrospectionBuilder::build_type_map(&introspection);
474
475 let schema_response = Arc::new(serde_json::json!({
477 "data": {
478 "__schema": introspection
479 }
480 }));
481
482 let mut type_responses = HashMap::new();
484 for (name, t) in type_map {
485 let response = Arc::new(serde_json::json!({
486 "data": {
487 "__type": t
488 }
489 }));
490 type_responses.insert(name, response);
491 }
492
493 Self {
494 schema_response,
495 type_responses,
496 }
497 }
498
499 #[must_use]
501 pub fn get_type_response(&self, type_name: &str) -> serde_json::Value {
502 self.type_responses.get(type_name).map_or_else(
503 || {
504 serde_json::json!({
505 "data": {
506 "__type": null
507 }
508 })
509 },
510 |v| v.as_ref().clone(),
511 )
512 }
513}