1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct SchemaInferenceConfig {
7 pub ignore_outer_array: bool,
9 pub delimiter: Option<u8>,
11 pub schema_uri: Option<String>,
13 pub map_threshold: usize,
15 pub map_max_required_keys: Option<usize>,
18 pub unify_maps: bool,
20 pub no_unify: std::collections::HashSet<String>,
22 pub force_field_types: HashMap<String, String>,
24 pub force_parent_field_types: HashMap<String, String>,
28 pub force_scalar_promotion: std::collections::HashSet<String>,
32 pub wrap_scalars: bool,
38 #[serde(default)]
43 pub coerce_strings: bool,
44 pub wrap_root: Option<String>,
48 pub no_root_map: bool,
50 pub max_builders: Option<usize>,
54 #[cfg(feature = "avro")]
56 pub avro: bool,
57 pub debug: bool,
60 pub profile: bool,
62 pub verbosity: DebugVerbosity,
64}
65
66#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
67pub enum DebugVerbosity {
68 #[default]
70 Normal,
71 Verbose,
73}
74
75impl SchemaInferenceConfig {
76 pub(crate) fn profile(&self, args: std::fmt::Arguments) {
77 if self.profile {
78 let message = format!("{}", args);
79 anstream::eprintln!("{}", message);
80 }
81 }
82
83 pub(crate) fn profile_verbose(&self, args: std::fmt::Arguments) {
84 if self.profile && matches!(self.verbosity, DebugVerbosity::Verbose) {
85 let message = format!("{}", args);
86 anstream::eprintln!("{}", message);
87 }
88 }
89
90 pub(crate) fn debug(&self, args: std::fmt::Arguments) {
91 if self.debug {
92 let message = format!("{}", args);
93 anstream::eprintln!("{}", self.maybe_truncate(message));
94 }
95 }
96
97 pub(crate) fn debug_verbose(&self, args: std::fmt::Arguments) {
98 if self.debug_verbose_enabled() {
99 let message = format!("{}", args);
100 anstream::eprintln!("{}", self.maybe_truncate(message));
101 }
102 }
103
104 pub(crate) fn debug_verbose_enabled(&self) -> bool {
106 self.debug && matches!(self.verbosity, DebugVerbosity::Verbose)
107 }
108
109 fn maybe_truncate(&self, message: String) -> String {
110 let lines: Vec<&str> = message.lines().collect();
111
112 if lines.len() > 20 && self.verbosity == DebugVerbosity::Normal {
113 let mut truncated = String::new();
114
115 for line in lines.iter().take(10) {
117 truncated.push_str(line);
118 truncated.push('\n');
119 }
120
121 truncated.push_str(&format!("... ({} lines truncated) ...\n", lines.len() - 15));
122
123 for line in lines.iter().skip(lines.len() - 5) {
125 truncated.push_str(line);
126 truncated.push('\n');
127 }
128
129 truncated
130 } else {
131 message
132 }
133 }
134}
135
136impl Default for SchemaInferenceConfig {
137 fn default() -> Self {
138 Self {
139 ignore_outer_array: true,
140 delimiter: None,
141 schema_uri: Some("AUTO".to_string()),
142 map_threshold: 20,
143 map_max_required_keys: None,
144 unify_maps: false,
145 no_unify: std::collections::HashSet::new(),
146 force_field_types: std::collections::HashMap::new(),
147 force_parent_field_types: std::collections::HashMap::new(),
148 force_scalar_promotion: std::collections::HashSet::new(),
149 wrap_scalars: true,
150 coerce_strings: false,
151 wrap_root: None,
152 no_root_map: true,
153 max_builders: None,
154 #[cfg(feature = "avro")]
155 avro: false,
156 debug: false,
157 profile: false,
158 verbosity: DebugVerbosity::default(),
159 }
160 }
161}
162
163#[macro_export]
164macro_rules! profile {
165 ($cfg:expr, $($arg:tt)*) => {
166 $cfg.profile(format_args!($($arg)*))
167 };
168}
169
170#[macro_export]
171macro_rules! profile_verbose {
172 ($cfg:expr, $($arg:tt)*) => {
173 $cfg.profile_verbose(format_args!($($arg)*))
174 };
175}
176
177#[macro_export]
182macro_rules! debug {
183 ($cfg:expr, $($arg:tt)*) => {{
184 let cfg = &$cfg;
185 if cfg.debug {
186 cfg.debug(format_args!($($arg)*))
187 }
188 }};
189}
190
191#[macro_export]
192macro_rules! debug_verbose {
193 ($cfg:expr, $($arg:tt)*) => {{
194 let cfg = &$cfg;
195 if cfg.debug_verbose_enabled() {
196 cfg.debug_verbose(format_args!($($arg)*))
197 }
198 }};
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct SchemaInferenceResult {
203 pub schema: Value,
204 pub processed_count: usize,
205}
206
207#[cfg(feature = "avro")]
208impl SchemaInferenceResult {
209 pub fn to_avro_schema(
210 &self,
211 namespace: &str,
212 utility_namespace: Option<&str>,
213 base_uri: Option<&str>,
214 split_top_level: bool,
215 ) -> Value {
216 let mut schema = self.schema.clone();
217 type_empty_array_items_as_null(&mut schema);
218 keep_empty_objects_as_records(&mut schema);
219 avrotize::converter::jsons_to_avro(
220 &schema,
221 namespace,
222 utility_namespace.unwrap_or(""),
223 base_uri.unwrap_or("genson-core"),
224 split_top_level,
225 )
226 }
227}
228
229#[cfg(feature = "avro")]
238fn type_empty_array_items_as_null(schema: &mut Value) {
239 match schema {
240 Value::Object(obj) => {
241 if let Some(Value::Array(branches)) = obj.get_mut("anyOf") {
242 type_empty_array_items_by_scalars(branches);
243 }
244 if is_untyped_array(obj) {
245 obj.insert("items".to_string(), serde_json::json!({"type": "null"}));
246 }
247 obj.values_mut().for_each(type_empty_array_items_as_null);
248 }
249 Value::Array(arr) => arr.iter_mut().for_each(type_empty_array_items_as_null),
250 _ => {}
251 }
252}
253
254#[cfg(feature = "avro")]
261fn keep_empty_objects_as_records(schema: &mut Value) {
262 match schema {
263 Value::Object(obj) => {
264 if is_empty_object(obj) {
265 obj.insert("properties".to_string(), serde_json::json!({}));
266 }
267 obj.values_mut().for_each(keep_empty_objects_as_records);
268 }
269 Value::Array(arr) => arr.iter_mut().for_each(keep_empty_objects_as_records),
270 _ => {}
271 }
272}
273
274#[cfg(feature = "avro")]
275fn is_empty_object(obj: &serde_json::Map<String, Value>) -> bool {
276 let object = match obj.get("type") {
277 Some(Value::String(t)) => t == "object",
278 Some(Value::Array(types)) => types.iter().any(|t| t == "object"),
279 _ => false,
280 };
281 object
282 && !["properties", "patternProperties", "additionalProperties"]
283 .iter()
284 .any(|k| obj.contains_key(*k))
285}
286
287#[cfg(feature = "avro")]
288fn is_untyped_array(obj: &serde_json::Map<String, Value>) -> bool {
289 obj.get("type").and_then(|t| t.as_str()) == Some("array")
290 && obj
291 .get("items")
292 .is_some_and(|i| i.as_object().is_some_and(|o| o.is_empty()))
293}
294
295#[cfg(feature = "avro")]
298fn type_empty_array_items_by_scalars(branches: &mut [Value]) {
299 let mut scalars: Vec<Value> = Vec::new();
300 for branch in branches.iter() {
301 let types = match branch.get("type") {
302 Some(Value::String(t)) => vec![t.as_str()],
303 Some(Value::Array(ts)) => ts.iter().filter_map(Value::as_str).collect(),
304 _ => continue,
305 };
306 if branch.as_object().is_some_and(|o| o.len() != 1) {
308 continue;
309 }
310 for t in types {
311 let scalar = matches!(t, "string" | "integer" | "number" | "boolean");
312 if scalar && !scalars.iter().any(|s| s == t) {
313 scalars.push(Value::String(t.to_string()));
314 }
315 }
316 }
317 let items = match scalars.len() {
318 0 => return,
319 1 => serde_json::json!({"type": scalars[0]}),
320 _ => serde_json::json!({"type": scalars}),
321 };
322 for branch in branches.iter_mut() {
323 if let Some(obj) = branch.as_object_mut() {
324 if is_untyped_array(obj) {
325 obj.insert("items".to_string(), items.clone());
326 }
327 }
328 }
329}
330
331pub fn make_promoted_scalar_key(field_prefix: &str, scalar_type: &str) -> String {
336 format!("{}__{}", field_prefix, scalar_type)
338}