use serde_json::Value as Json;
use crate::errors::GraphDDBError;
pub type Params = serde_json::Map<String, Json>;
pub fn validate_params(
params: &Params,
param_specs: &serde_json::Map<String, Json>,
operation_id: &str,
) -> Result<(), GraphDDBError> {
for (name, spec) in param_specs {
let required = spec
.get("required")
.and_then(Json::as_bool)
.unwrap_or(false);
let present = params.get(name).map(|v| !v.is_null()).unwrap_or(false);
if required && !present {
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: missing required parameter '{name}'"
)));
}
if !present {
continue;
}
check_value(operation_id, name, spec, ¶ms[name])?;
}
for name in params.keys() {
if !param_specs.contains_key(name) {
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: unknown parameter '{name}'"
)));
}
}
Ok(())
}
fn check_value(
operation_id: &str,
name: &str,
spec: &Json,
value: &Json,
) -> Result<(), GraphDDBError> {
let kind = spec.get("type").and_then(Json::as_str).unwrap_or("string");
match kind {
"number" => {
if !value.is_number() {
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}' must be a number"
)));
}
}
"boolean" => {
if !value.is_boolean() {
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}' must be a boolean"
)));
}
}
"literal" => {
let empty = vec![];
let literals = spec
.get("literals")
.and_then(Json::as_array)
.unwrap_or(&empty);
if !literals.iter().any(|lit| lit == value) {
let allowed = literals
.iter()
.map(py_repr_json)
.collect::<Vec<_>>()
.join(", ");
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}' must be one of [{allowed}], got {}",
py_repr_json(value)
)));
}
}
"array" => {
let items = value.as_array().ok_or_else(|| {
GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}' must be an array"
))
})?;
let empty = serde_json::Map::new();
let element_specs = spec
.get("element")
.and_then(Json::as_object)
.unwrap_or(&empty);
for (index, element) in items.iter().enumerate() {
let obj = element.as_object().ok_or_else(|| {
GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}'[{index}] must be an object"
))
})?;
for (field, field_spec) in element_specs {
let present = obj.get(field).map(|v| !v.is_null()).unwrap_or(false);
let field_required = field_spec
.get("required")
.and_then(Json::as_bool)
.unwrap_or(false);
if field_required && !present {
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}'[{index}] missing required field '{field}'"
)));
}
if present {
check_value(
operation_id,
&format!("{name}[{index}].{field}"),
field_spec,
&obj[field],
)?;
}
}
}
}
_ => {
if !value.is_string() {
return Err(GraphDDBError::parameter_validation(format!(
"{operation_id}: parameter '{name}' must be a string"
)));
}
}
}
Ok(())
}
fn py_repr_json(v: &Json) -> String {
match v {
Json::String(s) => format!("'{s}'"),
other => other.to_string(),
}
}
pub fn resolve_template(template: &str, params: &Params) -> Result<String, GraphDDBError> {
resolve_with(template, |name| {
params.get(name).and_then(|v| {
if v.is_null() {
None
} else {
Some(json_to_template_string(v))
}
})
})
.map_err(|name| {
GraphDDBError::parameter_validation(format!(
"template '{template}' references unbound parameter '{name}'"
))
})
}
pub fn has_result_placeholder(template: &str) -> bool {
each_placeholder(template).any(|name| name.starts_with("result."))
}
pub fn json_to_template_string(v: &Json) -> String {
match v {
Json::String(s) => s.clone(),
Json::Bool(b) => {
if *b {
"True".to_string()
} else {
"False".to_string()
}
}
Json::Null => "None".to_string(),
Json::Number(n) => n.to_string(),
other => other.to_string(),
}
}
pub(crate) fn resolve_with<F>(template: &str, mut lookup: F) -> Result<String, String>
where
F: FnMut(&str) -> Option<String>,
{
let mut out = String::new();
let bytes = template.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'{' {
if let Some(close) = template[i + 1..].find('}') {
let name = &template[i + 1..i + 1 + close];
if !name.is_empty() && !name.contains('{') {
match lookup(name) {
Some(v) => out.push_str(&v),
None => return Err(name.to_string()),
}
i += 1 + close + 1;
continue;
}
}
}
out.push(bytes[i] as char);
i += 1;
}
Ok(out)
}
pub fn each_placeholder_pub(template: &str) -> Vec<String> {
each_placeholder(template).collect()
}
pub(crate) fn each_placeholder(template: &str) -> impl Iterator<Item = String> + '_ {
let bytes = template.as_bytes();
let mut i = 0;
std::iter::from_fn(move || {
while i < bytes.len() {
if bytes[i] == b'{' {
if let Some(close) = template[i + 1..].find('}') {
let name = &template[i + 1..i + 1 + close];
if !name.is_empty() && !name.contains('{') {
let name = name.to_string();
i += 1 + close + 1;
return Some(name);
}
}
}
i += 1;
}
None
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn params(v: Json) -> Params {
v.as_object().unwrap().clone()
}
#[test]
fn resolves_placeholders() {
let p = params(json!({"userId": "alice"}));
assert_eq!(resolve_template("USER#{userId}", &p).unwrap(), "USER#alice");
assert_eq!(resolve_template("PROFILE", &p).unwrap(), "PROFILE");
}
#[test]
fn unbound_placeholder_errors() {
let p = params(json!({}));
assert!(resolve_template("USER#{userId}", &p).is_err());
}
#[test]
fn validate_required_and_unknown_and_type() {
let specs = json!({"userId": {"type": "string", "required": true}});
let specs = specs.as_object().unwrap();
assert!(validate_params(¶ms(json!({})), specs, "q").is_err());
assert!(validate_params(¶ms(json!({"x": "1"})), specs, "q").is_err());
assert!(validate_params(¶ms(json!({"userId": 5})), specs, "q").is_err());
assert!(validate_params(¶ms(json!({"userId": "a"})), specs, "q").is_ok());
}
#[test]
fn literal_membership() {
let specs = json!({"order": {"type": "literal", "literals": ["asc", "desc"]}});
let specs = specs.as_object().unwrap();
assert!(validate_params(¶ms(json!({"order": "asc"})), specs, "q").is_ok());
assert!(validate_params(¶ms(json!({"order": "up"})), specs, "q").is_err());
}
}