use std::fmt;
use serde::{Deserialize, Serialize};
use crate::datatypes::values::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum DeclaredType {
Boolean,
String,
Integer,
Float,
Date,
LocalDateTime,
Duration,
Point,
}
impl DeclaredType {
pub fn resolve(declared: &str) -> Option<Self> {
match normalize(declared).as_str() {
"BOOLEAN" => Some(Self::Boolean),
"STRING" => Some(Self::String),
"INTEGER" => Some(Self::Integer),
"FLOAT" => Some(Self::Float),
"DATE" => Some(Self::Date),
"LOCAL DATETIME" => Some(Self::LocalDateTime),
"DURATION" => Some(Self::Duration),
"POINT" => Some(Self::Point),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Boolean => "BOOLEAN",
Self::String => "STRING",
Self::Integer => "INTEGER",
Self::Float => "FLOAT",
Self::Date => "DATE",
Self::LocalDateTime => "LOCAL DATETIME",
Self::Duration => "DURATION",
Self::Point => "POINT",
}
}
pub fn accepts(&self, value: &Value) -> bool {
if matches!(value, Value::Null) {
return true;
}
match self {
Self::Boolean => matches!(value, Value::Boolean(_)),
Self::String => matches!(value, Value::String(_)),
Self::Integer => matches!(value, Value::Int64(_) | Value::UniqueId(_)),
Self::Float => matches!(value, Value::Float64(_)),
Self::Date => matches!(value, Value::DateTime(_)),
Self::LocalDateTime => matches!(value, Value::Timestamp(_)),
Self::Duration => matches!(value, Value::Duration { .. }),
Self::Point => matches!(value, Value::Point { .. }),
}
}
pub fn accepted_names() -> &'static [&'static str] {
&[
"BOOLEAN",
"STRING",
"INTEGER",
"FLOAT",
"DATE",
"LOCAL DATETIME",
"DURATION",
"POINT",
]
}
}
impl fmt::Display for DeclaredType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
fn normalize(declared: &str) -> String {
declared
.split_whitespace()
.map(str::to_uppercase)
.collect::<Vec<_>>()
.join(" ")
}
pub fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "NULL",
Value::Boolean(_) => "BOOLEAN",
Value::String(_) => "STRING",
Value::Int64(_) | Value::UniqueId(_) => "INTEGER",
Value::Float64(_) => "FLOAT",
Value::DateTime(_) => "DATE",
Value::Timestamp(_) => "LOCAL DATETIME",
Value::Duration { .. } => "DURATION",
Value::Point { .. } => "POINT",
Value::List(_) => "LIST",
Value::Map(_) => "MAP",
Value::Node(_) | Value::NodeRef(_) => "NODE",
Value::Relationship(_) => "RELATIONSHIP",
Value::Path(_) => "PATH",
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, NaiveDateTime};
fn all_values() -> Vec<(&'static str, Value)> {
vec![
("boolean", Value::Boolean(true)),
("string", Value::String("x".to_string())),
("int64", Value::Int64(7)),
("unique_id", Value::UniqueId(7)),
("float64", Value::Float64(7.0)),
(
"date",
Value::DateTime(NaiveDate::from_ymd_opt(2026, 8, 18).unwrap()),
),
(
"timestamp",
Value::Timestamp(
NaiveDateTime::parse_from_str("2026-08-18 09:30:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),
),
),
(
"duration",
Value::Duration {
months: 1,
days: 2,
seconds: 3,
},
),
("point", Value::Point { lat: 1.0, lon: 2.0 }),
("list", Value::List(vec![Value::Int64(1)])),
("map", Value::Map(crate::datatypes::PropMap::new())),
]
}
fn accepted_shapes(declared: DeclaredType) -> &'static [&'static str] {
match declared {
DeclaredType::Boolean => &["boolean"],
DeclaredType::String => &["string"],
DeclaredType::Integer => &["int64", "unique_id"],
DeclaredType::Float => &["float64"],
DeclaredType::Date => &["date"],
DeclaredType::LocalDateTime => &["timestamp"],
DeclaredType::Duration => &["duration"],
DeclaredType::Point => &["point"],
}
}
fn every_declared_type() -> Vec<DeclaredType> {
DeclaredType::accepted_names()
.iter()
.map(|name| {
DeclaredType::resolve(name)
.unwrap_or_else(|| panic!("{name} is advertised but does not resolve"))
})
.collect()
}
#[test]
fn accept_matrix_admits_exactly_the_mapped_variant() {
for declared in every_declared_type() {
let accepted = accepted_shapes(declared);
for (shape, value) in all_values() {
let expected = accepted.contains(&shape);
assert_eq!(
declared.accepts(&value),
expected,
"{declared} vs {shape}: expected accepts() == {expected}"
);
}
}
}
#[test]
fn null_satisfies_every_declared_type() {
for declared in every_declared_type() {
assert!(
declared.accepts(&Value::Null),
"{declared} must admit null — combine with NOT NULL for presence"
);
}
}
#[test]
fn integer_admits_ids_and_float_admits_no_integer() {
assert!(DeclaredType::Integer.accepts(&Value::UniqueId(1)));
assert!(DeclaredType::Integer.accepts(&Value::Int64(1)));
assert!(!DeclaredType::Float.accepts(&Value::Int64(1)));
assert!(!DeclaredType::Float.accepts(&Value::UniqueId(1)));
assert!(!DeclaredType::Integer.accepts(&Value::Float64(1.0)));
}
#[test]
fn canonical_spellings_resolve_case_and_space_insensitively() {
assert_eq!(DeclaredType::resolve("STRING"), Some(DeclaredType::String));
assert_eq!(DeclaredType::resolve("string"), Some(DeclaredType::String));
assert_eq!(
DeclaredType::resolve(" String "),
Some(DeclaredType::String)
);
assert_eq!(
DeclaredType::resolve("local datetime"),
Some(DeclaredType::LocalDateTime)
);
assert_eq!(
DeclaredType::resolve("LOCAL DATETIME"),
Some(DeclaredType::LocalDateTime)
);
}
#[test]
fn permissive_and_decorated_names_are_rejected() {
for name in [
"str",
"int",
"i64",
"int64",
"double",
"number",
"float64",
"bool",
"uniqueid",
"timestamp",
"datetime",
"null",
"any",
"ZONED DATETIME",
"LOCAL TIME",
"ZONED TIME",
"MAP",
"NODE",
"RELATIONSHIP",
"PATH",
"LIST < STRING >",
"LIST<STRING>",
"STRING NOT NULL",
"INTEGER | STRING",
"",
" ",
"STRINGY",
"strings",
"42",
] {
assert_eq!(
DeclaredType::resolve(name),
None,
"{name:?} must not resolve — the accept-list is closed"
);
}
}
#[test]
fn every_advertised_name_round_trips_through_its_canonical_spelling() {
for declared in every_declared_type() {
assert_eq!(DeclaredType::resolve(declared.name()), Some(declared));
assert_eq!(declared.to_string(), declared.name());
}
}
#[test]
fn value_type_names_use_the_declaration_vocabulary() {
assert_eq!(value_type_name(&Value::String("x".to_string())), "STRING");
assert_eq!(value_type_name(&Value::Int64(1)), "INTEGER");
assert_eq!(value_type_name(&Value::UniqueId(1)), "INTEGER");
assert_eq!(value_type_name(&Value::Float64(1.0)), "FLOAT");
assert_eq!(
value_type_name(&Value::DateTime(
NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
)),
"DATE"
);
assert_eq!(value_type_name(&Value::Null), "NULL");
assert_eq!(value_type_name(&Value::List(Vec::new())), "LIST");
}
#[test]
fn a_values_reported_name_is_a_type_it_satisfies() {
for (shape, value) in all_values() {
let name = value_type_name(&value);
if let Some(declared) = DeclaredType::resolve(name) {
assert!(
declared.accepts(&value),
"{shape} reports as {name} but {name} does not accept it"
);
}
}
}
}