use std::collections::{BTreeMap, HashMap};
use std::sync::LazyLock;
use graphql_tools::validation::rules::default_rules_validation_plan;
use graphql_tools::validation::validate::validate;
use sonic_rs::Value;
use crate::executor::introspection::partition::partition_operation;
use crate::executor::introspection::schema::{SchemaMetadata, SchemaWithMetadata};
use crate::executor::variables::{collect_variables, VariableCoercionError};
use crate::query_planner::ast::normalization::normalize_operation;
use crate::query_planner::consumer_schema::ConsumerSchema;
use crate::query_planner::state::supergraph_state::SupergraphState;
use crate::query_planner::utils::parsing::{parse_schema, safe_parse_operation};
struct TestSchema {
supergraph_state: SupergraphState,
consumer_schema: ConsumerSchema,
metadata: SchemaMetadata,
}
static SCHEMA: LazyLock<TestSchema> = LazyLock::new(|| {
let supergraph = parse_schema(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/fixture/variables_coercion/supergraph.graphql"
)));
let consumer_schema = ConsumerSchema::new_from_supergraph(&supergraph);
let metadata = consumer_schema.schema_metadata();
TestSchema {
supergraph_state: SupergraphState::new(&supergraph),
consumer_schema,
metadata,
}
});
#[derive(Debug, PartialEq)]
pub(super) enum Outcome {
Coerced(BTreeMap<String, Value>),
Rejected(Vec<String>),
}
pub(super) fn coerce(operation: &str, variables: &str) -> Outcome {
let document = safe_parse_operation(operation)
.unwrap_or_else(|err| panic!("failed to parse operation: {err}\n{operation}"));
let normalized = normalize_operation(&SCHEMA.supergraph_state, &document, None)
.unwrap_or_else(|err| panic!("failed to normalize operation: {err}\n{operation}"));
let operation_for_plan = partition_operation(normalized.operation).downstream_operation;
let mut variables: HashMap<String, Value> = sonic_rs::from_str(variables)
.unwrap_or_else(|err| panic!("variables must be a JSON object: {err}\n{variables}"));
match collect_variables(&operation_for_plan, &mut variables, &SCHEMA.metadata) {
Ok(coerced) => Outcome::Coerced(coerced.unwrap_or_default().into_iter().collect()),
Err(err) => Outcome::Rejected(messages(&err)),
}
}
fn messages(err: &VariableCoercionError) -> Vec<String> {
vec![err.to_string()]
}
#[track_caller]
pub(super) fn assert_coerced(operation: &str, variables: &str, expected: &str) {
let expected: BTreeMap<String, Value> = sonic_rs::from_str(expected)
.unwrap_or_else(|err| panic!("expected must be a JSON object: {err}\n{expected}"));
assert_eq!(
coerce(operation, variables),
Outcome::Coerced(expected),
"\noperation: {operation}\nvariables: {variables}"
);
}
#[track_caller]
pub(super) fn assert_forwarded(operation: &str, variables: &str, expected: &str) {
match coerce(operation, variables) {
Outcome::Coerced(coerced) => assert_eq!(
sonic_rs::to_string(&coerced).unwrap(),
expected,
"\noperation: {operation}\nvariables: {variables}"
),
rejected => panic!(
"expected the variables to be accepted, got {rejected:?}\noperation: {operation}\nvariables: {variables}"
),
}
}
#[track_caller]
pub(super) fn assert_invalid_json(variables: &str) {
let parsed = sonic_rs::from_str::<HashMap<String, Value>>(variables);
assert!(
parsed.is_err(),
"expected invalid JSON, parsed {parsed:?}\nvariables: {variables}"
);
}
#[track_caller]
pub(super) fn assert_rejected(operation: &str, variables: &str, expected: &str) {
assert_eq!(
coerce(operation, variables),
Outcome::Rejected(vec![expected.to_string()]),
"\noperation: {operation}\nvariables: {variables}"
);
}
#[track_caller]
pub(super) fn assert_rejected_by_validation(operation: &str, rule: &str) {
let document = safe_parse_operation(operation)
.unwrap_or_else(|err| panic!("failed to parse operation: {err}\n{operation}"));
let errors = validate(
&SCHEMA.consumer_schema.document,
&document,
&default_rules_validation_plan(),
);
assert!(
errors.iter().any(|error| error.error_code == rule),
"expected a {rule} validation error\noperation: {operation}\nerrors: {errors:#?}"
);
}