use crate::config::ConnectorSpec;
use faucet_core::FaucetError;
use faucet_core::diff::Normalizer;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const DEFAULT_RANGES: usize = 16;
pub const DEFAULT_LEAF_ROWS: u64 = 1_000;
pub const DEFAULT_MAX_DIFFERENCES: usize = 1_000;
fn default_ranges() -> usize {
DEFAULT_RANGES
}
fn default_leaf_rows() -> u64 {
DEFAULT_LEAF_ROWS
}
fn default_max_differences() -> usize {
DEFAULT_MAX_DIFFERENCES
}
fn default_exclude() -> Vec<String> {
vec!["_faucet_*".to_string()]
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct VerifySpec {
#[serde(default)]
pub key: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<String>>,
#[serde(default = "default_exclude")]
pub exclude: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destination: Option<ConnectorSpec>,
#[serde(default = "default_ranges")]
pub ranges: usize,
#[serde(default = "default_leaf_rows")]
pub leaf_rows: u64,
#[serde(default = "default_max_differences")]
pub max_differences: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_rows_scanned: Option<u64>,
#[serde(default)]
pub normalize: Normalizer,
#[serde(default = "default_true")]
pub after_run: bool,
#[serde(default = "default_true")]
pub fail_on_difference: bool,
#[serde(default)]
pub repair: bool,
#[serde(default)]
pub allow_delete: bool,
}
impl Default for VerifySpec {
fn default() -> Self {
Self {
key: Vec::new(),
columns: None,
exclude: default_exclude(),
destination: None,
ranges: DEFAULT_RANGES,
leaf_rows: DEFAULT_LEAF_ROWS,
max_differences: DEFAULT_MAX_DIFFERENCES,
max_rows_scanned: None,
normalize: Normalizer::default(),
after_run: true,
fail_on_difference: true,
repair: false,
allow_delete: false,
}
}
}
impl VerifySpec {
pub fn validate(&self) -> Result<(), FaucetError> {
if self.ranges == 0 {
return Err(FaucetError::Config(
"verify: `ranges` must be at least 1".into(),
));
}
if self.leaf_rows == 0 {
return Err(FaucetError::Config(
"verify: `leaf_rows` must be at least 1".into(),
));
}
if self.max_differences == 0 {
return Err(FaucetError::Config(
"verify: `max_differences` must be at least 1".into(),
));
}
if let Some(cols) = &self.columns
&& cols.is_empty()
{
return Err(FaucetError::Config(
"verify: `columns` must list at least one column when set (omit it to compare every column)".into(),
));
}
if self.key.iter().any(|k| k.trim().is_empty()) {
return Err(FaucetError::Config(
"verify: `key` entries must not be empty".into(),
));
}
if let Some(dest) = &self.destination
&& dest.kind.trim().is_empty()
{
return Err(FaucetError::Config(
"verify: `destination.type` must name a source connector".into(),
));
}
self.normalize.validate()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_and_validation() {
let spec: VerifySpec = serde_yaml::from_str("{}").unwrap();
assert_eq!(spec, VerifySpec::default());
assert_eq!(spec.exclude, vec!["_faucet_*".to_string()]);
assert!(spec.after_run && spec.fail_on_difference && !spec.repair);
spec.validate().unwrap();
let bad = VerifySpec {
ranges: 0,
..VerifySpec::default()
};
assert!(bad.validate().unwrap_err().to_string().contains("ranges"));
let bad = VerifySpec {
leaf_rows: 0,
..VerifySpec::default()
};
assert!(
bad.validate()
.unwrap_err()
.to_string()
.contains("leaf_rows")
);
let bad = VerifySpec {
max_differences: 0,
..VerifySpec::default()
};
assert!(
bad.validate()
.unwrap_err()
.to_string()
.contains("max_differences")
);
let bad = VerifySpec {
columns: Some(vec![]),
..VerifySpec::default()
};
assert!(bad.validate().unwrap_err().to_string().contains("columns"));
let bad = VerifySpec {
key: vec![" ".into()],
..VerifySpec::default()
};
assert!(bad.validate().unwrap_err().to_string().contains("key"));
let bad: VerifySpec =
serde_yaml::from_str("destination: { type: '', config: {} }").unwrap();
assert!(
bad.validate()
.unwrap_err()
.to_string()
.contains("destination")
);
}
#[test]
fn unknown_keys_are_rejected() {
assert!(serde_yaml::from_str::<VerifySpec>("leaf: 5").is_err());
}
}