use serde_json::{json, Value as JsonValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeKind {
Scalar,
Object,
Interface,
Union,
Enum,
InputObject,
List,
NonNull,
}
impl TypeKind {
fn as_str(&self) -> &'static str {
match self {
TypeKind::Scalar => "SCALAR",
TypeKind::Object => "OBJECT",
TypeKind::Interface => "INTERFACE",
TypeKind::Union => "UNION",
TypeKind::Enum => "ENUM",
TypeKind::InputObject => "INPUT_OBJECT",
TypeKind::List => "LIST",
TypeKind::NonNull => "NON_NULL",
}
}
}
#[derive(Debug, Clone)]
pub struct TypeRef {
pub kind: TypeKind,
pub name: Option<String>,
pub of_type: Option<Box<TypeRef>>,
}
impl TypeRef {
pub fn named(kind: TypeKind, name: impl Into<String>) -> Self {
TypeRef {
kind,
name: Some(name.into()),
of_type: None,
}
}
pub fn non_null(inner: TypeRef) -> Self {
TypeRef {
kind: TypeKind::NonNull,
name: None,
of_type: Some(Box::new(inner)),
}
}
pub fn list(inner: TypeRef) -> Self {
TypeRef {
kind: TypeKind::List,
name: None,
of_type: Some(Box::new(inner)),
}
}
fn to_json(&self) -> JsonValue {
let of_type = self
.of_type
.as_ref()
.map(|t| t.to_json())
.unwrap_or(JsonValue::Null);
json!({
"kind": self.kind.as_str(),
"name": self.name,
"ofType": of_type,
})
}
}
#[derive(Debug, Clone)]
pub struct InputValue {
pub name: String,
pub type_ref: TypeRef,
pub default_value: Option<String>,
}
impl InputValue {
pub fn new(name: impl Into<String>, type_ref: TypeRef) -> Self {
InputValue {
name: name.into(),
type_ref,
default_value: None,
}
}
fn to_json(&self) -> JsonValue {
json!({
"name": self.name,
"type": self.type_ref.to_json(),
"defaultValue": self.default_value,
})
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionField {
pub name: String,
pub type_ref: TypeRef,
pub args: Vec<InputValue>,
pub description: Option<String>,
pub is_deprecated: bool,
pub deprecation_reason: Option<String>,
}
impl IntrospectionField {
pub fn new(name: impl Into<String>, type_ref: TypeRef) -> Self {
IntrospectionField {
name: name.into(),
type_ref,
args: Vec::new(),
description: None,
is_deprecated: false,
deprecation_reason: None,
}
}
pub fn deprecated(mut self, reason: impl Into<String>) -> Self {
self.is_deprecated = true;
self.deprecation_reason = Some(reason.into());
self
}
fn to_json(&self) -> JsonValue {
json!({
"name": self.name,
"type": self.type_ref.to_json(),
"args": self.args.iter().map(InputValue::to_json).collect::<Vec<_>>(),
"description": self.description,
"isDeprecated": self.is_deprecated,
"deprecationReason": self.deprecation_reason,
})
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionType {
pub kind: TypeKind,
pub name: Option<String>,
pub description: Option<String>,
pub fields: Vec<IntrospectionField>,
pub enum_values: Vec<String>,
pub possible_types: Vec<String>,
pub input_fields: Vec<InputValue>,
}
impl IntrospectionType {
pub fn scalar(name: impl Into<String>) -> Self {
IntrospectionType {
kind: TypeKind::Scalar,
name: Some(name.into()),
description: None,
fields: Vec::new(),
enum_values: Vec::new(),
possible_types: Vec::new(),
input_fields: Vec::new(),
}
}
pub fn object(name: impl Into<String>, fields: Vec<IntrospectionField>) -> Self {
IntrospectionType {
kind: TypeKind::Object,
name: Some(name.into()),
description: None,
fields,
enum_values: Vec::new(),
possible_types: Vec::new(),
input_fields: Vec::new(),
}
}
pub fn enum_type(name: impl Into<String>, values: Vec<String>) -> Self {
IntrospectionType {
kind: TypeKind::Enum,
name: Some(name.into()),
description: None,
fields: Vec::new(),
enum_values: values,
possible_types: Vec::new(),
input_fields: Vec::new(),
}
}
pub fn input_object(name: impl Into<String>, input_fields: Vec<InputValue>) -> Self {
IntrospectionType {
kind: TypeKind::InputObject,
name: Some(name.into()),
description: None,
fields: Vec::new(),
enum_values: Vec::new(),
possible_types: Vec::new(),
input_fields,
}
}
fn to_json(&self) -> JsonValue {
json!({
"kind": self.kind.as_str(),
"name": self.name,
"description": self.description,
"fields": self.fields.iter().map(IntrospectionField::to_json).collect::<Vec<_>>(),
"enumValues": if self.enum_values.is_empty() { JsonValue::Null } else {
json!(self.enum_values)
},
"possibleTypes": if self.possible_types.is_empty() { JsonValue::Null } else {
json!(self.possible_types)
},
"inputFields": if self.input_fields.is_empty() { JsonValue::Null } else {
json!(self.input_fields.iter().map(InputValue::to_json).collect::<Vec<_>>())
},
})
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionSchema {
pub query_type: String,
pub mutation_type: Option<String>,
pub subscription_type: Option<String>,
pub types: Vec<IntrospectionType>,
}
impl IntrospectionSchema {
pub fn new(query_type: impl Into<String>) -> Self {
IntrospectionSchema {
query_type: query_type.into(),
mutation_type: None,
subscription_type: None,
types: Vec::new(),
}
}
pub fn add_type(mut self, t: IntrospectionType) -> Self {
self.types.push(t);
self
}
}
pub struct IntrospectionEngine {
schema: IntrospectionSchema,
}
impl IntrospectionEngine {
pub fn new(schema: IntrospectionSchema) -> Self {
IntrospectionEngine { schema }
}
pub fn schema_introspection(&self) -> String {
let mut all_types: Vec<JsonValue> = self
.schema
.types
.iter()
.map(IntrospectionType::to_json)
.collect();
for builtin in Self::built_in_scalars() {
all_types.push(builtin.to_json());
}
let response = json!({
"data": {
"__schema": {
"queryType": { "name": self.schema.query_type },
"mutationType": self.schema.mutation_type.as_ref().map(|n| json!({ "name": n })),
"subscriptionType": self.schema.subscription_type.as_ref().map(|n| json!({ "name": n })),
"types": all_types,
}
}
});
response.to_string()
}
pub fn type_introspection(&self, type_name: &str) -> Option<String> {
let found = self.get_type(type_name).map(|t| t.to_json()).or_else(|| {
Self::built_in_scalars()
.into_iter()
.find(|t| t.name.as_deref() == Some(type_name))
.map(|t| t.to_json())
});
found.map(|type_json| json!({ "data": { "__type": type_json } }).to_string())
}
pub fn field_names(&self, type_name: &str) -> Vec<String> {
self.get_type(type_name)
.map(|t| t.fields.iter().map(|f| f.name.clone()).collect())
.unwrap_or_default()
}
pub fn get_type(&self, name: &str) -> Option<&IntrospectionType> {
self.schema
.types
.iter()
.find(|t| t.name.as_deref() == Some(name))
}
pub fn built_in_scalars() -> Vec<IntrospectionType> {
vec![
IntrospectionType::scalar("String"),
IntrospectionType::scalar("Int"),
IntrospectionType::scalar("Float"),
IntrospectionType::scalar("Boolean"),
IntrospectionType::scalar("ID"),
]
}
pub fn type_count(&self) -> usize {
self.schema.types.len() + Self::built_in_scalars().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value as JsonValue;
fn simple_engine() -> IntrospectionEngine {
let query_type = IntrospectionType::object(
"Query",
vec![
IntrospectionField::new("hero", TypeRef::named(TypeKind::Object, "Character")),
IntrospectionField::new(
"search",
TypeRef::non_null(TypeRef::list(TypeRef::non_null(TypeRef::named(
TypeKind::Object,
"SearchResult",
)))),
)
.deprecated("Use heroSearch instead"),
],
);
let char_type = IntrospectionType::object(
"Character",
vec![
IntrospectionField::new(
"id",
TypeRef::non_null(TypeRef::named(TypeKind::Scalar, "ID")),
),
IntrospectionField::new("name", TypeRef::named(TypeKind::Scalar, "String")),
],
);
let episode_enum = IntrospectionType::enum_type(
"Episode",
vec![
"NEWHOPE".to_string(),
"EMPIRE".to_string(),
"JEDI".to_string(),
],
);
let schema = IntrospectionSchema::new("Query")
.add_type(query_type)
.add_type(char_type)
.add_type(episode_enum);
IntrospectionEngine::new(schema)
}
#[test]
fn test_schema_introspection_returns_json_string() {
let engine = simple_engine();
let result = engine.schema_introspection();
assert!(!result.is_empty());
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
assert!(parsed.is_object());
}
#[test]
fn test_schema_introspection_has_data_key() {
let engine = simple_engine();
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
assert!(parsed["data"].is_object());
}
#[test]
fn test_schema_introspection_has_schema_key() {
let engine = simple_engine();
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
assert!(parsed["data"]["__schema"].is_object());
}
#[test]
fn test_schema_introspection_query_type_name() {
let engine = simple_engine();
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
let qtype = &parsed["data"]["__schema"]["queryType"]["name"];
assert_eq!(qtype, "Query");
}
#[test]
fn test_schema_introspection_types_array() {
let engine = simple_engine();
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
let types = &parsed["data"]["__schema"]["types"];
assert!(types.is_array());
assert!(!types.as_array().expect("should succeed").is_empty());
}
#[test]
fn test_schema_introspection_includes_builtin_scalars() {
let engine = simple_engine();
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
let types = parsed["data"]["__schema"]["types"]
.as_array()
.expect("should succeed");
let names: Vec<&str> = types.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(names.contains(&"String"));
assert!(names.contains(&"Int"));
assert!(names.contains(&"Boolean"));
assert!(names.contains(&"Float"));
assert!(names.contains(&"ID"));
}
#[test]
fn test_schema_introspection_no_mutation_type_by_default() {
let engine = simple_engine();
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
let mut_type = &parsed["data"]["__schema"]["mutationType"];
assert!(mut_type.is_null());
}
#[test]
fn test_schema_introspection_mutation_type_included_when_set() {
let mut_type = IntrospectionType::object("Mutation", vec![]);
let schema = IntrospectionSchema::new("Query")
.add_type(IntrospectionType::object("Query", vec![]))
.add_type(mut_type);
let mut schema_desc = schema.clone();
schema_desc.mutation_type = Some("Mutation".to_string());
let engine = IntrospectionEngine::new(schema_desc);
let result = engine.schema_introspection();
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
assert_eq!(
parsed["data"]["__schema"]["mutationType"]["name"],
"Mutation"
);
}
#[test]
fn test_type_introspection_object_found() {
let engine = simple_engine();
let result = engine.type_introspection("Query");
assert!(result.is_some());
}
#[test]
fn test_type_introspection_scalar_found() {
let engine = simple_engine();
let result = engine.type_introspection("String");
assert!(result.is_some());
}
#[test]
fn test_type_introspection_enum_found() {
let engine = simple_engine();
let result = engine.type_introspection("Episode");
assert!(result.is_some());
let parsed: JsonValue =
serde_json::from_str(&result.expect("should succeed")).expect("should succeed");
assert_eq!(parsed["data"]["__type"]["kind"], "ENUM");
}
#[test]
fn test_type_introspection_not_found_returns_none() {
let engine = simple_engine();
let result = engine.type_introspection("NonexistentType");
assert!(result.is_none());
}
#[test]
fn test_type_introspection_object_has_fields() {
let engine = simple_engine();
let result = engine
.type_introspection("Character")
.expect("should succeed");
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
let fields = parsed["data"]["__type"]["fields"]
.as_array()
.expect("should succeed");
assert_eq!(fields.len(), 2);
let field_names: Vec<&str> = fields
.iter()
.map(|f| f["name"].as_str().expect("should succeed"))
.collect();
assert!(field_names.contains(&"id"));
assert!(field_names.contains(&"name"));
}
#[test]
fn test_type_introspection_scalar_kind() {
let engine = simple_engine();
let result = engine.type_introspection("String").expect("should succeed");
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
assert_eq!(parsed["data"]["__type"]["kind"], "SCALAR");
}
#[test]
fn test_field_names_returns_names_for_object() {
let engine = simple_engine();
let names = engine.field_names("Character");
assert_eq!(names.len(), 2);
assert!(names.contains(&"id".to_string()));
assert!(names.contains(&"name".to_string()));
}
#[test]
fn test_field_names_empty_for_unknown_type() {
let engine = simple_engine();
let names = engine.field_names("Unknown");
assert!(names.is_empty());
}
#[test]
fn test_field_names_empty_for_enum() {
let engine = simple_engine();
let names = engine.field_names("Episode");
assert!(names.is_empty());
}
#[test]
fn test_built_in_scalars_count_is_five() {
let scalars = IntrospectionEngine::built_in_scalars();
assert_eq!(scalars.len(), 5);
}
#[test]
fn test_built_in_scalars_names() {
let scalars = IntrospectionEngine::built_in_scalars();
let names: Vec<&str> = scalars.iter().filter_map(|s| s.name.as_deref()).collect();
assert!(names.contains(&"String"));
assert!(names.contains(&"Int"));
assert!(names.contains(&"Float"));
assert!(names.contains(&"Boolean"));
assert!(names.contains(&"ID"));
}
#[test]
fn test_built_in_scalars_are_scalar_kind() {
for scalar in IntrospectionEngine::built_in_scalars() {
assert_eq!(scalar.kind, TypeKind::Scalar);
}
}
#[test]
fn test_get_type_found() {
let engine = simple_engine();
assert!(engine.get_type("Query").is_some());
}
#[test]
fn test_get_type_not_found() {
let engine = simple_engine();
assert!(engine.get_type("Missing").is_none());
}
#[test]
fn test_get_type_returns_correct_kind() {
let engine = simple_engine();
let t = engine.get_type("Episode").expect("should succeed");
assert_eq!(t.kind, TypeKind::Enum);
}
#[test]
fn test_type_count_includes_builtins() {
let engine = simple_engine();
assert_eq!(engine.type_count(), 8);
}
#[test]
fn test_type_count_empty_schema_is_five() {
let schema = IntrospectionSchema::new("Query");
let engine = IntrospectionEngine::new(schema);
assert_eq!(engine.type_count(), 5);
}
#[test]
fn test_type_ref_non_null_kind() {
let inner = TypeRef::named(TypeKind::Scalar, "String");
let non_null = TypeRef::non_null(inner);
assert_eq!(non_null.kind, TypeKind::NonNull);
assert!(non_null.name.is_none());
}
#[test]
fn test_type_ref_list_kind() {
let inner = TypeRef::named(TypeKind::Scalar, "String");
let list = TypeRef::list(inner);
assert_eq!(list.kind, TypeKind::List);
assert!(list.name.is_none());
}
#[test]
fn test_type_ref_nested_non_null_list() {
let inner = TypeRef::non_null(TypeRef::named(TypeKind::Scalar, "String"));
let list = TypeRef::list(inner);
let outer = TypeRef::non_null(list);
assert_eq!(outer.kind, TypeKind::NonNull);
let list_ref = outer.of_type.as_ref().expect("should succeed");
assert_eq!(list_ref.kind, TypeKind::List);
}
#[test]
fn test_type_ref_to_json_named() {
let tr = TypeRef::named(TypeKind::Scalar, "String");
let j = tr.to_json();
assert_eq!(j["kind"], "SCALAR");
assert_eq!(j["name"], "String");
assert!(j["ofType"].is_null());
}
#[test]
fn test_type_ref_to_json_non_null_has_of_type() {
let inner = TypeRef::named(TypeKind::Scalar, "Int");
let nn = TypeRef::non_null(inner);
let j = nn.to_json();
assert_eq!(j["kind"], "NON_NULL");
assert!(!j["ofType"].is_null());
assert_eq!(j["ofType"]["name"], "Int");
}
#[test]
fn test_deprecated_field_flag() {
let engine = simple_engine();
let t = engine.get_type("Query").expect("should succeed");
let search = t
.fields
.iter()
.find(|f| f.name == "search")
.expect("should succeed");
assert!(search.is_deprecated);
assert!(search.deprecation_reason.is_some());
}
#[test]
fn test_deprecated_field_in_json() {
let engine = simple_engine();
let result = engine.type_introspection("Query").expect("should succeed");
let parsed: JsonValue = serde_json::from_str(&result).expect("should succeed");
let fields = parsed["data"]["__type"]["fields"]
.as_array()
.expect("should succeed");
let search = fields
.iter()
.find(|f| f["name"] == "search")
.expect("should succeed");
assert_eq!(search["isDeprecated"], true);
}
#[test]
fn test_empty_schema_introspection_valid_json() {
let schema = IntrospectionSchema::new("Query");
let engine = IntrospectionEngine::new(schema);
let result = engine.schema_introspection();
let parsed: Result<JsonValue, _> = serde_json::from_str(&result);
assert!(parsed.is_ok());
}
#[test]
fn test_empty_schema_type_introspection_not_found() {
let schema = IntrospectionSchema::new("Query");
let engine = IntrospectionEngine::new(schema);
assert!(engine.type_introspection("Query").is_none());
}
}