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(),
}
}
#[cfg(test)]
#[path = "value_json_tests.rs"]
mod value_json_tests;