use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaInferenceConfig {
pub ignore_outer_array: bool,
pub delimiter: Option<u8>,
pub schema_uri: Option<String>,
pub map_threshold: usize,
pub map_max_required_keys: Option<usize>,
pub unify_maps: bool,
pub no_unify: std::collections::HashSet<String>,
pub force_field_types: HashMap<String, String>,
pub force_parent_field_types: HashMap<String, String>,
pub force_scalar_promotion: std::collections::HashSet<String>,
pub wrap_scalars: bool,
pub wrap_root: Option<String>,
pub no_root_map: bool,
pub max_builders: Option<usize>,
#[cfg(feature = "avro")]
pub avro: bool,
pub debug: bool,
pub profile: bool,
pub verbosity: DebugVerbosity,
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum DebugVerbosity {
#[default]
Normal,
Verbose,
}
impl SchemaInferenceConfig {
pub(crate) fn profile(&self, args: std::fmt::Arguments) {
if self.profile {
let message = format!("{}", args);
anstream::eprintln!("{}", message);
}
}
pub(crate) fn profile_verbose(&self, args: std::fmt::Arguments) {
if self.profile && matches!(self.verbosity, DebugVerbosity::Verbose) {
let message = format!("{}", args);
anstream::eprintln!("{}", message);
}
}
pub(crate) fn debug(&self, args: std::fmt::Arguments) {
if self.debug {
let message = format!("{}", args);
anstream::eprintln!("{}", self.maybe_truncate(message));
}
}
pub(crate) fn debug_verbose(&self, args: std::fmt::Arguments) {
if self.debug_verbose_enabled() {
let message = format!("{}", args);
anstream::eprintln!("{}", self.maybe_truncate(message));
}
}
pub(crate) fn debug_verbose_enabled(&self) -> bool {
self.debug && matches!(self.verbosity, DebugVerbosity::Verbose)
}
fn maybe_truncate(&self, message: String) -> String {
let lines: Vec<&str> = message.lines().collect();
if lines.len() > 20 && self.verbosity == DebugVerbosity::Normal {
let mut truncated = String::new();
for line in lines.iter().take(10) {
truncated.push_str(line);
truncated.push('\n');
}
truncated.push_str(&format!("... ({} lines truncated) ...\n", lines.len() - 15));
for line in lines.iter().skip(lines.len() - 5) {
truncated.push_str(line);
truncated.push('\n');
}
truncated
} else {
message
}
}
}
impl Default for SchemaInferenceConfig {
fn default() -> Self {
Self {
ignore_outer_array: true,
delimiter: None,
schema_uri: Some("AUTO".to_string()),
map_threshold: 20,
map_max_required_keys: None,
unify_maps: false,
no_unify: std::collections::HashSet::new(),
force_field_types: std::collections::HashMap::new(),
force_parent_field_types: std::collections::HashMap::new(),
force_scalar_promotion: std::collections::HashSet::new(),
wrap_scalars: true,
wrap_root: None,
no_root_map: true,
max_builders: None,
#[cfg(feature = "avro")]
avro: false,
debug: false,
profile: false,
verbosity: DebugVerbosity::default(),
}
}
}
#[macro_export]
macro_rules! profile {
($cfg:expr, $($arg:tt)*) => {
$cfg.profile(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! profile_verbose {
($cfg:expr, $($arg:tt)*) => {
$cfg.profile_verbose(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! debug {
($cfg:expr, $($arg:tt)*) => {{
let cfg = &$cfg;
if cfg.debug {
cfg.debug(format_args!($($arg)*))
}
}};
}
#[macro_export]
macro_rules! debug_verbose {
($cfg:expr, $($arg:tt)*) => {{
let cfg = &$cfg;
if cfg.debug_verbose_enabled() {
cfg.debug_verbose(format_args!($($arg)*))
}
}};
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaInferenceResult {
pub schema: Value,
pub processed_count: usize,
}
#[cfg(feature = "avro")]
impl SchemaInferenceResult {
pub fn to_avro_schema(
&self,
namespace: &str,
utility_namespace: Option<&str>,
base_uri: Option<&str>,
split_top_level: bool,
) -> Value {
let mut schema = self.schema.clone();
type_empty_array_items_as_null(&mut schema);
avrotize::converter::jsons_to_avro(
&schema,
namespace,
utility_namespace.unwrap_or(""),
base_uri.unwrap_or("genson-core"),
split_top_level,
)
}
}
#[cfg(feature = "avro")]
fn type_empty_array_items_as_null(schema: &mut Value) {
match schema {
Value::Object(obj) => {
if let Some(Value::Array(branches)) = obj.get_mut("anyOf") {
type_empty_array_items_by_scalars(branches);
}
if is_untyped_array(obj) {
obj.insert("items".to_string(), serde_json::json!({"type": "null"}));
}
obj.values_mut().for_each(type_empty_array_items_as_null);
}
Value::Array(arr) => arr.iter_mut().for_each(type_empty_array_items_as_null),
_ => {}
}
}
#[cfg(feature = "avro")]
fn is_untyped_array(obj: &serde_json::Map<String, Value>) -> bool {
obj.get("type").and_then(|t| t.as_str()) == Some("array")
&& obj
.get("items")
.is_some_and(|i| i.as_object().is_some_and(|o| o.is_empty()))
}
#[cfg(feature = "avro")]
fn type_empty_array_items_by_scalars(branches: &mut [Value]) {
let mut scalars: Vec<Value> = Vec::new();
for branch in branches.iter() {
let types = match branch.get("type") {
Some(Value::String(t)) => vec![t.as_str()],
Some(Value::Array(ts)) => ts.iter().filter_map(Value::as_str).collect(),
_ => continue,
};
if branch.as_object().is_some_and(|o| o.len() != 1) {
continue;
}
for t in types {
let scalar = matches!(t, "string" | "integer" | "number" | "boolean");
if scalar && !scalars.iter().any(|s| s == t) {
scalars.push(Value::String(t.to_string()));
}
}
}
let items = match scalars.len() {
0 => return,
1 => serde_json::json!({"type": scalars[0]}),
_ => serde_json::json!({"type": scalars}),
};
for branch in branches.iter_mut() {
if let Some(obj) = branch.as_object_mut() {
if is_untyped_array(obj) {
obj.insert("items".to_string(), items.clone());
}
}
}
}
pub fn make_promoted_scalar_key(field_prefix: &str, scalar_type: &str) -> String {
format!("{}__{}", field_prefix, scalar_type)
}