#[cfg(test)]
mod tests;
use std::collections::HashMap;
use serde_json::{Map, Value};
use crate::graphql::{
types::{Directive, FieldSelection},
value_json,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeferredPayload {
pub path: Vec<Value>,
pub label: Option<String>,
pub data: Map<String, Value>,
}
#[must_use]
#[allow(clippy::implicit_hasher)]
pub fn contains_defer(selections: &[FieldSelection], variables: &HashMap<String, Value>) -> bool {
selections.iter().any(|s| {
defer_directive(s, variables).is_some_and(|d| d.enabled)
|| contains_defer(&s.nested_fields, variables)
})
}
#[must_use]
#[allow(clippy::implicit_hasher)] pub fn split(
selections: &[FieldSelection],
data: &mut Value,
variables: &HashMap<String, Value>,
) -> Vec<DeferredPayload> {
let mut out = Vec::new();
let mut path = Vec::new();
walk(selections, data, &mut path, variables, &mut out);
out
}
struct DeferSpec {
enabled: bool,
label: Option<String>,
}
fn defer_directive(
selection: &FieldSelection,
variables: &HashMap<String, Value>,
) -> Option<DeferSpec> {
let directive = selection.directives.iter().find(|d| d.name == "defer")?;
Some(DeferSpec {
enabled: directive_arg(directive, "if", variables)
.is_none_or(|v| v.as_bool().unwrap_or(true)),
label: directive_arg(directive, "label", variables)
.and_then(|v| v.as_str().map(ToOwned::to_owned)),
})
}
fn directive_arg(
directive: &Directive,
name: &str,
variables: &HashMap<String, Value>,
) -> Option<Value> {
let arg = directive.arguments.iter().find(|a| a.name == name)?;
let decoded = value_json::decode(&arg.value_json).ok()?;
Some(value_json::resolve_variables(decoded, variables))
}
fn walk(
selections: &[FieldSelection],
value: &mut Value,
path: &mut Vec<Value>,
variables: &HashMap<String, Value>,
out: &mut Vec<DeferredPayload>,
) {
if let Value::Array(items) = value {
for (index, item) in items.iter_mut().enumerate() {
path.push(Value::from(index));
walk(selections, item, path, variables, out);
path.pop();
}
return;
}
let Value::Object(object) = value else {
return;
};
for selection in selections {
let key = selection.response_key();
match defer_directive(selection, variables) {
Some(spec) if spec.enabled => {
if let Some(field) = object.remove(key) {
push_deferred(out, path, spec.label, key, field);
}
},
_ => {
if selection.nested_fields.is_empty() {
continue;
}
if let Some(child) = object.get_mut(key) {
path.push(Value::from(key));
walk(&selection.nested_fields, child, path, variables, out);
path.pop();
}
},
}
}
}
fn push_deferred(
out: &mut Vec<DeferredPayload>,
path: &[Value],
label: Option<String>,
key: &str,
field: Value,
) {
if let Some(existing) = out.iter_mut().find(|p| p.path == path && p.label == label) {
existing.data.insert(key.to_owned(), field);
return;
}
let mut data = Map::new();
data.insert(key.to_owned(), field);
out.push(DeferredPayload {
path: path.to_vec(),
label,
data,
});
}