use crate::schema::core::make_promoted_scalar_key;
use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MapEncoding {
Mapping,
Entries,
#[serde(rename = "kv")]
KeyValueEntries,
}
#[derive(Debug, Clone)]
pub struct NormaliseConfig {
pub empty_as_null: bool,
pub coerce_string: bool,
pub map_encoding: MapEncoding,
pub wrap_root: Option<String>,
}
impl Default for NormaliseConfig {
fn default() -> Self {
Self {
empty_as_null: true,
coerce_string: false,
map_encoding: MapEncoding::Mapping,
wrap_root: None,
}
}
}
fn apply_map_encoding(m: serde_json::Map<String, Value>, encoding: MapEncoding) -> Value {
match encoding {
MapEncoding::Mapping => Value::Object(m),
MapEncoding::Entries => {
let arr: Vec<Value> = m.into_iter().map(|(k, v)| json!({ k: v })).collect();
Value::Array(arr)
}
MapEncoding::KeyValueEntries => {
let arr: Vec<Value> = m
.into_iter()
.map(|(k, v)| json!({ "key": k, "value": v }))
.collect();
Value::Array(arr)
}
}
}
fn get_scalar_type_from_value(value: &Value) -> &'static str {
match value {
Value::String(_) => "string",
Value::Number(n) if n.is_i64() => "int",
Value::Number(_) => "float",
Value::Bool(_) => "boolean",
_ => "unknown",
}
}
pub fn normalise_value(
value: Value,
schema: &Value,
cfg: &NormaliseConfig,
field_name: Option<&str>,
) -> Value {
match schema {
Value::String(t) if t == "string" => match value {
Value::Null => Value::Null,
v @ Value::String(_) => v,
v => Value::String(v.to_string()),
},
Value::String(t) if t == "int" || t == "long" => match value {
Value::Null => Value::Null,
Value::Number(n) if n.is_i64() => Value::Number(n),
Value::String(s) if cfg.coerce_string => {
s.parse::<i64>().map(|i| json!(i)).unwrap_or(Value::Null)
}
_ => Value::Null,
},
Value::String(t) if t == "double" || t == "float" => match value {
Value::Null => Value::Null,
Value::Number(n) if n.is_f64() => Value::Number(n),
Value::String(s) if cfg.coerce_string => {
s.parse::<f64>().map(|f| json!(f)).unwrap_or(Value::Null)
}
_ => Value::Null,
},
Value::String(t) if t == "boolean" => match value {
Value::Null => Value::Null,
Value::Bool(b) => Value::Bool(b),
Value::String(s) if cfg.coerce_string => match s.as_str() {
"true" | "1" => Value::Bool(true),
"false" | "0" => Value::Bool(false),
_ => Value::Null,
},
_ => Value::Null,
},
Value::Object(obj) if obj.get("type") == Some(&Value::String("record".into())) => {
let mut out = serde_json::Map::new();
if let Some(Value::Array(fields)) = obj.get("fields") {
for f in fields {
if let (Some(Value::String(name)), Some(field_schema)) =
(f.get("name"), f.get("type"))
{
let val = match &value {
Value::Object(m) => m.get(name).cloned().unwrap_or(Value::Null),
scalar_value => {
if name.contains("__") {
let type_suffix = name.split("__").last().unwrap_or("");
let matches_type = matches!(
(scalar_value, type_suffix),
(Value::String(_), "string")
| (
Value::Number(_),
"int" | "long" | "float" | "double",
)
| (Value::Bool(_), "boolean")
);
if matches_type {
scalar_value.clone()
} else {
Value::Null
}
} else {
Value::Null
}
}
};
out.insert(
name.clone(),
normalise_value(val, field_schema, cfg, Some(name)),
);
}
}
}
Value::Object(out)
}
Value::Object(obj) if obj.get("type") == Some(&Value::String("array".into())) => {
let default_items = Value::String("string".into());
let items_schema = obj.get("items").unwrap_or(&default_items);
match value {
Value::Null => Value::Null,
Value::Array(arr) if arr.is_empty() && cfg.empty_as_null => Value::Null,
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| normalise_value(v, items_schema, cfg, field_name))
.collect(),
),
v => Value::Array(vec![normalise_value(v, items_schema, cfg, field_name)]),
}
}
Value::Object(obj) if obj.get("type") == Some(&Value::String("map".into())) => {
let default_values = Value::String("string".into());
let values_schema = obj.get("values").unwrap_or(&default_values);
match value {
Value::Null => Value::Null,
Value::Object(m) if m.is_empty() && cfg.empty_as_null => Value::Null,
Value::Object(m) => {
let mut out = serde_json::Map::new();
if values_schema.get("type") == Some(&Value::String("object".into())) {
for (k, v) in m {
let normalised_record =
normalise_value(v, values_schema, cfg, Some(&k));
out.insert(k, normalised_record);
}
} else {
for (k, v) in m {
let normalised_value = normalise_value(v, values_schema, cfg, Some(&k));
out.insert(k, normalised_value);
}
}
apply_map_encoding(out, cfg.map_encoding)
}
v => {
let mut synthetic = serde_json::Map::new();
let scalar_type = get_scalar_type_from_value(&v);
let wrapped_key =
make_promoted_scalar_key(field_name.unwrap_or(""), scalar_type);
synthetic.insert(
wrapped_key,
normalise_value(v, values_schema, cfg, field_name),
);
apply_map_encoding(synthetic, cfg.map_encoding)
}
}
}
Value::Array(types) => {
if types.iter().any(|t| t == "null") {
if value.is_null() {
Value::Null
} else {
let branch = types.iter().find(|t| *t != "null").unwrap();
normalise_value(value, branch, cfg, field_name)
}
} else {
normalise_value(value, &types[0], cfg, field_name)
}
}
_ => value,
}
}
pub fn normalise_values(values: Vec<Value>, schema: &Value, cfg: &NormaliseConfig) -> Vec<Value> {
values
.into_iter()
.map(|mut v| {
if let Some(ref field) = cfg.wrap_root {
v = Value::Object(
std::iter::once((field.clone(), v)).collect::<serde_json::Map<String, Value>>(),
);
}
normalise_value(v, schema, cfg, None) })
.collect()
}
#[cfg(test)]
mod tests {
include!("tests/normalise.rs");
}