use crate::naming;
use crate::reader::SqlDialect;
use arrow::datatypes::DataType;
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayElementType {
String,
Number,
Boolean,
Date,
DateTime,
Time,
}
#[derive(Debug, Clone)]
pub struct ColumnInfo {
pub name: String,
pub dtype: DataType,
pub is_discrete: bool,
pub min: Option<ArrayElement>,
pub max: Option<ArrayElement>,
}
pub type Schema = Vec<ColumnInfo>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Mappings {
pub wildcard: bool,
pub aesthetics: HashMap<String, AestheticValue>,
}
impl Mappings {
pub fn new() -> Self {
Self {
wildcard: false,
aesthetics: HashMap::new(),
}
}
pub fn with_wildcard() -> Self {
Self {
wildcard: true,
aesthetics: HashMap::new(),
}
}
pub fn is_empty(&self) -> bool {
!self.wildcard && self.aesthetics.is_empty()
}
pub fn insert(&mut self, aesthetic: impl Into<String>, value: AestheticValue) {
self.aesthetics.insert(aesthetic.into(), value);
}
pub fn insert_column(&mut self, aesthetic: &str, column: &str) {
self.insert(
aesthetic,
AestheticValue::standard_column(crate::naming::aesthetic_column(column)),
);
}
pub fn column_names(&self) -> Vec<String> {
self.aesthetics
.keys()
.map(|k| crate::naming::aesthetic_column(k))
.collect()
}
pub fn get(&self, aesthetic: &str) -> Option<&AestheticValue> {
self.aesthetics.get(aesthetic)
}
pub fn contains_key(&self, aesthetic: &str) -> bool {
self.aesthetics.contains_key(aesthetic)
}
pub fn len(&self) -> usize {
self.aesthetics.len()
}
pub fn transform_to_internal(&mut self, ctx: &super::AestheticContext) {
let original_aesthetics = std::mem::take(&mut self.aesthetics);
for (aesthetic, value) in original_aesthetics {
let internal_name = ctx
.map_user_to_internal(&aesthetic)
.map(|s| s.to_string())
.unwrap_or(aesthetic);
self.aesthetics.insert(internal_name, value);
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DataSource {
Identifier(String),
FilePath(String),
Annotation,
}
impl DataSource {
pub fn as_str(&self) -> &str {
match self {
DataSource::Identifier(s) => s,
DataSource::FilePath(s) => s,
DataSource::Annotation => "__annotation__",
}
}
pub fn is_file(&self) -> bool {
matches!(self, DataSource::FilePath(_))
}
pub fn is_annotation(&self) -> bool {
matches!(self, DataSource::Annotation)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AestheticValue {
Column {
name: String,
original_name: Option<String>,
is_dummy: bool,
},
AnnotationColumn { name: String },
Literal(ParameterValue),
}
impl AestheticValue {
pub fn standard_column(name: impl Into<String>) -> Self {
Self::Column {
name: name.into(),
original_name: None,
is_dummy: false,
}
}
pub fn dummy_column(name: impl Into<String>) -> Self {
Self::Column {
name: name.into(),
original_name: None,
is_dummy: true,
}
}
pub fn column_with_original(name: impl Into<String>, original_name: impl Into<String>) -> Self {
Self::Column {
name: name.into(),
original_name: Some(original_name.into()),
is_dummy: false,
}
}
pub fn annotation_column(name: impl Into<String>) -> Self {
Self::AnnotationColumn { name: name.into() }
}
pub fn column_name(&self) -> Option<&str> {
match self {
Self::Column { name, .. } | Self::AnnotationColumn { name } => Some(name),
_ => None,
}
}
pub fn label_name(&self) -> Option<&str> {
match self {
Self::Column {
name,
original_name,
..
} => Some(original_name.as_deref().unwrap_or(name)),
Self::AnnotationColumn { name } => Some(name),
_ => None,
}
}
pub fn is_dummy(&self) -> bool {
matches!(self, Self::Column { is_dummy: true, .. })
}
pub fn is_annotation(&self) -> bool {
matches!(self, Self::AnnotationColumn { .. })
}
pub fn is_literal(&self) -> bool {
matches!(self, Self::Literal(_))
}
}
impl std::fmt::Display for AestheticValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AestheticValue::Column { name, .. } | AestheticValue::AnnotationColumn { name } => {
write!(f, "{}", name)
}
AestheticValue::Literal(lit) => write!(f, "{}", lit),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DefaultAestheticValue {
Column(&'static str),
String(&'static str),
Number(f64),
Boolean(bool),
Null,
Required,
Delayed,
Dummy,
}
impl DefaultAestheticValue {
pub fn to_parameter_value(&self) -> ParameterValue {
match self {
Self::String(s) => ParameterValue::String(s.to_string()),
Self::Number(n) => ParameterValue::Number(*n),
Self::Boolean(b) => ParameterValue::Boolean(*b),
Self::Column(_) | Self::Null | Self::Required | Self::Delayed | Self::Dummy => {
ParameterValue::Null
}
}
}
pub fn to_aesthetic_value(&self) -> AestheticValue {
match self {
Self::Column(name) => AestheticValue::standard_column(name.to_string()),
_ => AestheticValue::Literal(self.to_parameter_value()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ParameterValue {
String(String),
Number(f64),
Boolean(bool),
Array(Vec<ArrayElement>),
Null,
}
pub type Parameters = HashMap<String, ParameterValue>;
impl std::fmt::Display for ParameterValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParameterValue::String(s) => write!(f, "'{}'", s),
ParameterValue::Number(n) => write!(f, "{}", n),
ParameterValue::Boolean(b) => write!(f, "{}", b),
ParameterValue::Array(arr) => {
write!(f, "[")?;
for (i, elem) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
match elem {
ArrayElement::String(s) => write!(f, "'{}'", s)?,
ArrayElement::Number(n) => write!(f, "{}", n)?,
ArrayElement::Boolean(b) => write!(f, "{}", b)?,
ArrayElement::Null => write!(f, "null")?,
ArrayElement::Date(d) => write!(f, "'{}'", d)?,
ArrayElement::DateTime(dt) => write!(f, "'{}'", dt)?,
ArrayElement::Time(t) => write!(f, "'{}'", t)?,
}
}
write!(f, "]")
}
ParameterValue::Null => write!(f, "null"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ArrayElement {
String(String),
Number(f64),
Boolean(bool),
Null,
Date(i32),
DateTime(i64),
Time(i64),
}
const UNIX_EPOCH_CE_DAYS: i32 = 719163;
fn date_to_iso_string(days: i32) -> String {
NaiveDate::from_num_days_from_ce_opt(days + UNIX_EPOCH_CE_DAYS)
.map(|d| d.format("%Y-%m-%d").to_string())
.unwrap_or_else(|| days.to_string())
}
fn datetime_to_iso_string(micros: i64) -> String {
DateTime::from_timestamp_micros(micros)
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string())
.unwrap_or_else(|| micros.to_string())
}
fn time_to_iso_string(nanos: i64) -> String {
let secs = (nanos / 1_000_000_000) as u32;
let nano_part = (nanos % 1_000_000_000) as u32;
NaiveTime::from_num_seconds_from_midnight_opt(secs, nano_part)
.map(|t| t.format("%H:%M:%S").to_string())
.unwrap_or_else(|| format!("{}ns", nanos))
}
pub fn format_number(n: f64) -> String {
if n.fract() == 0.0 {
format!("{:.0}", n)
} else {
n.to_string()
}
}
fn target_type_name(t: ArrayElementType) -> &'static str {
match t {
ArrayElementType::String => "string",
ArrayElementType::Number => "number",
ArrayElementType::Boolean => "boolean",
ArrayElementType::Date => "date",
ArrayElementType::DateTime => "datetime",
ArrayElementType::Time => "time",
}
}
impl ArrayElement {
pub fn element_type(&self) -> Option<ArrayElementType> {
match self {
Self::String(_) => Some(ArrayElementType::String),
Self::Number(_) => Some(ArrayElementType::Number),
Self::Boolean(_) => Some(ArrayElementType::Boolean),
Self::Date(_) => Some(ArrayElementType::Date),
Self::DateTime(_) => Some(ArrayElementType::DateTime),
Self::Time(_) => Some(ArrayElementType::Time),
Self::Null => None,
}
}
pub fn infer_type(values: &[ArrayElement]) -> Option<ArrayElementType> {
let mut found_bool = false;
let mut found_number = false;
let mut found_date = false;
let mut found_datetime = false;
let mut found_time = false;
let mut found_string = false;
for elem in values {
match elem {
Self::Boolean(_) => found_bool = true,
Self::Number(_) => found_number = true,
Self::Date(_) => found_date = true,
Self::DateTime(_) => found_datetime = true,
Self::Time(_) => found_time = true,
Self::String(_) => found_string = true,
Self::Null => {}
}
}
if found_bool {
Some(ArrayElementType::Boolean)
} else if found_number {
Some(ArrayElementType::Number)
} else if found_date {
Some(ArrayElementType::Date)
} else if found_datetime {
Some(ArrayElementType::DateTime)
} else if found_time {
Some(ArrayElementType::Time)
} else if found_string {
Some(ArrayElementType::String)
} else {
None
}
}
pub fn coerce_to(&self, target: ArrayElementType) -> Result<ArrayElement, String> {
if self.element_type() == Some(target) {
return Ok(self.clone());
}
if matches!(self, Self::Null) {
return Ok(Self::Null);
}
match (self, target) {
(Self::String(s), ArrayElementType::Boolean) => match s.to_lowercase().as_str() {
"true" | "yes" | "1" => Ok(Self::Boolean(true)),
"false" | "no" | "0" => Ok(Self::Boolean(false)),
_ => Err(format!("Cannot coerce string '{}' to boolean", s)),
},
(Self::String(s), ArrayElementType::Number) => s
.parse::<f64>()
.map(Self::Number)
.map_err(|_| format!("Cannot coerce string '{}' to number", s)),
(Self::String(s), ArrayElementType::Date) => {
Self::from_date_string(s).ok_or_else(|| {
format!("Cannot coerce string '{}' to date (expected YYYY-MM-DD)", s)
})
}
(Self::String(s), ArrayElementType::DateTime) => Self::from_datetime_string(s)
.ok_or_else(|| format!("Cannot coerce string '{}' to datetime", s)),
(Self::String(s), ArrayElementType::Time) => Self::from_time_string(s)
.ok_or_else(|| format!("Cannot coerce string '{}' to time (expected HH:MM:SS)", s)),
(Self::String(s), ArrayElementType::String) => Ok(Self::String(s.clone())),
(Self::Number(n), ArrayElementType::Boolean) => Ok(Self::Boolean(*n != 0.0)),
(Self::Number(n), ArrayElementType::String) => Ok(Self::String(format_number(*n))),
(Self::Number(n), ArrayElementType::Date) => Ok(Self::Date(*n as i32)),
(Self::Number(n), ArrayElementType::DateTime) => Ok(Self::DateTime(*n as i64)),
(Self::Number(n), ArrayElementType::Time) => Ok(Self::Time(*n as i64)),
(Self::Boolean(b), ArrayElementType::Number) => {
Ok(Self::Number(if *b { 1.0 } else { 0.0 }))
}
(Self::Boolean(b), ArrayElementType::String) => Ok(Self::String(b.to_string())),
(Self::Boolean(_), ArrayElementType::Date)
| (Self::Boolean(_), ArrayElementType::DateTime)
| (Self::Boolean(_), ArrayElementType::Time) => Err(format!(
"Cannot coerce boolean to {}",
target_type_name(target)
)),
(Self::Date(d), ArrayElementType::String) => Ok(Self::String(date_to_iso_string(*d))),
(Self::Date(d), ArrayElementType::Number) => Ok(Self::Number(*d as f64)),
(Self::DateTime(dt), ArrayElementType::String) => {
Ok(Self::String(datetime_to_iso_string(*dt)))
}
(Self::DateTime(dt), ArrayElementType::Number) => Ok(Self::Number(*dt as f64)),
(Self::Time(t), ArrayElementType::String) => Ok(Self::String(time_to_iso_string(*t))),
(Self::Time(t), ArrayElementType::Number) => Ok(Self::Number(*t as f64)),
(Self::Date(_), ArrayElementType::Boolean)
| (Self::DateTime(_), ArrayElementType::Boolean)
| (Self::Time(_), ArrayElementType::Boolean) => {
Err(format!("Cannot coerce {} to boolean", self.type_name()))
}
(Self::Date(_), ArrayElementType::DateTime)
| (Self::Date(_), ArrayElementType::Time)
| (Self::DateTime(_), ArrayElementType::Date)
| (Self::DateTime(_), ArrayElementType::Time)
| (Self::Time(_), ArrayElementType::Date)
| (Self::Time(_), ArrayElementType::DateTime) => Err(format!(
"Cannot coerce {} to {}",
self.type_name(),
target_type_name(target)
)),
(Self::Number(n), ArrayElementType::Number) => Ok(Self::Number(*n)),
(Self::Boolean(b), ArrayElementType::Boolean) => Ok(Self::Boolean(*b)),
(Self::Date(d), ArrayElementType::Date) => Ok(Self::Date(*d)),
(Self::DateTime(dt), ArrayElementType::DateTime) => Ok(Self::DateTime(*dt)),
(Self::Time(t), ArrayElementType::Time) => Ok(Self::Time(*t)),
(Self::Null, _) => Ok(Self::Null),
}
}
pub fn homogenize(values: &[Self]) -> Vec<Self> {
let Some(target_type) = Self::infer_type(values) else {
return values.to_vec();
};
let coerced: Result<Vec<_>, _> = values
.iter()
.map(|elem| elem.coerce_to(target_type))
.collect();
match coerced {
Ok(coerced_arr) => coerced_arr,
Err(_) => {
values
.iter()
.map(|elem| {
elem.coerce_to(ArrayElementType::String)
.unwrap_or(Self::Null)
})
.collect()
}
}
}
pub fn to_sql(&self, dialect: &dyn SqlDialect) -> String {
match self {
Self::String(s) => naming::quote_literal(s),
Self::Number(n) => n.to_string(),
Self::Boolean(b) => dialect.sql_boolean_literal(*b),
Self::Date(d) => dialect.sql_date_literal(*d),
Self::DateTime(dt) => dialect.sql_datetime_literal(*dt),
Self::Time(t) => dialect.sql_time_literal(*t),
Self::Null => "NULL".to_string(),
}
}
fn type_name(&self) -> &'static str {
match self {
Self::String(_) => "string",
Self::Number(_) => "number",
Self::Boolean(_) => "boolean",
Self::Date(_) => "date",
Self::DateTime(_) => "datetime",
Self::Time(_) => "time",
Self::Null => "null",
}
}
pub fn to_f64(&self) -> Option<f64> {
match self {
Self::Number(n) => Some(*n),
Self::Date(d) => Some(*d as f64),
Self::DateTime(dt) => Some(*dt as f64),
Self::Time(t) => Some(*t as f64),
_ => None,
}
}
pub fn from_date_string(s: &str) -> Option<Self> {
NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.map(|d| Self::Date(d.num_days_from_ce() - UNIX_EPOCH_CE_DAYS))
}
pub fn from_datetime_string(s: &str) -> Option<Self> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(Self::DateTime(dt.timestamp_micros()));
}
for fmt in &[
"%Y-%m-%dT%H:%M:%S%.f%:z", "%Y-%m-%dT%H:%M:%S%:z", "%Y-%m-%dT%H:%M:%S%.f%z", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%d %H:%M:%S%:z", "%Y-%m-%d %H:%M:%S%z", ] {
if let Ok(dt) = DateTime::parse_from_str(s, fmt) {
return Some(Self::DateTime(dt.timestamp_micros()));
}
}
for fmt in &[
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M:%S",
] {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Some(Self::DateTime(dt.and_utc().timestamp_micros()));
}
}
None
}
pub fn from_time_string(s: &str) -> Option<Self> {
for fmt in &["%H:%M:%S%.f", "%H:%M:%S", "%H:%M"] {
if let Ok(t) = NaiveTime::parse_from_str(s, fmt) {
let nanos =
t.num_seconds_from_midnight() as i64 * 1_000_000_000 + t.nanosecond() as i64;
return Some(Self::Time(nanos));
}
}
None
}
pub fn try_as_temporal(self) -> Self {
if let Self::String(ref s) = self {
if let Some(dt) = Self::from_datetime_string(s) {
return dt;
}
if let Some(d) = Self::from_date_string(s) {
return d;
}
if let Some(t) = Self::from_time_string(s) {
return t;
}
}
self
}
pub fn to_key_string(&self) -> String {
match self {
Self::String(s) => s.clone(),
Self::Number(n) => format_number(*n),
Self::Boolean(b) => b.to_string(),
Self::Null => "null".to_string(),
Self::Date(d) => date_to_iso_string(*d),
Self::DateTime(dt) => datetime_to_iso_string(*dt),
Self::Time(t) => time_to_iso_string(*t),
}
}
pub fn to_json(&self) -> serde_json::Value {
match self {
ArrayElement::String(s) => serde_json::Value::String(s.clone()),
ArrayElement::Number(n) => serde_json::json!(n),
ArrayElement::Boolean(b) => serde_json::Value::Bool(*b),
ArrayElement::Null => serde_json::Value::Null,
ArrayElement::Date(d) => serde_json::Value::String(date_to_iso_string(*d)),
ArrayElement::DateTime(dt) => serde_json::Value::String(datetime_to_iso_string(*dt)),
ArrayElement::Time(t) => serde_json::Value::String(time_to_iso_string(*t)),
}
}
pub fn date_to_iso(days: i32) -> String {
date_to_iso_string(days)
}
pub fn datetime_to_iso(micros: i64) -> String {
datetime_to_iso_string(micros)
}
pub fn time_to_iso(nanos: i64) -> String {
time_to_iso_string(nanos)
}
}
impl ParameterValue {
pub fn to_json(&self) -> serde_json::Value {
match self {
ParameterValue::String(s) => serde_json::Value::String(s.clone()),
ParameterValue::Number(n) => serde_json::json!(n),
ParameterValue::Boolean(b) => serde_json::Value::Bool(*b),
ParameterValue::Array(arr) => {
serde_json::Value::Array(arr.iter().map(|e| e.to_json()).collect())
}
ParameterValue::Null => serde_json::Value::Null,
}
}
pub fn is_null(&self) -> bool {
matches!(self, ParameterValue::Null)
}
pub fn as_str(&self) -> Option<&str> {
match self {
ParameterValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_number(&self) -> Option<f64> {
match self {
ParameterValue::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
ParameterValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[ArrayElement]> {
match self {
ParameterValue::Array(arr) => Some(arr),
_ => None,
}
}
pub fn to_sql(&self, dialect: &dyn SqlDialect) -> String {
match self {
ParameterValue::String(s) => naming::quote_literal(s),
ParameterValue::Number(n) => n.to_string(),
ParameterValue::Boolean(b) => dialect.sql_boolean_literal(*b),
ParameterValue::Array(_) => {
panic!("ParameterValue::to_sql() does not support arrays. Arrays in annotation layers should be handled via VALUES clause generation.")
}
ParameterValue::Null => "NULL".to_string(),
}
}
fn to_array_element(&self) -> ArrayElement {
match self {
ParameterValue::Number(num) => ArrayElement::Number(*num),
ParameterValue::String(s) => ArrayElement::String(s.clone()),
ParameterValue::Boolean(b) => ArrayElement::Boolean(*b),
ParameterValue::Null => ArrayElement::Null,
ParameterValue::Array(_) => panic!("Cannot convert Array to single ArrayElement"),
}
}
pub fn rep(self, n: usize) -> Result<Self, crate::GgsqlError> {
match self {
ParameterValue::Array(arr) => {
if arr.len() == 1 {
let element = arr[0].clone();
Ok(ParameterValue::Array(vec![element; n]))
} else if arr.len() == n {
let arr = ArrayElement::homogenize(&arr);
Ok(ParameterValue::Array(arr))
} else {
Err(crate::GgsqlError::InternalError(format!(
"Attempted to recycle array of length {} to length {} (should have been caught earlier)",
arr.len(),
n
)))
}
}
scalar => {
let elem = scalar.to_array_element();
Ok(ParameterValue::Array(vec![elem; n]))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SqlExpression(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CastTargetType {
Number,
Integer,
Date,
DateTime,
Time,
String,
Boolean,
}
impl SqlExpression {
pub fn new(sql: impl Into<String>) -> Self {
Self(sql.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
#[derive(Debug, Clone)]
pub enum DefaultParamValue {
String(&'static str),
Number(f64),
Boolean(bool),
Null,
}
#[derive(Debug, Clone, Copy)]
pub enum TypeConstraint<T> {
Forbidden,
Any,
Constrained(T),
}
#[derive(Debug, Clone, Copy)]
pub struct NumberConstraint {
pub min: Option<f64>,
pub max: Option<f64>,
pub min_exclusive: bool,
pub max_exclusive: bool,
pub whole: bool,
}
impl NumberConstraint {
pub const fn unconstrained() -> Self {
Self {
min: None,
max: None,
min_exclusive: false,
max_exclusive: false,
whole: false,
}
}
pub const fn min(min: f64) -> Self {
Self {
min: Some(min),
max: None,
min_exclusive: false,
max_exclusive: false,
whole: false,
}
}
pub const fn min_exclusive(min: f64) -> Self {
Self {
min: Some(min),
max: None,
min_exclusive: true,
max_exclusive: false,
whole: false,
}
}
pub const fn range(min: f64, max: f64) -> Self {
Self {
min: Some(min),
max: Some(max),
min_exclusive: false,
max_exclusive: false,
whole: false,
}
}
pub const fn count(min: f64) -> Self {
Self {
min: Some(min),
max: None,
min_exclusive: false,
max_exclusive: false,
whole: true,
}
}
pub const fn count_range(min: f64, max: f64) -> Self {
Self {
min: Some(min),
max: Some(max),
min_exclusive: false,
max_exclusive: false,
whole: true,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StringConstraint {
pub allowed_values: &'static [&'static str],
}
impl StringConstraint {
pub const fn unconstrained() -> Self {
Self {
allowed_values: &[],
}
}
pub const fn one_of(values: &'static [&'static str]) -> Self {
Self {
allowed_values: values,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum ArrayElementConstraint {
Any,
Number(NumberConstraint),
String(StringConstraint),
Boolean,
}
#[derive(Debug, Clone, Copy)]
pub struct ArrayConstraint {
pub element: ArrayElementConstraint,
pub min_len: Option<usize>,
pub max_len: Option<usize>,
pub allow_null_elements: bool,
}
impl ArrayConstraint {
pub const fn of_numbers(constraint: NumberConstraint) -> Self {
Self {
element: ArrayElementConstraint::Number(constraint),
min_len: None,
max_len: None,
allow_null_elements: false,
}
}
pub const fn of_numbers_len(constraint: NumberConstraint, len: usize) -> Self {
Self {
element: ArrayElementConstraint::Number(constraint),
min_len: Some(len),
max_len: Some(len),
allow_null_elements: false,
}
}
pub const fn of_strings(constraint: StringConstraint) -> Self {
Self {
element: ArrayElementConstraint::String(constraint),
min_len: None,
max_len: None,
allow_null_elements: false,
}
}
#[allow(dead_code)]
pub const fn of_strings_len(constraint: StringConstraint, len: usize) -> Self {
Self {
element: ArrayElementConstraint::String(constraint),
min_len: Some(len),
max_len: Some(len),
allow_null_elements: false,
}
}
#[allow(dead_code)]
pub const fn any_elements() -> Self {
Self {
element: ArrayElementConstraint::Any,
min_len: None,
max_len: None,
allow_null_elements: true,
}
}
#[allow(dead_code)]
pub const fn with_null_elements(mut self) -> Self {
self.allow_null_elements = true;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParamConstraint {
pub number: TypeConstraint<NumberConstraint>,
pub string: TypeConstraint<StringConstraint>,
pub boolean: TypeConstraint<()>,
pub array: TypeConstraint<ArrayConstraint>,
pub allow_null: bool,
}
impl ParamConstraint {
pub const fn unconstrained() -> Self {
Self {
number: TypeConstraint::Any,
string: TypeConstraint::Any,
boolean: TypeConstraint::Any,
array: TypeConstraint::Any,
allow_null: true,
}
}
pub const fn number(constraint: NumberConstraint) -> Self {
Self {
number: TypeConstraint::Constrained(constraint),
string: TypeConstraint::Forbidden,
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Forbidden,
allow_null: true,
}
}
#[allow(dead_code)]
pub const fn number_any() -> Self {
Self {
number: TypeConstraint::Any,
string: TypeConstraint::Forbidden,
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Forbidden,
allow_null: true,
}
}
pub const fn string_option(values: &'static [&'static str]) -> Self {
Self {
number: TypeConstraint::Forbidden,
string: TypeConstraint::Constrained(StringConstraint::one_of(values)),
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Forbidden,
allow_null: true,
}
}
pub const fn string() -> Self {
Self {
number: TypeConstraint::Forbidden,
string: TypeConstraint::Any,
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Forbidden,
allow_null: true,
}
}
pub const fn boolean() -> Self {
Self {
number: TypeConstraint::Forbidden,
string: TypeConstraint::Forbidden,
boolean: TypeConstraint::Any,
array: TypeConstraint::Forbidden,
allow_null: true,
}
}
pub const fn number_or_numeric_array(num: NumberConstraint, arr: ArrayConstraint) -> Self {
Self {
number: TypeConstraint::Constrained(num),
string: TypeConstraint::Forbidden,
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Constrained(arr),
allow_null: true,
}
}
pub const fn number_or_array_or_string(num: NumberConstraint, arr: ArrayConstraint) -> Self {
Self {
number: TypeConstraint::Constrained(num),
string: TypeConstraint::Any, boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Constrained(arr),
allow_null: true,
}
}
#[allow(dead_code)]
pub const fn string_or_string_array(values: &'static [&'static str]) -> Self {
Self {
number: TypeConstraint::Forbidden,
string: TypeConstraint::Constrained(StringConstraint::one_of(values)),
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Constrained(ArrayConstraint::of_strings(
StringConstraint::one_of(values),
)),
allow_null: true,
}
}
pub const fn string_or_string_array_unconstrained() -> Self {
Self {
number: TypeConstraint::Forbidden,
string: TypeConstraint::Any,
boolean: TypeConstraint::Forbidden,
array: TypeConstraint::Constrained(ArrayConstraint {
element: ArrayElementConstraint::String(StringConstraint::unconstrained()),
min_len: None,
max_len: None,
allow_null_elements: true,
}),
allow_null: true,
}
}
#[allow(dead_code)]
pub const fn required(mut self) -> Self {
self.allow_null = false;
self
}
pub const fn number_min(min: f64) -> Self {
Self::number(NumberConstraint::min(min))
}
pub const fn number_min_exclusive(min: f64) -> Self {
Self::number(NumberConstraint::min_exclusive(min))
}
pub const fn number_range(min: f64, max: f64) -> Self {
Self::number(NumberConstraint::range(min, max))
}
pub const fn count(min: f64) -> Self {
Self::number(NumberConstraint::count(min))
}
pub const fn count_range(min: f64, max: f64) -> Self {
Self::number(NumberConstraint::count_range(min, max))
}
}
pub fn validate_parameter(
param_name: &str,
value: &ParameterValue,
constraint: &ParamConstraint,
) -> Result<(), String> {
match value {
ParameterValue::Number(n) => match &constraint.number {
TypeConstraint::Forbidden => {
Err(type_not_allowed_error(param_name, "Number", constraint))
}
TypeConstraint::Any => Ok(()),
TypeConstraint::Constrained(c) => validate_number(param_name, *n, c),
},
ParameterValue::String(s) => match &constraint.string {
TypeConstraint::Forbidden => {
Err(type_not_allowed_error(param_name, "String", constraint))
}
TypeConstraint::Any => Ok(()),
TypeConstraint::Constrained(c) => validate_string(param_name, s, c),
},
ParameterValue::Boolean(_) => match &constraint.boolean {
TypeConstraint::Forbidden => {
Err(type_not_allowed_error(param_name, "Boolean", constraint))
}
TypeConstraint::Any | TypeConstraint::Constrained(()) => Ok(()),
},
ParameterValue::Array(arr) => match &constraint.array {
TypeConstraint::Forbidden => {
Err(type_not_allowed_error(param_name, "Array", constraint))
}
TypeConstraint::Any => Ok(()),
TypeConstraint::Constrained(c) => validate_array(param_name, arr, c),
},
ParameterValue::Null => {
if constraint.allow_null {
Ok(())
} else {
Err(format!(
"Parameter '{}' is required (cannot be null)",
param_name
))
}
}
}
}
fn validate_number(name: &str, n: f64, c: &NumberConstraint) -> Result<(), String> {
if c.whole && n.fract() != 0.0 {
return Err(format!("'{}' should be a whole number, not {}", name, n));
}
if let Some(min) = c.min {
let ok = if c.min_exclusive { n > min } else { n >= min };
if !ok {
let op = if c.min_exclusive { ">" } else { ">=" };
return Err(format!("'{}' should be {} {}, not {}", name, op, min, n));
}
}
if let Some(max) = c.max {
let ok = if c.max_exclusive { n < max } else { n <= max };
if !ok {
let op = if c.max_exclusive { "<" } else { "<=" };
return Err(format!("'{}' should be {} {}, not {}", name, op, max, n));
}
}
Ok(())
}
fn validate_string(name: &str, s: &str, c: &StringConstraint) -> Result<(), String> {
if c.allowed_values.is_empty() {
return Ok(());
}
if !c.allowed_values.contains(&s) {
return Err(format!(
"'{}' should be {}, not '{}'",
name,
crate::or_list_quoted(c.allowed_values, '\''),
s
));
}
Ok(())
}
fn validate_array(name: &str, arr: &[ArrayElement], c: &ArrayConstraint) -> Result<(), String> {
let len = arr.len();
match (c.min_len, c.max_len) {
(Some(min), Some(max)) if min == max && len != min => {
return Err(format!(
"Parameter '{}' array must have exactly {} element(s) (got {})",
name, min, len
));
}
(Some(min), Some(max)) if len < min || len > max => {
return Err(format!(
"Parameter '{}' array must have between {} and {} element(s) (got {})",
name, min, max, len
));
}
(Some(min), None) if len < min => {
return Err(format!(
"Parameter '{}' array must have at least {} element(s) (got {})",
name, min, len
));
}
(None, Some(max)) if len > max => {
return Err(format!(
"Parameter '{}' array must have at most {} element(s) (got {})",
name, max, len
));
}
_ => {}
}
let mut errors: Vec<String> = Vec::new();
for (i, elem) in arr.iter().enumerate() {
match (&c.element, elem) {
(_, ArrayElement::Null) if c.allow_null_elements => {}
(_, ArrayElement::Null) => {
errors.push(format!("'{}[{}]' cannot be null", name, i));
}
(ArrayElementConstraint::Any, _) => {}
(ArrayElementConstraint::Number(nc), ArrayElement::Number(n)) => {
if let Err(e) = validate_number(&format!("{}[{}]", name, i), *n, nc) {
errors.push(e);
}
}
(ArrayElementConstraint::Number(_), _) => {
errors.push(format!("'{}[{}]' must be a number", name, i));
}
(ArrayElementConstraint::String(sc), ArrayElement::String(s)) => {
if !sc.allowed_values.is_empty() {
if let Err(e) = validate_string(&format!("{}[{}]", name, i), s, sc) {
errors.push(e);
}
}
}
(ArrayElementConstraint::String(_), _) => {
errors.push(format!("'{}[{}]' must be a string", name, i));
}
(ArrayElementConstraint::Boolean, ArrayElement::Boolean(_)) => {}
(ArrayElementConstraint::Boolean, _) => {
errors.push(format!("'{}[{}]' must be a boolean", name, i));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(format!(
"Parameter '{}' has invalid elements:\n— {}",
name,
errors.join("\n— ")
))
}
}
fn type_not_allowed_error(name: &str, got: &str, c: &ParamConstraint) -> String {
let mut allowed = Vec::new();
if !matches!(c.number, TypeConstraint::Forbidden) {
allowed.push("Number");
}
if !matches!(c.string, TypeConstraint::Forbidden) {
allowed.push("String");
}
if !matches!(c.boolean, TypeConstraint::Forbidden) {
allowed.push("Boolean");
}
if !matches!(c.array, TypeConstraint::Forbidden) {
allowed.push("Array");
}
format!(
"'{}' should be {}, not {}",
name,
crate::or_list(&allowed),
got
)
}
#[derive(Debug, Clone)]
pub struct ParamDefinition {
pub name: &'static str,
pub default: DefaultParamValue,
pub constraint: ParamConstraint,
}
impl ParamDefinition {
pub fn to_parameter_value(&self) -> Option<ParameterValue> {
match &self.default {
DefaultParamValue::String(s) => Some(ParameterValue::String(s.to_string())),
DefaultParamValue::Number(n) => Some(ParameterValue::Number(*n)),
DefaultParamValue::Boolean(b) => Some(ParameterValue::Boolean(*b)),
DefaultParamValue::Null => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_date_from_string() {
let elem = ArrayElement::from_date_string("2024-01-15").unwrap();
assert!(matches!(elem, ArrayElement::Date(_)));
assert_eq!(elem.to_key_string(), "2024-01-15");
}
#[test]
fn test_date_from_string_roundtrip() {
let original = "2024-06-30";
let elem = ArrayElement::from_date_string(original).unwrap();
assert_eq!(elem.to_key_string(), original);
}
#[test]
fn test_datetime_from_string() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
assert!(elem.to_key_string().starts_with("2024-01-15T10:30:00"));
}
#[test]
fn test_datetime_from_string_with_space() {
let elem = ArrayElement::from_datetime_string("2024-01-15 10:30:00").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
}
#[test]
fn test_datetime_from_string_with_z() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00Z").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
assert_eq!(elem.to_key_string(), "2024-01-15T10:30:00");
}
#[test]
fn test_datetime_from_string_with_positive_offset() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00+05:30").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
assert_eq!(elem.to_key_string(), "2024-01-15T05:00:00");
}
#[test]
fn test_datetime_from_string_with_negative_offset() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00-08:00").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
assert_eq!(elem.to_key_string(), "2024-01-15T18:30:00");
}
#[test]
fn test_datetime_from_string_with_zero_offset() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00+00:00").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
assert_eq!(elem.to_key_string(), "2024-01-15T10:30:00");
}
#[test]
fn test_datetime_from_string_with_fractional_and_tz() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00.123Z").unwrap();
assert!(matches!(elem, ArrayElement::DateTime(_)));
}
#[test]
fn test_time_from_string() {
let elem = ArrayElement::from_time_string("14:30:00").unwrap();
assert!(matches!(elem, ArrayElement::Time(_)));
assert_eq!(elem.to_key_string(), "14:30:00");
}
#[test]
fn test_time_from_string_with_millis() {
let elem = ArrayElement::from_time_string("14:30:00.123").unwrap();
assert!(matches!(elem, ArrayElement::Time(_)));
}
#[test]
fn test_time_from_string_short() {
let elem = ArrayElement::from_time_string("14:30").unwrap();
assert!(matches!(elem, ArrayElement::Time(_)));
assert_eq!(elem.to_key_string(), "14:30:00");
}
#[test]
fn test_date_to_f64() {
let elem = ArrayElement::from_date_string("2024-01-15").unwrap();
let days = elem.to_f64().unwrap();
assert!(days > 19000.0 && days < 20000.0);
}
#[test]
fn test_time_to_f64() {
let elem = ArrayElement::from_time_string("12:00:00").unwrap();
let nanos = elem.to_f64().unwrap();
assert_eq!(nanos, 43_200_000_000_000.0);
}
#[test]
fn test_date_to_json() {
let elem = ArrayElement::from_date_string("2024-01-15").unwrap();
let json = elem.to_json();
assert_eq!(json, serde_json::json!("2024-01-15"));
}
#[test]
fn test_datetime_to_json() {
let elem = ArrayElement::from_datetime_string("2024-01-15T10:30:00").unwrap();
let json = elem.to_json();
assert!(json.is_string());
assert!(json.as_str().unwrap().starts_with("2024-01-15T10:30:00"));
}
#[test]
fn test_time_to_json() {
let elem = ArrayElement::from_time_string("14:30:00").unwrap();
let json = elem.to_json();
assert_eq!(json, serde_json::json!("14:30:00"));
}
#[test]
fn test_number_to_f64() {
let elem = ArrayElement::Number(42.5);
assert_eq!(elem.to_f64(), Some(42.5));
}
#[test]
fn test_string_to_f64_returns_none() {
let elem = ArrayElement::String("hello".to_string());
assert_eq!(elem.to_f64(), None);
}
#[test]
fn test_to_key_string_number_integer() {
let elem = ArrayElement::Number(25.0);
assert_eq!(elem.to_key_string(), "25");
}
#[test]
fn test_to_key_string_number_decimal() {
let elem = ArrayElement::Number(25.5);
assert_eq!(elem.to_key_string(), "25.5");
}
#[test]
fn test_invalid_date_returns_none() {
assert!(ArrayElement::from_date_string("not-a-date").is_none());
assert!(ArrayElement::from_date_string("2024/01/15").is_none());
}
#[test]
fn test_invalid_time_returns_none() {
assert!(ArrayElement::from_time_string("not-a-time").is_none());
assert!(ArrayElement::from_time_string("25:00:00").is_none());
}
#[test]
fn test_element_type() {
assert_eq!(
ArrayElement::String("hello".to_string()).element_type(),
Some(ArrayElementType::String)
);
assert_eq!(
ArrayElement::Number(42.0).element_type(),
Some(ArrayElementType::Number)
);
assert_eq!(
ArrayElement::Boolean(true).element_type(),
Some(ArrayElementType::Boolean)
);
assert_eq!(
ArrayElement::Date(100).element_type(),
Some(ArrayElementType::Date)
);
assert_eq!(
ArrayElement::DateTime(1000000).element_type(),
Some(ArrayElementType::DateTime)
);
assert_eq!(
ArrayElement::Time(1000000000).element_type(),
Some(ArrayElementType::Time)
);
assert_eq!(ArrayElement::Null.element_type(), None);
}
#[test]
fn test_infer_type_boolean() {
let values = vec![ArrayElement::Boolean(true), ArrayElement::Boolean(false)];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::Boolean)
);
}
#[test]
fn test_infer_type_number() {
let values = vec![ArrayElement::Number(1.0), ArrayElement::Number(2.0)];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::Number)
);
}
#[test]
fn test_infer_type_string() {
let values = vec![
ArrayElement::String("a".to_string()),
ArrayElement::String("b".to_string()),
];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::String)
);
}
#[test]
fn test_infer_type_date() {
let values = vec![ArrayElement::Date(100), ArrayElement::Date(200)];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::Date)
);
}
#[test]
fn test_infer_type_with_nulls() {
let values = vec![
ArrayElement::Null,
ArrayElement::Boolean(true),
ArrayElement::Null,
];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::Boolean)
);
}
#[test]
fn test_infer_type_all_nulls() {
let values = vec![ArrayElement::Null, ArrayElement::Null];
assert_eq!(ArrayElement::infer_type(&values), None);
}
#[test]
fn test_infer_type_empty() {
let values: Vec<ArrayElement> = vec![];
assert_eq!(ArrayElement::infer_type(&values), None);
}
#[test]
fn test_infer_type_priority_boolean_over_string() {
let values = vec![
ArrayElement::Boolean(true),
ArrayElement::String("hello".to_string()),
];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::Boolean)
);
}
#[test]
fn test_infer_type_priority_number_over_string() {
let values = vec![
ArrayElement::Number(42.0),
ArrayElement::String("hello".to_string()),
];
assert_eq!(
ArrayElement::infer_type(&values),
Some(ArrayElementType::Number)
);
}
#[test]
fn test_coerce_string_to_boolean_true() {
let elem = ArrayElement::String("true".to_string());
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(true));
let elem = ArrayElement::String("TRUE".to_string());
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(true));
}
#[test]
fn test_coerce_string_to_boolean_false() {
let elem = ArrayElement::String("false".to_string());
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(false));
let elem = ArrayElement::String("no".to_string());
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(false));
}
#[test]
fn test_coerce_string_to_boolean_error() {
let elem = ArrayElement::String("maybe".to_string());
let result = elem.coerce_to(ArrayElementType::Boolean);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Cannot coerce string 'maybe' to boolean"));
}
#[test]
fn test_coerce_string_to_number() {
let elem = ArrayElement::String("42.5".to_string());
let result = elem.coerce_to(ArrayElementType::Number).unwrap();
assert_eq!(result, ArrayElement::Number(42.5));
}
#[test]
fn test_coerce_string_to_number_error() {
let elem = ArrayElement::String("not a number".to_string());
let result = elem.coerce_to(ArrayElementType::Number);
assert!(result.is_err());
}
#[test]
fn test_coerce_string_to_date() {
let elem = ArrayElement::String("2024-01-15".to_string());
let result = elem.coerce_to(ArrayElementType::Date).unwrap();
assert!(matches!(result, ArrayElement::Date(_)));
assert_eq!(result.to_key_string(), "2024-01-15");
}
#[test]
fn test_coerce_string_to_date_error() {
let elem = ArrayElement::String("not-a-date".to_string());
let result = elem.coerce_to(ArrayElementType::Date);
assert!(result.is_err());
}
#[test]
fn test_coerce_number_to_boolean() {
let elem = ArrayElement::Number(1.0);
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(true));
let elem = ArrayElement::Number(0.0);
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(false));
}
#[test]
fn test_coerce_number_to_string() {
let elem = ArrayElement::Number(42.5);
let result = elem.coerce_to(ArrayElementType::String).unwrap();
assert_eq!(result, ArrayElement::String("42.5".to_string()));
let elem = ArrayElement::Number(42.0);
let result = elem.coerce_to(ArrayElementType::String).unwrap();
assert_eq!(result, ArrayElement::String("42".to_string()));
}
#[test]
fn test_coerce_boolean_to_number() {
let elem = ArrayElement::Boolean(true);
let result = elem.coerce_to(ArrayElementType::Number).unwrap();
assert_eq!(result, ArrayElement::Number(1.0));
let elem = ArrayElement::Boolean(false);
let result = elem.coerce_to(ArrayElementType::Number).unwrap();
assert_eq!(result, ArrayElement::Number(0.0));
}
#[test]
fn test_coerce_boolean_to_string() {
let elem = ArrayElement::Boolean(true);
let result = elem.coerce_to(ArrayElementType::String).unwrap();
assert_eq!(result, ArrayElement::String("true".to_string()));
}
#[test]
fn test_coerce_null_stays_null() {
let elem = ArrayElement::Null;
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Null);
let result = elem.coerce_to(ArrayElementType::Number).unwrap();
assert_eq!(result, ArrayElement::Null);
}
#[test]
fn test_coerce_same_type_identity() {
let elem = ArrayElement::Boolean(true);
let result = elem.coerce_to(ArrayElementType::Boolean).unwrap();
assert_eq!(result, ArrayElement::Boolean(true));
let elem = ArrayElement::Number(42.0);
let result = elem.coerce_to(ArrayElementType::Number).unwrap();
assert_eq!(result, ArrayElement::Number(42.0));
}
#[test]
fn test_coerce_date_to_string() {
let elem = ArrayElement::from_date_string("2024-01-15").unwrap();
let result = elem.coerce_to(ArrayElementType::String).unwrap();
assert_eq!(result, ArrayElement::String("2024-01-15".to_string()));
}
#[test]
fn test_coerce_cross_temporal_not_supported() {
let elem = ArrayElement::Date(100);
let result = elem.coerce_to(ArrayElementType::DateTime);
assert!(result.is_err());
let elem = ArrayElement::DateTime(100000);
let result = elem.coerce_to(ArrayElementType::Date);
assert!(result.is_err());
}
#[test]
fn test_number_constraint_accepts_valid() {
let constraint = ParamConstraint::number_min(0.0);
assert!(validate_parameter("test", &ParameterValue::Number(5.0), &constraint).is_ok());
assert!(validate_parameter("test", &ParameterValue::Number(0.0), &constraint).is_ok());
}
#[test]
fn test_number_constraint_rejects_invalid() {
let constraint = ParamConstraint::number_min(0.0);
let result = validate_parameter("test", &ParameterValue::Number(-1.0), &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains(">= 0"));
}
#[test]
fn test_number_constraint_rejects_wrong_type() {
let constraint = ParamConstraint::number_min(0.0);
let result = validate_parameter(
"test",
&ParameterValue::String("hello".to_string()),
&constraint,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("should be Number"));
}
#[test]
fn test_count_constraint_accepts_whole() {
let constraint = ParamConstraint::count(1.0);
assert!(validate_parameter("bins", &ParameterValue::Number(5.0), &constraint).is_ok());
assert!(validate_parameter("bins", &ParameterValue::Number(1.0), &constraint).is_ok());
assert!(validate_parameter("bins", &ParameterValue::Number(100.0), &constraint).is_ok());
}
#[test]
fn test_count_constraint_rejects_fractional() {
let constraint = ParamConstraint::count(1.0);
let result = validate_parameter("bins", &ParameterValue::Number(5.5), &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("whole number"));
}
#[test]
fn test_count_constraint_rejects_below_min() {
let constraint = ParamConstraint::count(1.0);
let result = validate_parameter("bins", &ParameterValue::Number(0.0), &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains(">= 1"));
}
#[test]
fn test_string_option_accepts_valid() {
let constraint = ParamConstraint::string_option(&["a", "b", "c"]);
assert!(validate_parameter(
"test",
&ParameterValue::String("a".to_string()),
&constraint
)
.is_ok());
}
#[test]
fn test_string_option_rejects_invalid() {
let constraint = ParamConstraint::string_option(&["a", "b", "c"]);
let result = validate_parameter(
"test",
&ParameterValue::String("d".to_string()),
&constraint,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("not 'd'"));
}
#[test]
fn test_string_option_rejects_wrong_type() {
let constraint = ParamConstraint::string_option(&["a", "b", "c"]);
let result = validate_parameter("test", &ParameterValue::Number(1.0), &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("should be String"));
}
#[test]
fn test_boolean_accepts_valid() {
let constraint = ParamConstraint::boolean();
assert!(validate_parameter("reverse", &ParameterValue::Boolean(true), &constraint).is_ok());
assert!(
validate_parameter("reverse", &ParameterValue::Boolean(false), &constraint).is_ok()
);
}
#[test]
fn test_boolean_rejects_wrong_type() {
let constraint = ParamConstraint::boolean();
let result = validate_parameter(
"reverse",
&ParameterValue::String("true".to_string()),
&constraint,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("should be Boolean"));
}
#[test]
fn test_multi_type_number_or_array_accepts_number() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers_len(NumberConstraint::unconstrained(), 2),
);
assert!(validate_parameter("expand", &ParameterValue::Number(0.05), &constraint).is_ok());
}
#[test]
fn test_multi_type_number_or_array_accepts_array() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers_len(NumberConstraint::min(0.0), 2),
);
let arr =
ParameterValue::Array(vec![ArrayElement::Number(0.05), ArrayElement::Number(10.0)]);
assert!(validate_parameter("expand", &arr, &constraint).is_ok());
}
#[test]
fn test_multi_type_number_or_array_rejects_wrong_array_length() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers_len(NumberConstraint::min(0.0), 2),
);
let arr = ParameterValue::Array(vec![ArrayElement::Number(0.05)]);
let result = validate_parameter("expand", &arr, &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("exactly 2"));
}
#[test]
fn test_multi_type_number_or_array_rejects_string() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers_len(NumberConstraint::min(0.0), 2),
);
let result = validate_parameter(
"expand",
&ParameterValue::String("0.05".to_string()),
&constraint,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("should be Number or Array"));
}
#[test]
fn test_multi_type_number_or_array_validates_element_values() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers_len(NumberConstraint::min(0.0), 2),
);
let arr = ParameterValue::Array(vec![
ArrayElement::Number(0.05),
ArrayElement::Number(-10.0), ]);
let result = validate_parameter("expand", &arr, &constraint);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("expand[1]"));
assert!(err.contains(">= 0"));
}
#[test]
fn test_breaks_constraint_accepts_number() {
let constraint = ParamConstraint::number_or_array_or_string(
NumberConstraint::min(1.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
assert!(validate_parameter("breaks", &ParameterValue::Number(10.0), &constraint).is_ok());
}
#[test]
fn test_breaks_constraint_accepts_array() {
let constraint = ParamConstraint::number_or_array_or_string(
NumberConstraint::min(1.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
let arr = ParameterValue::Array(vec![
ArrayElement::Number(0.0),
ArrayElement::Number(25.0),
ArrayElement::Number(50.0),
]);
assert!(validate_parameter("breaks", &arr, &constraint).is_ok());
}
#[test]
fn test_breaks_constraint_accepts_string() {
let constraint = ParamConstraint::number_or_array_or_string(
NumberConstraint::min(1.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
assert!(validate_parameter(
"breaks",
&ParameterValue::String("1 month".to_string()),
&constraint
)
.is_ok());
}
#[test]
fn test_breaks_constraint_rejects_boolean() {
let constraint = ParamConstraint::number_or_array_or_string(
NumberConstraint::min(1.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
let result = validate_parameter("breaks", &ParameterValue::Boolean(true), &constraint);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("should be Number, String, or Array"));
}
#[test]
fn test_breaks_constraint_validates_number_value() {
let constraint = ParamConstraint::number_or_array_or_string(
NumberConstraint::min(1.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
let result = validate_parameter("breaks", &ParameterValue::Number(0.0), &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains(">= 1"));
}
#[test]
fn test_array_element_type_validation() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
let arr = ParameterValue::Array(vec![
ArrayElement::Number(1.0),
ArrayElement::String("fifty".to_string()),
ArrayElement::Number(100.0),
]);
let result = validate_parameter("breaks", &arr, &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("'breaks[1]' must be a number"));
}
#[test]
fn test_null_allowed_by_default() {
let constraint = ParamConstraint::number_min(1.0);
assert!(validate_parameter("test", &ParameterValue::Null, &constraint).is_ok());
}
#[test]
fn test_null_rejected_when_required() {
let constraint = ParamConstraint::number_min(1.0).required();
let result = validate_parameter("test", &ParameterValue::Null, &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("required"));
}
#[test]
fn test_array_null_elements_rejected_by_default() {
let constraint = ParamConstraint::number_or_numeric_array(
NumberConstraint::min(0.0),
ArrayConstraint::of_numbers(NumberConstraint::unconstrained()),
);
let arr = ParameterValue::Array(vec![
ArrayElement::Number(1.0),
ArrayElement::Null,
ArrayElement::Number(3.0),
]);
let result = validate_parameter("values", &arr, &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("'values[1]' cannot be null"));
}
#[test]
fn test_string_or_string_array_accepts_string() {
let constraint = ParamConstraint::string_or_string_array(&["x", "y"]);
assert!(validate_parameter(
"free",
&ParameterValue::String("x".to_string()),
&constraint
)
.is_ok());
}
#[test]
fn test_string_or_string_array_accepts_array() {
let constraint = ParamConstraint::string_or_string_array(&["x", "y"]);
let arr = ParameterValue::Array(vec![
ArrayElement::String("x".to_string()),
ArrayElement::String("y".to_string()),
]);
assert!(validate_parameter("free", &arr, &constraint).is_ok());
}
#[test]
fn test_string_or_string_array_validates_array_elements() {
let constraint = ParamConstraint::string_or_string_array(&["x", "y"]);
let arr = ParameterValue::Array(vec![
ArrayElement::String("x".to_string()),
ArrayElement::String("z".to_string()),
]);
let result = validate_parameter("free", &arr, &constraint);
assert!(result.is_err());
assert!(result.unwrap_err().contains("not 'z'"));
}
#[test]
fn test_homogenize_mixed_number_string() {
let arr = vec![
ArrayElement::Number(1.0),
ArrayElement::String("foo".to_string()),
];
let homogenized = ArrayElement::homogenize(&arr);
assert_eq!(homogenized.len(), 2);
assert!(matches!(homogenized[0], ArrayElement::String(_)));
assert!(matches!(homogenized[1], ArrayElement::String(_)));
if let ArrayElement::String(s) = &homogenized[0] {
assert_eq!(s, "1");
}
if let ArrayElement::String(s) = &homogenized[1] {
assert_eq!(s, "foo");
}
}
#[test]
fn test_try_as_temporal_date() {
let elem = ArrayElement::String("1973-06-01".to_string());
let parsed = elem.try_as_temporal();
assert!(matches!(parsed, ArrayElement::Date(_)));
assert_eq!(parsed.to_key_string(), "1973-06-01");
}
#[test]
fn test_try_as_temporal_datetime() {
let elem = ArrayElement::String("2024-03-17T14:30:00".to_string());
let parsed = elem.try_as_temporal();
assert!(matches!(parsed, ArrayElement::DateTime(_)));
}
#[test]
fn test_try_as_temporal_time() {
let elem = ArrayElement::String("14:30:00".to_string());
let parsed = elem.try_as_temporal();
assert!(matches!(parsed, ArrayElement::Time(_)));
}
#[test]
fn test_try_as_temporal_non_temporal_string() {
let elem = ArrayElement::String("not a date".to_string());
let parsed = elem.try_as_temporal();
assert!(matches!(parsed, ArrayElement::String(_)));
assert_eq!(parsed.to_key_string(), "not a date");
}
#[test]
fn test_try_as_temporal_non_string() {
let elem = ArrayElement::Number(42.0);
let parsed = elem.try_as_temporal();
assert!(matches!(parsed, ArrayElement::Number(_)));
let elem = ArrayElement::Boolean(true);
let parsed = elem.try_as_temporal();
assert!(matches!(parsed, ArrayElement::Boolean(_)));
}
}