use std::fs;
use std::path::PathBuf;
use wesley_core::{
compute_registry_hash, to_canonical_json, ApolloLoweringAdapter, LoweringPort, TypeDefinition,
TypeKind,
};
fn get_fixture_path(name: &str) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../../test/fixtures/ir-parity");
path.push(name);
path
}
fn create_adapter() -> ApolloLoweringAdapter {
ApolloLoweringAdapter::new(3)
}
async fn validate_schema(name: &str) {
let sdl_path = get_fixture_path(&format!("{}.graphql", name));
let sdl = fs::read_to_string(sdl_path).expect("Failed to read SDL fixture");
let adapter = create_adapter();
let ir = adapter
.lower_sdl(&sdl)
.await
.expect("Failed to lower SDL to L1 IR");
let actual_hash = compute_registry_hash(&ir).expect("Failed to compute IR hash");
let mut parity_ir = ir.clone();
parity_ir.metadata = None;
let actual_json = to_canonical_json(&parity_ir).expect("Failed to canonicalize IR");
let hash_path = get_fixture_path(&format!("{}.l1.hash", name));
let json_path = get_fixture_path(&format!("{}.l1.json", name));
if !hash_path.exists() {
println!("Initializing L1 gold master for {}", name);
fs::write(&hash_path, &actual_hash).unwrap();
fs::write(&json_path, serde_json::to_string_pretty(&ir).unwrap()).unwrap();
} else {
let expected_hash = fs::read_to_string(&hash_path).unwrap().trim().to_string();
if actual_hash != expected_hash {
let diff_json_path = get_fixture_path(&format!("{}.l1.actual.json", name));
fs::write(&diff_json_path, actual_json).unwrap();
panic!(
"L1 Hash mismatch for {}.\nExpected: {}\nActual: {}\nActual JSON written to: {:?}",
name, expected_hash, actual_hash, diff_json_path
);
}
}
}
#[tokio::test]
async fn test_lower_small_schema() {
validate_schema("small-schema").await;
}
#[tokio::test]
async fn test_lower_medium_schema() {
validate_schema("medium-schema").await;
}
#[tokio::test]
async fn test_lower_large_schema() {
validate_schema("large-schema").await;
}
#[tokio::test]
async fn lowers_graphql_type_families_into_l1_ir() {
let sdl = r#"
scalar DateTime @specifiedBy(url: "https://example.com/datetime")
interface Node {
id: ID!
}
interface Timestamped implements Node {
id: ID!
updatedAt: DateTime
}
type Team implements Node {
id: ID!
}
type Organization implements Node {
id: ID!
}
type User implements Node & Timestamped {
id: ID!
updatedAt: DateTime
name: String
}
union SearchResult = User | Team
extend union SearchResult = Organization
enum Role {
ADMIN
MEMBER
}
extend enum Role {
VIEWER
}
input UserFilter {
role: Role
ids: [ID!]!
}
extend input UserFilter {
active: Boolean @wes_default(value: "true")
}
"#;
let adapter = create_adapter();
let ir = adapter
.lower_sdl(sdl)
.await
.expect("Failed to lower SDL to L1 IR");
let date_time = find_type(&ir.types, "DateTime");
assert_eq!(date_time.kind, TypeKind::Scalar);
assert_eq!(
date_time.directives["specifiedBy"]["url"],
serde_json::Value::String("https://example.com/datetime".to_string())
);
let timestamped = find_type(&ir.types, "Timestamped");
assert_eq!(timestamped.kind, TypeKind::Interface);
assert_eq!(timestamped.implements, vec!["Node"]);
assert_eq!(
timestamped
.fields
.iter()
.map(|field| field.name.as_str())
.collect::<Vec<_>>(),
vec!["id", "updatedAt"]
);
let user = find_type(&ir.types, "User");
assert_eq!(user.kind, TypeKind::Object);
assert_eq!(user.implements, vec!["Node", "Timestamped"]);
let search_result = find_type(&ir.types, "SearchResult");
assert_eq!(search_result.kind, TypeKind::Union);
assert_eq!(
search_result.union_members,
vec!["User", "Team", "Organization"]
);
let role = find_type(&ir.types, "Role");
assert_eq!(role.kind, TypeKind::Enum);
assert_eq!(role.enum_values, vec!["ADMIN", "MEMBER", "VIEWER"]);
let user_filter = find_type(&ir.types, "UserFilter");
assert_eq!(user_filter.kind, TypeKind::InputObject);
let ids_field = user_filter
.fields
.iter()
.find(|field| field.name == "ids")
.expect("missing ids input field");
assert_eq!(ids_field.r#type.base, "ID");
assert!(!ids_field.r#type.nullable);
assert!(ids_field.r#type.is_list);
assert_eq!(ids_field.r#type.list_item_nullable, Some(false));
}
#[tokio::test]
async fn preserves_nested_list_type_references() {
let sdl = r#"
type Matrix {
values: [[Int!]!]!
maybeValues: [[String]]
}
"#;
let adapter = create_adapter();
let ir = adapter
.lower_sdl(sdl)
.await
.expect("Failed to lower SDL to L1 IR");
let matrix = find_type(&ir.types, "Matrix");
let values = matrix
.fields
.iter()
.find(|field| field.name == "values")
.expect("missing values field");
let maybe_values = matrix
.fields
.iter()
.find(|field| field.name == "maybeValues")
.expect("missing maybeValues field");
assert_eq!(values.r#type.base, "Int");
assert!(!values.r#type.nullable);
assert!(values.r#type.is_list);
assert_eq!(values.r#type.list_item_nullable, Some(false));
assert_eq!(
values
.r#type
.list_wrappers
.iter()
.map(|wrapper| wrapper.nullable)
.collect::<Vec<_>>(),
vec![false, false]
);
assert_eq!(values.r#type.leaf_nullable, Some(false));
assert_eq!(maybe_values.r#type.base, "String");
assert!(maybe_values.r#type.nullable);
assert!(maybe_values.r#type.is_list);
assert_eq!(maybe_values.r#type.list_item_nullable, Some(true));
assert_eq!(
maybe_values
.r#type
.list_wrappers
.iter()
.map(|wrapper| wrapper.nullable)
.collect::<Vec<_>>(),
vec![true, true]
);
assert_eq!(maybe_values.r#type.leaf_nullable, Some(true));
}
#[tokio::test]
async fn rejects_mixed_type_kind_consolidation() {
let sdl = r#"
type Thing {
id: ID
}
extend input Thing {
label: String
}
"#;
let adapter = create_adapter();
let err = adapter
.lower_sdl(sdl)
.await
.expect_err("mixed type kind should fail lowering");
let message = err.to_string();
assert!(
message.contains("Thing") && message.contains("Object") && message.contains("InputObject"),
"unexpected error: {message}"
);
}
fn find_type<'a>(types: &'a [TypeDefinition], name: &str) -> &'a TypeDefinition {
types
.iter()
.find(|type_def| type_def.name == name)
.unwrap_or_else(|| panic!("missing type {name}"))
}