use std::collections::HashMap;
use fraiseql_error::{FraiseQLError, Result};
use graphql_parser::query;
use serde_json::{Map, Value};
pub const VARIABLE_TAG: &str = "$var";
pub const MAX_DEPTH: usize = 64;
pub fn encode(value: &query::Value<String>) -> Result<String> {
let json = to_json(value, 0)?;
serde_json::to_string(&json).map_err(|e| FraiseQLError::Internal {
message: format!("failed to serialize GraphQL argument: {e}"),
source: None,
})
}
fn to_json(value: &query::Value<String>, depth: usize) -> Result<Value> {
if depth > MAX_DEPTH {
return Err(FraiseQLError::validation(format!(
"GraphQL argument nests deeper than the {MAX_DEPTH}-level limit"
)));
}
Ok(match value {
query::Value::String(s) => Value::String(s.clone()),
query::Value::Int(i) => i.as_i64().map_or(Value::Null, Value::from),
query::Value::Float(f) => {
serde_json::Number::from_f64(*f).map_or(Value::Null, Value::Number)
},
query::Value::Boolean(b) => Value::Bool(*b),
query::Value::Null => Value::Null,
query::Value::Enum(e) => Value::String(e.clone()),
query::Value::List(items) => Value::Array(
items.iter().map(|item| to_json(item, depth + 1)).collect::<Result<Vec<_>>>()?,
),
query::Value::Object(obj) => {
let mut map = Map::with_capacity(obj.len());
for (k, v) in obj {
map.insert(k.clone(), to_json(v, depth + 1)?);
}
Value::Object(map)
},
query::Value::Variable(v) => variable_ref(v),
})
}
#[must_use]
pub fn variable_ref(name: &str) -> Value {
let mut map = Map::with_capacity(1);
map.insert(VARIABLE_TAG.to_string(), Value::String(name.to_string()));
Value::Object(map)
}
#[must_use]
pub fn variable_name(value: &Value) -> Option<&str> {
let map = value.as_object()?;
if map.len() != 1 {
return None;
}
map.get(VARIABLE_TAG)?.as_str()
}
pub fn decode(value_json: &str) -> Result<Value> {
serde_json::from_str(value_json).map_err(|e| FraiseQLError::Internal {
message: format!(
"stored GraphQL argument is not valid JSON ({e}); refusing to execute the query \
rather than dropping the argument, which would widen the result set"
),
source: None,
})
}
#[must_use]
#[allow(clippy::implicit_hasher)]
pub fn resolve_variables(value: Value, variables: &HashMap<String, Value>) -> Value {
resolve_at(value, variables, 0)
}
#[allow(clippy::implicit_hasher)] pub fn decode_resolved(value_json: &str, variables: &HashMap<String, Value>) -> Result<Value> {
Ok(resolve_variables(decode(value_json)?, variables))
}
fn resolve_at(value: Value, variables: &HashMap<String, Value>, depth: usize) -> Value {
if depth >= MAX_DEPTH {
return value;
}
match value {
Value::Array(items) => {
Value::Array(items.into_iter().map(|v| resolve_at(v, variables, depth + 1)).collect())
},
Value::Object(map) => {
if let Some(name) = map.get(VARIABLE_TAG).and_then(Value::as_str) {
if map.len() == 1 {
return variables.get(name).cloned().unwrap_or(Value::Null);
}
}
Value::Object(
map.into_iter().map(|(k, v)| (k, resolve_at(v, variables, depth + 1))).collect(),
)
},
other => other,
}
}
#[must_use]
pub fn to_graphql(value: &Value) -> String {
if let Some(name) = variable_name(value) {
return format!("${name}");
}
match value {
Value::Object(map) => {
let pairs: Vec<String> =
map.iter().map(|(k, v)| format!("{k}: {}", to_graphql(v))).collect();
format!("{{{}}}", pairs.join(", "))
},
Value::Array(items) => {
let rendered: Vec<String> = items.iter().map(to_graphql).collect();
format!("[{}]", rendered.join(", "))
},
Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => value.to_string(),
}
}
pub fn variable_defaults(
definitions: &[crate::graphql::VariableDefinition],
) -> Result<Vec<(String, Value)>> {
definitions
.iter()
.filter_map(|def| def.default_value.as_deref().map(|json| (def.name.as_str(), json)))
.map(|(name, json)| Ok((name.to_string(), decode(json)?)))
.collect()
}
#[must_use]
pub fn with_variable_defaults(
defaults: &[(String, Value)],
variables: Option<&Value>,
) -> Option<Value> {
let supplied = match variables {
None | Some(Value::Null) => Map::new(),
Some(Value::Object(map)) => map.clone(),
Some(_) => return None,
};
let missing: Vec<&(String, Value)> =
defaults.iter().filter(|(name, _)| !supplied.contains_key(name)).collect();
if missing.is_empty() {
return None;
}
let mut merged = supplied;
for (name, value) in missing {
merged.insert(name.clone(), value.clone());
}
Some(Value::Object(merged))
}
#[cfg(test)]
#[path = "value_json_tests.rs"]
mod value_json_tests;