use std::{collections::HashMap, sync::Arc};
use super::{
super::{CompiledSchema, MutationDefinition, QueryDefinition, SubscriptionDefinition},
directive_builder::{build_custom_directives, builtin_directives},
field_resolver::{build_arg_input_value, type_ref},
type_resolver::{
build_enum_type, build_input_object_type, build_interface_type, build_object_type,
build_union_type, builtin_scalars,
},
types::{
IntrospectionField, IntrospectionInputValue, IntrospectionSchema, IntrospectionType,
IntrospectionTypeRef, TypeKind,
},
};
#[must_use = "call .build() to construct the final value"]
pub struct IntrospectionBuilder;
impl IntrospectionBuilder {
#[must_use]
pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
let mut types = Vec::new();
types.extend(builtin_scalars());
for type_def in &schema.types {
types.push(build_object_type(type_def));
}
for enum_def in &schema.enums {
types.push(build_enum_type(enum_def));
}
for input_def in &schema.input_types {
types.push(build_input_object_type(input_def));
}
for interface_def in &schema.interfaces {
types.push(build_interface_type(interface_def, schema));
}
for union_def in &schema.unions {
types.push(build_union_type(union_def));
}
types.push(build_query_type(schema));
if !schema.mutations.is_empty() {
types.push(build_mutation_type(schema));
}
if !schema.subscriptions.is_empty() {
types.push(build_subscription_type(schema));
}
let mut directives = builtin_directives();
directives.extend(build_custom_directives(&schema.directives));
IntrospectionSchema {
description: Some("FraiseQL GraphQL Schema".to_string()),
types,
query_type: IntrospectionTypeRef {
name: "Query".to_string(),
},
mutation_type: if schema.mutations.is_empty() {
None
} else {
Some(IntrospectionTypeRef {
name: "Mutation".to_string(),
})
},
subscription_type: if schema.subscriptions.is_empty() {
None
} else {
Some(IntrospectionTypeRef {
name: "Subscription".to_string(),
})
},
directives,
}
}
#[must_use]
pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
let mut map = HashMap::new();
for t in &schema.types {
if let Some(ref name) = t.name {
map.insert(name.clone(), t.clone());
}
}
map
}
#[must_use]
pub fn type_ref(name: &str) -> IntrospectionType {
type_ref(name)
}
}
fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
let mut fields: Vec<IntrospectionField> =
schema.queries.iter().map(|q| build_query_field(q, schema)).collect();
let has_relay_types =
schema.types.iter().any(|t| t.relay) || schema.interfaces.iter().any(|i| i.name == "Node");
if has_relay_types && !fields.iter().any(|f| f.name == "node") {
fields.push(build_node_query_field());
}
IntrospectionType {
kind: TypeKind::Object,
name: Some("Query".to_string()),
description: Some("Root query type".to_string()),
fields: Some(fields),
interfaces: Some(vec![]),
possible_types: None,
enum_values: None,
input_fields: None,
of_type: None,
specified_by_u_r_l: None,
}
}
fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
let fields: Vec<IntrospectionField> =
schema.mutations.iter().map(|m| build_mutation_field(m, schema)).collect();
IntrospectionType {
kind: TypeKind::Object,
name: Some("Mutation".to_string()),
description: Some("Root mutation type".to_string()),
fields: Some(fields),
interfaces: Some(vec![]),
possible_types: None,
enum_values: None,
input_fields: None,
of_type: None,
specified_by_u_r_l: None,
}
}
fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
let fields: Vec<IntrospectionField> = schema
.subscriptions
.iter()
.map(|s| build_subscription_field(s, schema))
.collect();
IntrospectionType {
kind: TypeKind::Object,
name: Some("Subscription".to_string()),
description: Some("Root subscription type".to_string()),
fields: Some(fields),
interfaces: Some(vec![]),
possible_types: None,
enum_values: None,
input_fields: None,
of_type: None,
specified_by_u_r_l: None,
}
}
fn build_query_field(query: &QueryDefinition, schema: &CompiledSchema) -> IntrospectionField {
if query.relay {
return build_relay_query_field(query, schema);
}
let return_type = type_ref(&query.return_type);
let return_type = if query.returns_list {
IntrospectionType {
kind: TypeKind::List,
name: None,
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: Some(Box::new(return_type)),
specified_by_u_r_l: None,
}
} else {
return_type
};
let return_type = if query.nullable {
return_type
} else {
IntrospectionType {
kind: TypeKind::NonNull,
name: None,
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: Some(Box::new(return_type)),
specified_by_u_r_l: None,
}
};
let args: Vec<IntrospectionInputValue> =
query.graphql_arguments().iter().map(build_arg_input_value).collect();
IntrospectionField {
name: schema.display_name(&query.name),
description: query.description.clone(),
args,
field_type: return_type,
is_deprecated: query.is_deprecated(),
deprecation_reason: query.deprecation_reason().map(ToString::to_string),
}
}
fn build_relay_query_field(query: &QueryDefinition, schema: &CompiledSchema) -> IntrospectionField {
let connection_type = format!("{}Connection", query.return_type);
let return_type = IntrospectionType {
kind: TypeKind::NonNull,
name: None,
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: Some(Box::new(type_ref(&connection_type))),
specified_by_u_r_l: None,
};
let nullable_int = || IntrospectionType {
kind: TypeKind::Scalar,
name: Some("Int".to_string()),
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: None,
specified_by_u_r_l: None,
};
let nullable_string = || IntrospectionType {
kind: TypeKind::Scalar,
name: Some("String".to_string()),
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: None,
specified_by_u_r_l: None,
};
let relay_args = vec![
IntrospectionInputValue {
name: "first".to_string(),
description: Some("Return the first N items.".to_string()),
input_type: nullable_int(),
default_value: None,
is_deprecated: false,
deprecation_reason: None,
validation_rules: vec![],
},
IntrospectionInputValue {
name: "after".to_string(),
description: Some("Cursor: return items after this position.".to_string()),
input_type: nullable_string(),
default_value: None,
is_deprecated: false,
deprecation_reason: None,
validation_rules: vec![],
},
IntrospectionInputValue {
name: "last".to_string(),
description: Some("Return the last N items.".to_string()),
input_type: nullable_int(),
default_value: None,
is_deprecated: false,
deprecation_reason: None,
validation_rules: vec![],
},
IntrospectionInputValue {
name: "before".to_string(),
description: Some("Cursor: return items before this position.".to_string()),
input_type: nullable_string(),
default_value: None,
is_deprecated: false,
deprecation_reason: None,
validation_rules: vec![],
},
];
IntrospectionField {
name: schema.display_name(&query.name),
description: query.description.clone(),
args: relay_args,
field_type: return_type,
is_deprecated: query.is_deprecated(),
deprecation_reason: query.deprecation_reason().map(ToString::to_string),
}
}
fn build_node_query_field() -> IntrospectionField {
let return_type = IntrospectionType {
kind: TypeKind::Interface,
name: Some("Node".to_string()),
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: None,
specified_by_u_r_l: None,
};
let id_arg = IntrospectionInputValue {
name: "id".to_string(),
description: Some("Globally unique opaque identifier.".to_string()),
input_type: IntrospectionType {
kind: TypeKind::NonNull,
name: None,
description: None,
fields: None,
interfaces: None,
possible_types: None,
enum_values: None,
input_fields: None,
of_type: Some(Box::new(type_ref("ID"))),
specified_by_u_r_l: None,
},
default_value: None,
is_deprecated: false,
deprecation_reason: None,
validation_rules: vec![],
};
IntrospectionField {
name: "node".to_string(),
description: Some(
"Fetch any object that implements the Node interface by its global ID.".to_string(),
),
args: vec![id_arg],
field_type: return_type,
is_deprecated: false,
deprecation_reason: None,
}
}
fn build_mutation_field(
mutation: &MutationDefinition,
schema: &CompiledSchema,
) -> IntrospectionField {
let return_type = type_ref(&mutation.return_type);
let args: Vec<IntrospectionInputValue> =
mutation.arguments.iter().map(build_arg_input_value).collect();
IntrospectionField {
name: schema.display_name(&mutation.name),
description: mutation.description.clone(),
args,
field_type: return_type,
is_deprecated: mutation.is_deprecated(),
deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
}
}
fn build_subscription_field(
subscription: &SubscriptionDefinition,
schema: &CompiledSchema,
) -> IntrospectionField {
let return_type = type_ref(&subscription.return_type);
let args: Vec<IntrospectionInputValue> =
subscription.arguments.iter().map(build_arg_input_value).collect();
IntrospectionField {
name: schema.display_name(&subscription.name),
description: subscription.description.clone(),
args,
field_type: return_type,
is_deprecated: subscription.is_deprecated(),
deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionResponses {
pub schema_response: Arc<serde_json::Value>,
pub type_responses: HashMap<String, Arc<serde_json::Value>>,
}
impl IntrospectionResponses {
#[must_use]
pub fn build(schema: &CompiledSchema) -> Self {
let introspection = IntrospectionBuilder::build(schema);
let type_map = IntrospectionBuilder::build_type_map(&introspection);
let schema_response = Arc::new(serde_json::json!({
"data": {
"__schema": introspection
}
}));
let mut type_responses = HashMap::new();
for (name, t) in type_map {
let response = Arc::new(serde_json::json!({
"data": {
"__type": t
}
}));
type_responses.insert(name, response);
}
Self {
schema_response,
type_responses,
}
}
pub fn filter_inaccessible(&mut self, inaccessible: &HashMap<String, Vec<String>>) {
if inaccessible.is_empty() {
return;
}
for (type_name, field_names) in inaccessible {
if let Some(response) = self.type_responses.get_mut(type_name) {
let mut val = response.as_ref().clone();
if let Some(fields) =
val.pointer_mut("/data/__type/fields").and_then(|v| v.as_array_mut())
{
fields.retain(|f| {
f.get("name")
.and_then(|n| n.as_str())
.is_none_or(|name| !field_names.contains(&name.to_string()))
});
}
*response = Arc::new(val);
}
}
let mut schema_val = self.schema_response.as_ref().clone();
if let Some(types) =
schema_val.pointer_mut("/data/__schema/types").and_then(|v| v.as_array_mut())
{
for type_val in types.iter_mut() {
let type_name = type_val.get("name").and_then(|n| n.as_str()).unwrap_or("");
if let Some(field_names) = inaccessible.get(type_name) {
if let Some(fields) = type_val.get_mut("fields").and_then(|v| v.as_array_mut())
{
fields.retain(|f| {
f.get("name")
.and_then(|n| n.as_str())
.is_none_or(|name| !field_names.contains(&name.to_string()))
});
}
}
}
}
self.schema_response = Arc::new(schema_val);
}
#[must_use]
pub fn get_type_response(&self, type_name: &str) -> serde_json::Value {
self.type_responses.get(type_name).map_or_else(
|| {
serde_json::json!({
"data": {
"__type": null
}
})
},
|v| v.as_ref().clone(),
)
}
}