use serde::{Deserialize, Serialize};
use crate::column::{CanonicalColumnName, SourceColumnName};
use crate::unit::NullValue;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ColumnBinding {
pub physical: SourceColumnName,
pub canonical: CanonicalColumnName,
}
impl ColumnBinding {
pub fn new(
physical: impl Into<SourceColumnName>,
canonical: impl Into<CanonicalColumnName>,
) -> Self {
Self {
physical: physical.into(),
canonical: canonical.into(),
}
}
pub fn identity(name: impl Into<String>) -> Self {
let s = name.into();
Self {
physical: SourceColumnName::new(s.clone()),
canonical: CanonicalColumnName::new(s),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CodomainBinding {
pub physical: SourceColumnName,
pub canonical: CanonicalColumnName,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_null_fill: Option<NullValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub join_null_fill: Option<NullValue>,
}
impl CodomainBinding {
pub fn new(
physical: impl Into<SourceColumnName>,
canonical: impl Into<CanonicalColumnName>,
) -> Self {
Self {
physical: physical.into(),
canonical: canonical.into(),
source_null_fill: None,
join_null_fill: None,
}
}
pub fn with_source_null_fill(mut self, fill: NullValue) -> Self {
self.source_null_fill = Some(fill);
self
}
pub fn with_join_null_fill(mut self, fill: NullValue) -> Self {
self.join_null_fill = Some(fill);
self
}
pub fn as_column_binding(&self) -> ColumnBinding {
ColumnBinding {
physical: self.physical.clone(),
canonical: self.canonical.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn column_binding_identity() {
let b = ColumnBinding::identity("station_name");
assert_eq!(b.physical.as_str(), "station_name");
assert_eq!(b.canonical.as_str(), "station_name");
}
#[test]
fn column_binding_distinct_names() {
let b = ColumnBinding::new("obs_time", "timestamp");
assert_eq!(b.physical.as_str(), "obs_time");
assert_eq!(b.canonical.as_str(), "timestamp");
}
#[test]
fn codomain_binding_default_no_fills() {
let b = CodomainBinding::new("value_mm", "historical_precip");
assert!(b.source_null_fill.is_none());
assert!(b.join_null_fill.is_none());
}
#[test]
fn codomain_binding_with_fills() {
let b = CodomainBinding::new("engine_count", "engines_on_count")
.with_source_null_fill(NullValue::Integer(0))
.with_join_null_fill(NullValue::Integer(0));
assert!(b.source_null_fill.is_some());
assert!(b.join_null_fill.is_some());
}
#[test]
fn codomain_projects_to_column_binding() {
let cd = CodomainBinding::new("v", "value").with_source_null_fill(NullValue::Float(0.0));
let cb = cd.as_column_binding();
assert_eq!(cb.physical.as_str(), "v");
assert_eq!(cb.canonical.as_str(), "value");
}
#[test]
fn serde_roundtrip_column_binding() {
let b = ColumnBinding::new("a", "b");
let json = serde_json::to_string(&b).unwrap();
let back: ColumnBinding = serde_json::from_str(&json).unwrap();
assert_eq!(b, back);
}
#[test]
fn serde_skips_none_null_fills() {
let b = CodomainBinding::new("v", "value");
let json = serde_json::to_string(&b).unwrap();
assert!(!json.contains("source_null_fill"));
assert!(!json.contains("join_null_fill"));
}
}