use serde_json::{Map, Value};
use crate::{
error::{FraiseQLError, Result},
schema::{ArgumentDefinition, CompiledSchema, FieldType, LocaleConfig},
};
pub fn coerce_localized_arguments(
schema: &CompiledSchema,
label: &str,
declared: &[ArgumentDefinition],
arguments: Option<&Value>,
) -> Result<Option<Value>> {
let Some(config) = schema.locale.as_ref() else {
return Ok(None);
};
let Some(Value::Object(given)) = arguments else {
return Ok(None);
};
let request_locale =
crate::runtime::request_locale(schema).unwrap_or_else(|| config.default.clone());
let coercer = Coercer {
schema,
config,
request_locale: &request_locale,
};
let mut coerced = given.clone();
let mut changed = false;
for argument in declared {
for key in [argument.name.clone(), schema.display_name(&argument.name)] {
let Some(value) = coerced.get_mut(&key) else {
continue;
};
let at = format!("{label}({})", argument.name);
if argument.localized {
*value = coercer.localize(&at, value)?;
changed = true;
} else if let Some(input) = input_type_of(&argument.arg_type) {
changed |= coercer.walk(&at, input, value)?;
}
break;
}
}
Ok(changed.then_some(Value::Object(coerced)))
}
fn input_type_of(field_type: &FieldType) -> Option<&str> {
match field_type {
FieldType::List { item: inner, .. } => input_type_of(inner),
FieldType::Input(name) | FieldType::Object(name) => Some(name),
_ => None,
}
}
struct Coercer<'a> {
schema: &'a CompiledSchema,
config: &'a LocaleConfig,
request_locale: &'a str,
}
impl Coercer<'_> {
fn walk(&self, at: &str, type_name: &str, value: &mut Value) -> Result<bool> {
let Some(input) = self.schema.find_input_type(type_name) else {
return Ok(false);
};
match value {
Value::Array(items) => {
let mut changed = false;
for item in items {
changed |= self.walk(at, type_name, item)?;
}
Ok(changed)
},
Value::Object(object) => {
let mut changed = false;
for field in &input.fields {
for key in [field.name.clone(), self.schema.display_name(&field.name)] {
let Some(inner) = object.get_mut(&key) else {
continue;
};
let at = format!("{at}.{}", field.name);
if field.localized {
*inner = self.localize(&at, inner)?;
changed = true;
} else {
let nested = field.field_type.trim_matches(['[', ']', '!']);
changed |= self.walk(&at, nested, inner)?;
}
break;
}
}
Ok(changed)
},
_ => Ok(false),
}
}
fn localize(&self, at: &str, value: &Value) -> Result<Value> {
let map = match value {
Value::Null => return Ok(Value::Null),
Value::String(label) => {
Map::from_iter([(self.request_locale.to_string(), Value::String(label.clone()))])
},
Value::Object(object) if object.len() == 1 && object.contains_key("value") => {
let label = Self::label(at, self.request_locale, &object["value"])?;
Map::from_iter([(self.request_locale.to_string(), label)])
},
Value::Object(object) if object.len() == 1 && object.contains_key("translations") => {
self.translations(at, &object["translations"])?
},
Value::Object(object)
if !object.contains_key("value") && !object.contains_key("translations") =>
{
let mut map = Map::new();
for (locale, label) in object {
let tag = self.allowed(at, locale)?;
map.insert(tag.clone(), Self::label(at, &tag, label)?);
}
map
},
other => {
return Err(refusal(format!(
"{at} is localized: write a string, {{value: String}}, {{translations: \
[{{locale, value}}]}} or a map of locale to label, not {other}"
)));
},
};
Ok(Value::Object(map))
}
fn translations(&self, at: &str, list: &Value) -> Result<Map<String, Value>> {
let Value::Array(entries) = list else {
return Err(refusal(format!("{at}: `translations` must be a list, not {list}")));
};
let mut map = Map::new();
for entry in entries {
let (Some(Value::String(locale)), Some(label), 2) =
(entry.get("locale"), entry.get("value"), entry.as_object().map_or(0, Map::len))
else {
return Err(refusal(format!(
"{at}: each translation is {{locale: String!, value: String}}, not {entry}"
)));
};
let tag = self.allowed(at, locale)?;
if map.contains_key(&tag) {
return Err(refusal(format!("{at}: locale `{tag}` is given more than once")));
}
map.insert(tag.clone(), Self::label(at, &tag, label)?);
}
Ok(map)
}
fn allowed(&self, at: &str, locale: &str) -> Result<String> {
self.config
.allowed
.iter()
.find(|tag| tag.eq_ignore_ascii_case(locale))
.cloned()
.ok_or_else(|| {
refusal(format!(
"{at}: locale `{locale}` is not allowed (allowed: {})",
self.config.allowed.join(", ")
))
})
}
fn label(at: &str, locale: &str, label: &Value) -> Result<Value> {
match label {
Value::String(_) | Value::Null => Ok(label.clone()),
other => Err(refusal(format!(
"{at}: the `{locale}` label must be a string or null, not {other}"
))),
}
}
}
const fn refusal(message: String) -> FraiseQLError {
FraiseQLError::Validation {
message,
path: None,
}
}