use crate::error::{TraverserError, TraverserResult};
use crate::graph::schema::{ValueType, value_type_name};
use crate::value::uuid as uuid_codec;
use crate::value::{ElementProperty, Number};
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub enum CastPolicy {
#[default]
Error,
Skip,
Default(ElementProperty),
}
impl ElementProperty {
pub fn cast(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
if matches!(self, ElementProperty::Null) {
return Ok(ElementProperty::Null);
}
if let ElementProperty::Compressed(text) = self {
return ElementProperty::String(text.text()).cast(target);
}
match target {
ValueType::Null => self.cast_failure(target),
ValueType::Boolean => self.cast_to_boolean(target),
ValueType::Int64 => self.cast_to_int64(target),
ValueType::Float64 => self.cast_to_float64(target),
ValueType::String => self.cast_to_string(target),
ValueType::Uuid => self.cast_to_uuid(target),
ValueType::Array(_) | ValueType::Object => {
Err(Box::new(TraverserError::unsupported_cast_target(
target.to_string(),
"a container is built by fold() or project(), not by a conversion",
)))
}
ValueType::Union(_) => Err(Box::new(TraverserError::unsupported_cast_target(
target.to_string(),
"a union names no single target; convert to one of its members",
))),
}
}
pub fn uuid_from_string(s: &str) -> TraverserResult<ElementProperty> {
ElementProperty::from(s).cast(&ValueType::Uuid)
}
fn cast_to_string(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
match self {
ElementProperty::String(_) => Ok(self.clone()),
ElementProperty::Boolean(value) => {
Ok(ElementProperty::from(if *value { "true" } else { "false" }))
}
ElementProperty::Number(value) => Ok(ElementProperty::from(value.to_string())),
ElementProperty::Uuid(value) => {
Ok(ElementProperty::from(uuid_codec::format_canonical(*value)))
}
_ => self.cast_failure(target),
}
}
fn cast_to_uuid(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
match self {
ElementProperty::Uuid(_) => Ok(self.clone()),
ElementProperty::String(value) => match uuid_codec::parse_canonical(value) {
Some(parsed) => Ok(ElementProperty::Uuid(parsed)),
None => self.cast_failure(target),
},
_ => self.cast_failure(target),
}
}
fn cast_to_boolean(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
match self {
ElementProperty::Boolean(_) => Ok(self.clone()),
ElementProperty::String(value) => match value.as_str() {
"true" => Ok(ElementProperty::Boolean(true)),
"false" => Ok(ElementProperty::Boolean(false)),
_ => self.cast_failure(target),
},
ElementProperty::Number(value) => match (value.as_i64(), value.as_f64()) {
(Some(0), _) => Ok(ElementProperty::Boolean(false)),
(Some(1), _) => Ok(ElementProperty::Boolean(true)),
(None, Some(0.0)) => Ok(ElementProperty::Boolean(false)),
(None, Some(1.0)) => Ok(ElementProperty::Boolean(true)),
_ => self.cast_failure(target),
},
_ => self.cast_failure(target),
}
}
fn cast_to_int64(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
match self {
ElementProperty::Number(value) => match value.as_i64() {
Some(value) => Ok(ElementProperty::Number(Number::from_i64(value))),
None => match value.as_f64() {
Some(value)
if value.fract() == 0.0
&& value >= i64::MIN as f64
&& value <= i64::MAX as f64 =>
{
Ok(ElementProperty::Number(Number::from_i64(value as i64)))
}
_ => self.cast_failure(target),
},
},
ElementProperty::String(value) => match value.parse::<i64>() {
Ok(value) => Ok(ElementProperty::Number(Number::from_i64(value))),
Err(_) => self.cast_failure(target),
},
ElementProperty::Boolean(value) => {
Ok(ElementProperty::Number(Number::from_i64(if *value {
1
} else {
0
})))
}
_ => self.cast_failure(target),
}
}
fn cast_to_float64(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
match self {
ElementProperty::Number(value) => match (value.as_f64(), value.as_i64()) {
(Some(value), _) => Ok(ElementProperty::Number(Number::from_f64(value))),
(None, Some(value)) => Ok(ElementProperty::Number(Number::from_f64(value as f64))),
(None, None) => self.cast_failure(target),
},
ElementProperty::String(value) => match value.parse::<f64>() {
Ok(value) => Ok(ElementProperty::Number(Number::from_f64(value))),
Err(_) => self.cast_failure(target),
},
ElementProperty::Boolean(value) => {
Ok(ElementProperty::Number(Number::from_f64(if *value {
1.0
} else {
0.0
})))
}
_ => self.cast_failure(target),
}
}
fn cast_failure(&self, target: &ValueType) -> TraverserResult<ElementProperty> {
Err(Box::new(TraverserError::cast_exception_target(
target.to_string(),
value_type_name(self),
Some(crate::value::literal::rhai_literal(self)),
None,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn int(value: i64) -> ElementProperty {
ElementProperty::Number(Number::from_i64(value))
}
fn float(value: f64) -> ElementProperty {
ElementProperty::Number(Number::from_f64(value))
}
fn text(value: &str) -> ElementProperty {
ElementProperty::from(value)
}
#[test]
fn null_casts_to_null_whatever_the_target() {
for target in [
ValueType::Null,
ValueType::Boolean,
ValueType::Int64,
ValueType::Float64,
ValueType::String,
] {
assert_eq!(
ElementProperty::Null.cast(&target).unwrap(),
ElementProperty::Null,
"null must survive a cast to {target}"
);
}
}
#[test]
fn int64_widens_to_float64() {
assert_eq!(int(7).cast(&ValueType::Float64).unwrap(), float(7.0));
assert_eq!(int(-7).cast(&ValueType::Float64).unwrap(), float(-7.0));
}
#[test]
fn float64_narrows_to_int64_only_without_loss() {
assert_eq!(float(7.0).cast(&ValueType::Int64).unwrap(), int(7));
assert!(float(7.5).cast(&ValueType::Int64).is_err());
assert!(float(1e300).cast(&ValueType::Int64).is_err());
}
#[test]
fn everything_scalar_renders_to_string() {
assert_eq!(int(29).cast(&ValueType::String).unwrap(), text("29"));
assert_eq!(float(1.5).cast(&ValueType::String).unwrap(), text("1.5"));
assert_eq!(
ElementProperty::Boolean(true)
.cast(&ValueType::String)
.unwrap(),
text("true")
);
assert_eq!(
text("marko").cast(&ValueType::String).unwrap(),
text("marko")
);
}
#[test]
fn string_to_number_parses_the_whole_input() {
assert_eq!(text("999").cast(&ValueType::Int64).unwrap(), int(999));
assert_eq!(text("-999").cast(&ValueType::Int64).unwrap(), int(-999));
assert_eq!(text("1.5").cast(&ValueType::Float64).unwrap(), float(1.5));
assert!(text("12abc").cast(&ValueType::Int64).is_err());
assert!(text(" 12").cast(&ValueType::Int64).is_err());
assert!(text("12 ").cast(&ValueType::Int64).is_err());
assert!(text("1,5").cast(&ValueType::Float64).is_err());
assert!(text("1.5").cast(&ValueType::Int64).is_err());
assert!(text("").cast(&ValueType::Int64).is_err());
}
#[test]
fn string_to_boolean_takes_the_exact_literals_only() {
assert_eq!(
text("true").cast(&ValueType::Boolean).unwrap(),
ElementProperty::Boolean(true)
);
assert_eq!(
text("false").cast(&ValueType::Boolean).unwrap(),
ElementProperty::Boolean(false)
);
assert!(text("TRUE").cast(&ValueType::Boolean).is_err());
assert!(text("yes").cast(&ValueType::Boolean).is_err());
assert!(text("1").cast(&ValueType::Boolean).is_err());
}
#[test]
fn boolean_converts_to_zero_and_one() {
assert_eq!(
ElementProperty::Boolean(false)
.cast(&ValueType::Int64)
.unwrap(),
int(0)
);
assert_eq!(
ElementProperty::Boolean(true)
.cast(&ValueType::Int64)
.unwrap(),
int(1)
);
assert_eq!(
ElementProperty::Boolean(false)
.cast(&ValueType::Float64)
.unwrap(),
float(0.0)
);
assert_eq!(
ElementProperty::Boolean(true)
.cast(&ValueType::Float64)
.unwrap(),
float(1.0)
);
assert_eq!(
ElementProperty::Boolean(true)
.cast(&ValueType::Boolean)
.unwrap(),
ElementProperty::Boolean(true)
);
}
#[test]
fn number_to_boolean_takes_zero_and_one_only() {
assert_eq!(
int(0).cast(&ValueType::Boolean).unwrap(),
ElementProperty::Boolean(false)
);
assert_eq!(
int(1).cast(&ValueType::Boolean).unwrap(),
ElementProperty::Boolean(true)
);
assert_eq!(
float(0.0).cast(&ValueType::Boolean).unwrap(),
ElementProperty::Boolean(false)
);
assert_eq!(
float(1.0).cast(&ValueType::Boolean).unwrap(),
ElementProperty::Boolean(true)
);
assert!(int(2).cast(&ValueType::Boolean).is_err());
assert!(float(0.5).cast(&ValueType::Boolean).is_err());
}
#[test]
fn containers_and_graph_elements_never_cast_to_a_scalar() {
let containers = [
ElementProperty::Array(vec![int(1)]),
ElementProperty::Object(Default::default()),
ElementProperty::Vertex(Default::default()),
ElementProperty::Edge(Default::default()),
];
for value in containers {
for target in [
ValueType::Boolean,
ValueType::Int64,
ValueType::Float64,
ValueType::String,
] {
assert!(
value.cast(&target).is_err(),
"{value:?} must not cast to {target}"
);
}
}
}
#[test]
fn a_cast_to_null_fails_for_a_present_value() {
assert!(text("marko").cast(&ValueType::Null).is_err());
assert!(int(1).cast(&ValueType::Null).is_err());
}
#[test]
fn container_and_union_targets_fail_loudly() {
let targets = [
ValueType::Array(Box::new(ValueType::String)),
ValueType::Object,
ValueType::Union(vec![ValueType::Int64, ValueType::String]),
];
for target in targets {
let err = text("anything")
.cast(&target)
.expect_err("{target} must not silently pass the value through");
assert!(
matches!(
*err,
TraverserError::Value(crate::error::ValueError::UnsupportedCastTarget { .. })
),
"{target} must report an unsupported target, got: {err}"
);
}
}
#[test]
fn a_failure_reports_the_expected_target_and_the_offending_value() {
let err = text("12abc")
.cast(&ValueType::Int64)
.expect_err("must fail");
let rendered = err.to_string();
assert!(rendered.contains("int64"), "missing target in: {rendered}");
assert!(
rendered.contains("string"),
"missing source type in: {rendered}"
);
}
#[test]
fn type_of_reports_the_exact_logical_type() {
use crate::graph::schema::type_of;
assert_eq!(type_of(&text("999")), Some(ValueType::String));
assert_eq!(type_of(&int(999)), Some(ValueType::Int64));
assert_eq!(type_of(&float(1.5)), Some(ValueType::Float64));
assert_eq!(type_of(&ElementProperty::Null), Some(ValueType::Null));
}
#[test]
fn a_canonical_uuid_string_casts_to_uuid() {
let canonical = "550e8400-e29b-41d4-a716-446655440000";
let cast = text(canonical).cast(&ValueType::Uuid).unwrap();
assert_eq!(crate::graph::schema::type_of(&cast), Some(ValueType::Uuid));
assert_eq!(cast.as_uuid(), Some(0x550e8400_e29b_41d4_a716_446655440000));
assert_eq!(cast.clone().cast(&ValueType::Uuid).unwrap(), cast);
assert_eq!(cast.cast(&ValueType::String).unwrap(), text(canonical));
}
#[test]
fn a_malformed_uuid_string_fails_the_cast() {
for malformed in [
"550e8400-e29b-41d4-a716-44665544000", "550e8400-e29b-41d4-a716-4466554400000", "550e8400-e29b-41d4-a716-44665544000g", "550E8400-E29B-41D4-A716-446655440000", "{550e8400-e29b-41d4-a716-446655440000}", ] {
let err = text(malformed)
.cast(&ValueType::Uuid)
.expect_err(&format!("{malformed} must not parse as a UUID"));
assert!(matches!(
*err,
TraverserError::Value(crate::error::ValueError::CastException { .. })
));
}
}
#[test]
fn uuid_never_casts_from_a_non_string_non_uuid_value() {
for value in [int(1), float(1.0), ElementProperty::Boolean(true)] {
assert!(value.cast(&ValueType::Uuid).is_err());
}
}
#[test]
fn the_default_policy_is_to_fail() {
assert_eq!(CastPolicy::default(), CastPolicy::Error);
}
}