use std::collections::HashMap;
use apollo_parser::cst::{self, CstNode as _};
use fraiseql_error::{FraiseQLError, Result};
use serde_json::{Map, Value};
pub const VARIABLE_TAG: &str = "$var";
pub const MAX_DEPTH: usize = 64;
#[derive(Debug)]
pub(crate) struct LiteralError {
pub(crate) at: usize,
pub(crate) message: String,
}
pub(crate) fn encode_literal(value: &cst::Value) -> std::result::Result<String, LiteralError> {
let json = literal_to_json(value, 0)?;
Ok(json.to_string())
}
fn at(node: &apollo_parser::SyntaxNode) -> usize {
usize::from(node.text_range().start())
}
fn literal_to_json(value: &cst::Value, depth: usize) -> std::result::Result<Value, LiteralError> {
let refuse = |node: &apollo_parser::SyntaxNode, message: String| LiteralError {
at: at(node),
message,
};
if depth > MAX_DEPTH {
return Err(refuse(
value.syntax(),
format!("GraphQL argument nests deeper than the {MAX_DEPTH}-level limit"),
));
}
Ok(match value {
cst::Value::Variable(variable) => {
let name = variable.name().map(|n| n.text().to_string()).unwrap_or_default();
variable_ref(&name)
},
cst::Value::StringValue(s) => Value::String(String::from(s)),
cst::Value::IntValue(i) => {
let text = i.syntax().text().to_string();
let n: i64 = text.parse().map_err(|_| {
refuse(
i.syntax(),
format!("integer `{text}` is out of range (a 64-bit integer at most)"),
)
})?;
Value::from(n)
},
cst::Value::FloatValue(f) => {
let text = f.syntax().text().to_string();
text.parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(Value::Number)
.ok_or_else(|| {
refuse(
f.syntax(),
format!("number `{text}` is out of range (a 64-bit float at most)"),
)
})?
},
cst::Value::BooleanValue(b) => Value::Bool(b.true_token().is_some()),
cst::Value::NullValue(_) => Value::Null,
cst::Value::EnumValue(e) => {
Value::String(e.name().map(|n| n.text().to_string()).unwrap_or_default())
},
cst::Value::ListValue(list) => Value::Array(
list.values()
.map(|item| literal_to_json(&item, depth + 1))
.collect::<std::result::Result<_, _>>()?,
),
cst::Value::ObjectValue(object) => {
let mut map = Map::new();
for field in object.object_fields() {
let key = field.name().map(|n| n.text().to_string()).unwrap_or_default();
let Some(item) = field.value() else {
return Err(refuse(field.syntax(), format!("`{key}` has no value")));
};
let item = literal_to_json(&item, depth + 1)?;
if map.insert(key.clone(), item).is_some() {
return Err(refuse(
field.syntax(),
format!(
"the input object names `{key}` twice; an input object's field \
names are unique (GraphQL § 5.6.3)"
),
));
}
}
Value::Object(map)
},
})
}
#[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;