use std::collections::BTreeMap; use serde_json::{Map, Value}; use crate::{Schema, SchemaKind}; impl Schema { pub fn from_json_sample(value: &Value) -> Self { match value { Value::Null => Self { kind: SchemaKind::Null, description: None, required: true, nullable: true, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), }, Value::Bool(_) => scalar_schema(SchemaKind::Boolean), Value::String(_) => scalar_schema(SchemaKind::String), Value::Number(number) if number.is_i64() || number.is_u64() => { scalar_schema(SchemaKind::Integer) } Value::Number(_) => scalar_schema(SchemaKind::Number), Value::Array(items) => array_schema(items), Value::Object(fields) => object_schema(fields), } } } fn scalar_schema(kind: SchemaKind) -> Schema { Schema { kind, description: None, required: true, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), } } fn array_schema(items: &[Value]) -> Schema { Schema { kind: SchemaKind::Array, description: None, required: true, nullable: false, default_value: None, fields: BTreeMap::new(), items: items .first() .map(|item| Box::new(Schema::from_json_sample(item))), enum_values: Vec::new(), variants: Vec::new(), } } fn object_schema(fields: &Map) -> Schema { Schema { kind: SchemaKind::Object, description: None, required: true, nullable: false, default_value: None, fields: fields .iter() .map(|(name, value)| (name.clone(), Schema::from_json_sample(value))) .collect(), items: None, enum_values: Vec::new(), variants: Vec::new(), } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use serde_json::json; use crate::{Schema, SchemaKind}; #[test] fn infers_object_schema_from_json_sample() { let schema = Schema::from_json_sample(&json!({ "lead": { "email": "user@example.com", "active": true }, "score": 42 })); assert_eq!(schema.kind, SchemaKind::Object); assert_eq!( schema.field("lead").map(|field| field.kind.clone()), Some(SchemaKind::Object) ); assert_eq!( schema .field("lead") .and_then(|field| field.field("email")) .map(|field| field.kind.clone()), Some(SchemaKind::String) ); assert_eq!( schema.field("score").map(|field| field.kind.clone()), Some(SchemaKind::Integer) ); } #[test] fn infers_array_item_schema_from_first_sample_item() { let schema = Schema::from_json_sample(&json!([ { "id": "lead_01", "tags": ["warm", "priority"] } ])); assert_eq!(schema.kind, SchemaKind::Array); assert_eq!( schema .items .as_deref() .and_then(|item| item.field("id")) .map(|field| field.kind.clone()), Some(SchemaKind::String) ); assert_eq!( schema .items .as_deref() .and_then(|item| item.field("tags")) .map(|field| field.kind.clone()), Some(SchemaKind::Array) ); } #[test] fn infers_null_schema_for_null_sample() { let schema = Schema::from_json_sample(&serde_json::Value::Null); assert_eq!( schema, Schema { kind: SchemaKind::Null, description: None, required: true, nullable: true, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), } ); } }