use crate::schema::core::make_promoted_scalar_key;
use crate::value::object;
use serde_json::{json, Value};
use std::collections::HashSet;
mod plan;
pub use plan::NormalisePlan;
#[cfg(feature = "parquet")]
mod typed;
#[cfg(feature = "parquet")]
pub use typed::ArrowPlan;
#[cfg(feature = "parquet")]
mod json;
#[cfg(feature = "parquet")]
pub use json::normalise_to_json;
#[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, serde::Serialize, serde::Deserialize)]
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)| Value::Object([(k, v)].into_iter().collect()))
.collect();
Value::Array(arr)
}
MapEncoding::KeyValueEntries => {
let arr: Vec<Value> = m
.into_iter()
.map(|(k, v)| object([("key", Value::String(k)), ("value", v)]))
.collect();
Value::Array(arr)
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
#[serde(untagged)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct PrunedValue {
pub row: usize,
pub path: Vec<PathSegment>,
pub value: Value,
}
enum Outcome {
Kept(Value),
Pruned(Value),
Emptied,
}
fn prefix_paths(pruned: &mut [PrunedValue], start: usize, seg: impl FnOnce() -> PathSegment) {
if let Some(new) = pruned.get_mut(start..).filter(|new| !new.is_empty()) {
let seg = seg();
new.iter_mut().for_each(|p| p.path.push(seg.clone()));
}
}
fn promoted_scalar_matches(scalar: &Value, name: &str, has_int_field: bool) -> bool {
if !name.contains("__") {
return false;
}
match (scalar, name.split("__").last().unwrap_or("")) {
(Value::String(_), "string") => true,
(Value::Bool(_), "boolean") => true,
(Value::Number(n), "int" | "integer" | "long") => n.is_i64() || n.is_u64(),
(Value::Number(n), "float" | "double" | "number") => n.is_f64() || !has_int_field,
_ => false,
}
}
fn field_value(value: &mut Value, name: &str, has_int_field: bool, take: bool) -> Value {
match value {
Value::Object(m) if take => m.remove(name).unwrap_or(Value::Null),
Value::Object(m) => m.get(name).cloned().unwrap_or(Value::Null),
scalar if promoted_scalar_matches(scalar, name, has_int_field) => scalar.clone(),
_ => Value::Null,
}
}
fn holds(value: &Value, name: &str, has_int_field: bool) -> bool {
match value {
Value::Object(m) => m.get(name).is_some_and(|v| !v.is_null()),
Value::Null => false,
scalar => promoted_scalar_matches(scalar, name, has_int_field),
}
}
pub fn prune_schema(schema: &mut Value, prune: &HashSet<String>) {
match schema {
Value::Object(obj) => {
if obj.get("type") == Some(&Value::String("record".into())) {
if let Some(Value::Array(fields)) = obj.get_mut("fields") {
fields.retain(|f| {
!f.get("name")
.and_then(Value::as_str)
.is_some_and(|n| prune.contains(n))
});
}
}
obj.values_mut().for_each(|v| prune_schema(v, prune));
}
Value::Array(items) => items.iter_mut().for_each(|v| prune_schema(v, prune)),
_ => {}
}
}
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 normalise_inner(
value,
schema,
cfg,
field_name,
&HashSet::new(),
&mut Vec::new(),
) {
Outcome::Kept(v) => v,
Outcome::Pruned(_) | Outcome::Emptied => Value::Null,
}
}
fn normalise_inner(
value: Value,
schema: &Value,
cfg: &NormaliseConfig,
field_name: Option<&str>,
prune: &HashSet<String>,
pruned: &mut Vec<PrunedValue>,
) -> Outcome {
use Outcome::Kept;
match schema {
Value::String(t) if t == "string" => Kept(match value {
Value::Null => Value::Null,
v @ Value::String(_) => v,
v => Value::String(v.to_string()),
}),
Value::String(t) if t == "int" || t == "long" => Kept(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" => Kept(match value {
Value::Null => Value::Null,
Value::Number(n) => n.as_f64().map(|f| json!(f)).unwrap_or(Value::Null),
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" => Kept(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::String(t) if t == "null" => Kept(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") {
let has_int_field = fields.iter().any(|f| {
f.get("name").and_then(|n| n.as_str()).is_some_and(|n| {
n.contains("__")
&& matches!(n.rsplit("__").next(), Some("int" | "integer" | "long"))
})
});
if !prune.is_empty()
&& fields.iter().any(|f| {
f.get("name")
.and_then(Value::as_str)
.is_some_and(|n| prune.contains(n) && holds(&value, n, has_int_field))
})
{
return Outcome::Pruned(value);
}
let start = pruned.len();
let take = prune.is_empty();
let mut value = value;
for f in fields {
if let (Some(Value::String(name)), Some(field_schema)) =
(f.get("name"), f.get("type"))
{
if !prune.is_empty() && prune.contains(name) {
continue;
}
let val = field_value(&mut value, name, has_int_field, take);
let field_start = pruned.len();
let normalised =
normalise_inner(val, field_schema, cfg, Some(name), prune, pruned);
prefix_paths(pruned, field_start, || PathSegment::Key(name.clone()));
match normalised {
Kept(v) => out.insert(name.clone(), v),
Outcome::Emptied => out.insert(name.clone(), Value::Null),
Outcome::Pruned(_) => {
pruned.truncate(start);
return Outcome::Pruned(value);
}
};
}
}
}
Kept(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 => Kept(Value::Null),
Value::Array(arr) if arr.is_empty() && cfg.empty_as_null => Kept(Value::Null),
Value::Array(arr) => {
let len = arr.len();
let mut out = Vec::with_capacity(len);
for (i, v) in arr.into_iter().enumerate() {
let start = pruned.len();
match normalise_inner(v, items_schema, cfg, field_name, prune, pruned) {
Kept(v) => out.push(v),
Outcome::Pruned(v) => pruned.push(PrunedValue {
row: 0,
path: Vec::new(),
value: v,
}),
Outcome::Emptied => {}
}
prefix_paths(pruned, start, || PathSegment::Index(i));
}
if out.is_empty() && len > 0 {
Outcome::Emptied
} else {
Kept(Value::Array(out))
}
}
v => normalise_inner(
Value::Array(vec![v]),
schema,
cfg,
field_name,
prune,
pruned,
),
}
}
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);
let entries = match value {
Value::Null => return Kept(Value::Null),
Value::Object(m) if m.is_empty() && cfg.empty_as_null => return Kept(Value::Null),
Value::Object(m) => m,
v => {
let scalar_type = get_scalar_type_from_value(&v);
let wrapped_key =
make_promoted_scalar_key(field_name.unwrap_or(""), scalar_type);
std::iter::once((wrapped_key, v)).collect()
}
};
let len = entries.len();
let mut out = serde_json::Map::new();
for (k, v) in entries {
let start = pruned.len();
match normalise_inner(v, values_schema, cfg, Some(&k), prune, pruned) {
Kept(v) => {
prefix_paths(pruned, start, || PathSegment::Key(k.clone()));
out.insert(k, v);
}
Outcome::Pruned(v) => {
pruned.push(PrunedValue {
row: 0,
path: Vec::new(),
value: v,
});
prefix_paths(pruned, start, || PathSegment::Key(k));
}
Outcome::Emptied => prefix_paths(pruned, start, || PathSegment::Key(k)),
}
}
if out.is_empty() && len > 0 {
Outcome::Emptied
} else {
Kept(apply_map_encoding(out, cfg.map_encoding))
}
}
Value::Array(types) => {
if types.iter().any(|t| t == "null") {
match types.iter().find(|t| *t != "null") {
Some(branch) if !value.is_null() => {
normalise_inner(value, branch, cfg, field_name, prune, pruned)
}
_ => Kept(Value::Null),
}
} else {
normalise_inner(value, &types[0], cfg, field_name, prune, pruned)
}
}
_ => Kept(value),
}
}
pub fn normalise_values(values: Vec<Value>, schema: &Value, cfg: &NormaliseConfig) -> Vec<Value> {
normalise_values_pruned(values, schema, cfg, &HashSet::new()).0
}
pub fn normalise_values_pruned(
values: Vec<Value>,
schema: &Value,
cfg: &NormaliseConfig,
prune: &HashSet<String>,
) -> (Vec<Value>, Vec<PrunedValue>) {
crate::phase::normalise(|| {
let plan = NormalisePlan::new(schema, cfg, prune);
let mut pruned = Vec::new();
let rows = values
.into_iter()
.enumerate()
.map(|(row, v)| {
let (out, mut row_pruned) = plan.normalise(v);
row_pruned.iter_mut().for_each(|p| p.row = row);
pruned.append(&mut row_pruned);
out
})
.collect();
(rows, pruned)
})
}
#[doc(hidden)]
pub fn normalise_values_pruned_by_schema(
values: Vec<Value>,
schema: &Value,
cfg: &NormaliseConfig,
prune: &HashSet<String>,
) -> (Vec<Value>, Vec<PrunedValue>) {
let mut pruned = Vec::new();
let rows = values
.into_iter()
.enumerate()
.map(|(row, 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>>(),
);
}
let start = pruned.len();
let out = match normalise_inner(v, schema, cfg, None, prune, &mut pruned) {
Outcome::Kept(v) => v,
Outcome::Pruned(v) => {
pruned.push(PrunedValue {
row: 0,
path: Vec::new(),
value: v,
});
Value::Null
}
Outcome::Emptied => Value::Null,
};
for p in &mut pruned[start..] {
p.row = row;
p.path.reverse();
}
out
})
.collect();
(rows, pruned)
}
#[cfg(test)]
mod tests {
include!("tests/normalise.rs");
}