use serde::{Deserialize, Serialize};
use super::identifiers::is_vendor_namespaced_identifier;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Schema {
#[serde(default)]
pub fields: Vec<Field>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypeConversion {
pub from: String,
pub to: String,
#[serde(default)]
pub lossy: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Field {
pub name: String,
#[serde(rename = "type")]
pub type_name: String,
pub nullable: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conversions: Vec<TypeConversion>,
}
pub const PRIMITIVE_TYPES: &[&str] = &[
"boolean", "integer", "decimal", "string", "binary", "date", "time", "datetime", "duration",
];
pub const COMPOSITE_TYPES: &[&str] = &["list", "map", "object", "tuple"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeCompatibility {
Identical,
Compatible,
Incompatible,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogicalType {
Primitive(String),
Composite {
kind: String,
params: Vec<String>,
},
Extension(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeParseError {
Unknown(String),
BareComposite(String),
Malformed(String),
UnknownParameter(String),
InvalidArity {
kind: String,
expected: String,
actual: usize,
},
}
#[must_use]
pub fn is_extension_type_identifier(identifier: &str) -> bool {
let identifier = identifier.trim();
if !is_vendor_namespaced_identifier(identifier) {
return false;
}
let prefix = identifier.split(':').next().unwrap_or("");
!COMPOSITE_TYPES.contains(&prefix) && !PRIMITIVE_TYPES.contains(&prefix)
}
pub fn parse_logical_type(type_name: &str) -> Result<LogicalType, TypeParseError> {
let type_name = type_name.trim();
if type_name.is_empty() {
return Err(TypeParseError::Malformed("type is empty".into()));
}
if PRIMITIVE_TYPES.contains(&type_name) {
return Ok(LogicalType::Primitive(type_name.to_string()));
}
if COMPOSITE_TYPES.contains(&type_name) {
return Err(TypeParseError::BareComposite(type_name.to_string()));
}
if let Some(open) = type_name.find('<') {
let kind = type_name[..open].trim();
if !COMPOSITE_TYPES.contains(&kind) {
if is_extension_type_identifier(type_name) {
return Ok(LogicalType::Extension(type_name.to_string()));
}
return Err(TypeParseError::Unknown(kind.to_string()));
}
let Some(close) = find_matching_close(type_name, open) else {
return Err(TypeParseError::Malformed(type_name.to_string()));
};
if close <= open {
return Err(TypeParseError::Malformed(type_name.to_string()));
}
if !type_name[close + 1..].trim().is_empty() {
return Err(TypeParseError::Malformed(type_name.to_string()));
}
let inner = type_name[open + 1..close].trim();
if inner.is_empty() {
return Err(TypeParseError::Malformed(type_name.to_string()));
}
let params = split_type_parameters(inner)?;
for param in ¶ms {
if parse_logical_type(param).is_err() {
return Err(TypeParseError::UnknownParameter(param.clone()));
}
}
validate_composite_arity(kind, params.len())?;
return Ok(LogicalType::Composite {
kind: kind.to_string(),
params,
});
}
if is_extension_type_identifier(type_name) {
return Ok(LogicalType::Extension(type_name.to_string()));
}
Err(TypeParseError::Unknown(type_name.to_string()))
}
fn find_matching_close(type_name: &str, open: usize) -> Option<usize> {
let mut depth = 0;
for (offset, ch) in type_name[open..].char_indices() {
match ch {
'<' => depth += 1,
'>' => {
depth -= 1;
if depth == 0 {
return Some(open + offset);
}
}
_ => {}
}
}
None
}
fn split_type_parameters(inner: &str) -> Result<Vec<String>, TypeParseError> {
let mut params = Vec::new();
let mut depth = 0;
let mut start = 0;
for (index, ch) in inner.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth -= 1,
',' if depth == 0 => {
let part = inner[start..index].trim();
if !part.is_empty() {
params.push(part.to_string());
}
start = index + 1;
}
_ => {}
}
}
let part = inner[start..].trim();
if !part.is_empty() {
params.push(part.to_string());
}
if params.is_empty() {
return Err(TypeParseError::Malformed(inner.to_string()));
}
Ok(params)
}
fn validate_composite_arity(kind: &str, actual: usize) -> Result<(), TypeParseError> {
let (expected, valid) = match kind {
"list" => ("exactly 1", actual == 1),
"map" => ("exactly 2", actual == 2),
"object" | "tuple" => ("at least 1", actual >= 1),
_ => return Ok(()),
};
if !valid {
return Err(TypeParseError::InvalidArity {
kind: kind.to_string(),
expected: expected.to_string(),
actual,
});
}
Ok(())
}
#[must_use]
pub fn type_compatible(a: &LogicalType, b: &LogicalType) -> TypeCompatibility {
if a == b {
return TypeCompatibility::Identical;
}
match (a, b) {
(LogicalType::Primitive(a_name), LogicalType::Primitive(b_name))
if (a_name == "integer" && b_name == "decimal")
|| (a_name == "decimal" && b_name == "integer") =>
{
TypeCompatibility::Compatible
}
(
LogicalType::Composite {
kind: a_kind,
params: a_params,
},
LogicalType::Composite {
kind: b_kind,
params: b_params,
},
) if a_kind == b_kind && a_params == b_params => TypeCompatibility::Identical,
(LogicalType::Extension(a_name), LogicalType::Extension(b_name)) if a_name == b_name => {
TypeCompatibility::Identical
}
_ => TypeCompatibility::Incompatible,
}
}
#[must_use]
pub fn infer_logical_type(field: &Field) -> Option<LogicalType> {
parse_logical_type(&field.type_name).ok()
}
#[must_use]
pub fn is_known_logical_type(type_name: &str) -> bool {
parse_logical_type(type_name).is_ok()
}