use crate::executable::{
operation::{Analyzer, VariableValues, Visitor},
Cache,
};
use bluejay_core::{
definition::SchemaDefinition,
executable::{ExecutableDocument, OperationDefinition, VariableDefinition},
Argument, AsIter, ObjectValue, Value, ValueReference, Variable,
};
#[derive(Clone)]
pub struct Offender {
pub size: usize,
pub name: String,
}
#[derive(Clone)]
pub struct InputSize<'a, E: ExecutableDocument, VV: VariableValues> {
offenders: Vec<Offender>,
max_length: usize,
variable_values: &'a VV,
variable_definitions: Option<&'a E::VariableDefinitions>,
}
impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Visitor<'a, E, S, VV>
for InputSize<'a, E, VV>
{
type ExtraInfo = usize;
fn new(
op: &'a E::OperationDefinition,
_s: &'a S,
variables: &'a VV,
_: &'a Cache<'a, E, S>,
max_length: Self::ExtraInfo,
) -> Self {
Self {
max_length,
offenders: vec![],
variable_values: variables,
variable_definitions: op.as_ref().variable_definitions(),
}
}
fn visit_variable_argument(
&mut self,
argument: &'a <E as ExecutableDocument>::Argument<false>,
_input_value_definition: &'a S::InputValueDefinition,
) {
find_input_size_offenders_arguments::<E, VV, false>(
self.max_length,
&mut self.offenders,
self.variable_values,
self.variable_definitions,
argument.name().to_string(),
argument.value(),
);
}
}
fn find_input_size_offenders_arguments<
E: ExecutableDocument,
VV: VariableValues,
const CONST: bool,
>(
max_length: usize,
offenders: &mut Vec<Offender>,
variable_values: &VV,
variable_definitions: Option<&E::VariableDefinitions>,
argument_name: String,
argument_value: &<E as bluejay_core::executable::ExecutableDocument>::Value<CONST>,
) {
match argument_value.as_ref() {
ValueReference::List(list) => {
let list_length = list.len();
if list_length > max_length {
offenders.push(Offender {
size: list_length,
name: argument_name,
})
} else {
list.iter().enumerate().for_each(|(index, item)| {
find_input_size_offenders_arguments::<E, VV, CONST>(
max_length,
offenders,
variable_values,
variable_definitions,
format!("{}.{}", argument_name, index),
item,
);
})
}
}
ValueReference::Object(obj) => {
obj.iter().for_each(|(key, value)| {
find_input_size_offenders_arguments::<E, VV, CONST>(
max_length,
offenders,
variable_values,
variable_definitions,
format!("{}.{}", argument_name, key.as_ref()),
value,
);
});
}
ValueReference::Variable(var) => {
let name = var.name();
let variable = variable_values.get(name);
if let Some(value) = variable {
find_input_size_offenders_variables::<E, VV>(
max_length,
offenders,
argument_name,
value,
);
} else {
let variable_definition = variable_definitions.map(|variable_definitions| {
variable_definitions
.iter()
.find(|def| def.variable() == argument_name)
});
if let Some(Some(var_def)) = variable_definition {
let default_value = var_def.default_value();
if let Some(default_value) = default_value {
find_input_size_offenders_arguments::<E, VV, true>(
max_length,
offenders,
variable_values,
variable_definitions,
argument_name,
default_value,
);
}
}
}
}
_ => {}
};
}
fn find_input_size_offenders_variables<E: ExecutableDocument, VV: VariableValues>(
max_length: usize,
offenders: &mut Vec<Offender>,
argument_name: String,
argument_value: &VV::Value,
) {
match argument_value.as_ref() {
ValueReference::List(list) => {
let list_length = list.len();
if list_length > max_length {
offenders.push(Offender {
size: list_length,
name: argument_name,
})
} else {
list.iter().enumerate().for_each(|(index, item)| {
find_input_size_offenders_variables::<E, VV>(
max_length,
offenders,
format!("{}.{}", argument_name, index),
item,
);
})
}
}
ValueReference::Object(obj) => {
obj.iter().for_each(|(key, value)| {
find_input_size_offenders_variables::<E, VV>(
max_length,
offenders,
format!("{}.{}", argument_name, key.as_ref()),
value,
);
});
}
_ => {}
};
}
impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Analyzer<'a, E, S, VV>
for InputSize<'a, E, VV>
{
type Output = Vec<Offender>;
fn into_output(self) -> Self::Output {
self.offenders
}
}
#[cfg(test)]
mod tests {
use super::{InputSize, Offender};
use crate::executable::{operation::Orchestrator, Cache};
use bluejay_parser::ast::{
definition::{
DefaultContext, DefinitionDocument, SchemaDefinition as ParserSchemaDefinition,
},
executable::ExecutableDocument as ParserExecutableDocument,
Parse,
};
use serde_json::{Map as JsonMap, Value as JsonValue};
const TEST_SCHEMA: &str = r#"
directive @test(y: [String]) on FIELD_DEFINITION
input ObjectList {
property: [String]
}
type Query {
simple(x: [String]): String!
object(x: ObjectList): String!
list_object(x: [ObjectList]): String!
}
schema {
query: Query
}
"#;
fn analyze_input_size(query: &str, variables: serde_json::Value) -> Vec<Offender> {
let definition_document: DefinitionDocument<'_, DefaultContext> =
DefinitionDocument::parse(TEST_SCHEMA).expect("Schema had parse errors");
let schema_definition =
ParserSchemaDefinition::try_from(&definition_document).expect("Schema had errors");
let executable_document = ParserExecutableDocument::parse(query)
.unwrap_or_else(|_| panic!("Document had parse errors"));
let cache = Cache::new(&executable_document, &schema_definition);
let variables = variables.as_object().expect("Variables must be an object");
Orchestrator::<_, _, JsonMap<String, JsonValue>, InputSize<_, _>>::analyze(
&executable_document,
&schema_definition,
None,
variables,
&cache,
1,
)
.unwrap()
}
#[test]
fn simple_size() {
let result =
analyze_input_size(r#"query { simple(x: ["x", "y"])} "#, serde_json::json!({}));
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x");
}
#[test]
fn simple_directive_size() {
let result = analyze_input_size(
r#"query { simple(x: []) @test(y: ["x", "y"])} "#,
serde_json::json!({}),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "y");
}
#[test]
fn simple_size_variable() {
let result = analyze_input_size(
r#"query ($x: [String]) { simple(x: $x)} "#,
serde_json::json!({ "x": ["x", "y"] }),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x");
}
#[test]
fn simple_size_variable_default_value() {
let result = analyze_input_size(
r#"query ($x: [String] = ["x", "y"]) { simple(x: $x)} "#,
serde_json::json!({}),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x");
}
#[test]
fn object_size() {
let result = analyze_input_size(
r#"query { object(x: { property: ["x", "y"] })} "#,
serde_json::json!({}),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x.property");
}
#[test]
fn object_size_variable() {
let result = analyze_input_size(
r#"query($x: ObjectList) { object(x: $x)} "#,
serde_json::json!({ "x": { "property": ["x", "y"] } }),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x.property");
}
#[test]
fn list_object_size() {
let result = analyze_input_size(
r#"query { list_object(x: [{ property: ["x", "y"] }, { property: ["x", "y"] }])} "#,
serde_json::json!({}),
);
assert_eq!(result.len(), 1);
let first = result.first().unwrap();
assert_eq!(first.size, 2);
assert_eq!(first.name, "x");
}
#[test]
fn list_object_size_variable() {
let result = analyze_input_size(
r#"query($x: [ObjectList]) { list_object(x: $x)} "#,
serde_json::json!({ "x": [{ "property": ["x", "y"] }, { "property": ["x", "y"] }] }),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x");
}
#[test]
fn list_nested_object_size() {
let result = analyze_input_size(
r#"query { list_object(x: [{ property: ["x", "y"] }])} "#,
serde_json::json!({}),
);
assert_eq!(result.len(), 1);
let first = result.first().unwrap();
assert_eq!(first.size, 2);
assert_eq!(first.name, "x.0.property");
}
#[test]
fn list_nested_object_size_variable() {
let result = analyze_input_size(
r#"query($x: [ObjectList]) { list_object(x: $x)} "#,
serde_json::json!({ "x": [{ "property": ["x", "y"] }] }),
);
let result = result.first().unwrap();
assert_eq!(result.size, 2);
assert_eq!(result.name, "x.0.property");
}
}