use crate::data::DataValue;
use crate::error::{LaurusError, Result};
use super::schema::FieldOption;
pub fn coerce_value(field_name: &str, option: &FieldOption, value: DataValue) -> Result<DataValue> {
match option {
FieldOption::Text(_) => coerce_to_text(field_name, value),
FieldOption::Integer(opt) => coerce_to_integer(field_name, opt, value),
FieldOption::Float(opt) => coerce_to_float(field_name, opt, value),
FieldOption::Boolean(_) => coerce_to_boolean(field_name, value),
FieldOption::DateTime(_) => coerce_to_datetime(field_name, value),
FieldOption::Geo(_) => coerce_to_geo(field_name, value),
FieldOption::Geo3d(_) => coerce_to_geo3d(field_name, value),
FieldOption::Bytes(_) => coerce_to_bytes(field_name, value),
FieldOption::Hnsw(_) | FieldOption::Flat(_) | FieldOption::Ivf(_) => {
coerce_to_vector(field_name, value)
}
}
}
fn coerce_to_text(_field_name: &str, value: DataValue) -> Result<DataValue> {
Ok(match value {
DataValue::Text(s) => DataValue::Text(s),
DataValue::Int64(i) => DataValue::Text(i.to_string()),
DataValue::Float64(f) => DataValue::Text(f.to_string()),
DataValue::Bool(b) => DataValue::Text(b.to_string()),
DataValue::DateTime(dt) => DataValue::Text(dt.to_rfc3339()),
DataValue::Null => DataValue::Text(String::new()),
other => {
return Err(LaurusError::invalid_argument(format!(
"cannot coerce {} to a text value",
describe(&other)
)));
}
})
}
fn coerce_to_integer(
field_name: &str,
option: &crate::lexical::core::field::IntegerOption,
value: DataValue,
) -> Result<DataValue> {
if option.multi_valued {
match value {
DataValue::Int64Array(arr) => Ok(DataValue::Int64Array(arr)),
DataValue::Float64Array(arr) => Ok(DataValue::Int64Array(
arr.iter().map(|f| *f as i64).collect(),
)),
DataValue::Int64(i) => Ok(DataValue::Int64Array(vec![i])),
DataValue::Float64(f) => Ok(DataValue::Int64Array(vec![f as i64])),
DataValue::Bool(b) => Ok(DataValue::Int64Array(vec![if b { 1 } else { 0 }])),
DataValue::Text(s) => s
.trim()
.parse::<i64>()
.map(|n| DataValue::Int64Array(vec![n]))
.map_err(|_| {
LaurusError::invalid_argument(format!(
"field '{field_name}': cannot parse '{s}' as an integer"
))
}),
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a multi-valued integer",
describe(&other)
))),
}
} else {
match value {
DataValue::Int64(i) => Ok(DataValue::Int64(i)),
DataValue::Float64(f) => Ok(DataValue::Int64(f as i64)),
DataValue::Bool(b) => Ok(DataValue::Int64(if b { 1 } else { 0 })),
DataValue::Text(s) => s.trim().parse::<i64>().map(DataValue::Int64).map_err(|_| {
LaurusError::invalid_argument(format!(
"field '{field_name}': cannot parse '{s}' as an integer"
))
}),
DataValue::Int64Array(_) | DataValue::Float64Array(_) => {
Err(LaurusError::invalid_argument(format!(
"field '{field_name}': received an array but the field is single-valued; \
declare the field with multi_valued = true to accept arrays"
)))
}
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to an integer",
describe(&other)
))),
}
}
}
fn coerce_to_float(
field_name: &str,
option: &crate::lexical::core::field::FloatOption,
value: DataValue,
) -> Result<DataValue> {
if option.multi_valued {
match value {
DataValue::Float64Array(arr) => Ok(DataValue::Float64Array(arr)),
DataValue::Int64Array(arr) => Ok(DataValue::Float64Array(
arr.iter().map(|i| *i as f64).collect(),
)),
DataValue::Float64(f) => Ok(DataValue::Float64Array(vec![f])),
DataValue::Int64(i) => Ok(DataValue::Float64Array(vec![i as f64])),
DataValue::Bool(b) => Ok(DataValue::Float64Array(vec![if b { 1.0 } else { 0.0 }])),
DataValue::Text(s) => s
.trim()
.parse::<f64>()
.map(|n| DataValue::Float64Array(vec![n]))
.map_err(|_| {
LaurusError::invalid_argument(format!(
"field '{field_name}': cannot parse '{s}' as a float"
))
}),
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a multi-valued float",
describe(&other)
))),
}
} else {
match value {
DataValue::Float64(f) => Ok(DataValue::Float64(f)),
DataValue::Int64(i) => Ok(DataValue::Float64(i as f64)),
DataValue::Bool(b) => Ok(DataValue::Float64(if b { 1.0 } else { 0.0 })),
DataValue::Text(s) => s
.trim()
.parse::<f64>()
.map(DataValue::Float64)
.map_err(|_| {
LaurusError::invalid_argument(format!(
"field '{field_name}': cannot parse '{s}' as a float"
))
}),
DataValue::Int64Array(_) | DataValue::Float64Array(_) => {
Err(LaurusError::invalid_argument(format!(
"field '{field_name}': received an array but the field is single-valued; \
declare the field with multi_valued = true to accept arrays"
)))
}
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a float",
describe(&other)
))),
}
}
}
fn coerce_to_boolean(field_name: &str, value: DataValue) -> Result<DataValue> {
match value {
DataValue::Bool(b) => Ok(DataValue::Bool(b)),
DataValue::Int64(0) => Ok(DataValue::Bool(false)),
DataValue::Int64(1) => Ok(DataValue::Bool(true)),
DataValue::Int64(n) => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce integer {n} to bool (only 0 and 1 are accepted)"
))),
DataValue::Text(s) => match s.trim().to_ascii_lowercase().as_str() {
"true" => Ok(DataValue::Bool(true)),
"false" => Ok(DataValue::Bool(false)),
_ => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot parse '{s}' as a bool (expected 'true' or 'false')"
))),
},
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a bool",
describe(&other)
))),
}
}
fn coerce_to_datetime(field_name: &str, value: DataValue) -> Result<DataValue> {
match value {
DataValue::DateTime(dt) => Ok(DataValue::DateTime(dt)),
DataValue::Text(s) => {
let parsed = chrono::DateTime::parse_from_rfc3339(s.trim()).map_err(|e| {
LaurusError::invalid_argument(format!(
"field '{field_name}': cannot parse '{s}' as an RFC 3339 datetime: {e}"
))
})?;
Ok(DataValue::DateTime(parsed.with_timezone(&chrono::Utc)))
}
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a datetime",
describe(&other)
))),
}
}
fn coerce_to_geo(field_name: &str, value: DataValue) -> Result<DataValue> {
match value {
DataValue::Geo(p) => Ok(DataValue::Geo(p)),
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a geographic point",
describe(&other)
))),
}
}
fn coerce_to_geo3d(field_name: &str, value: DataValue) -> Result<DataValue> {
match value {
DataValue::GeoEcef(p) => Ok(DataValue::GeoEcef(p)),
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to a 3D ECEF geo point",
describe(&other)
))),
}
}
fn coerce_to_bytes(field_name: &str, value: DataValue) -> Result<DataValue> {
match value {
DataValue::Bytes(data, mime) => Ok(DataValue::Bytes(data, mime)),
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': cannot coerce {} to bytes",
describe(&other)
))),
}
}
fn coerce_to_vector(field_name: &str, value: DataValue) -> Result<DataValue> {
match value {
DataValue::Vector(v) => Ok(DataValue::Vector(v)),
DataValue::Text(s) => Ok(DataValue::Text(s)),
DataValue::Bytes(data, mime) => Ok(DataValue::Bytes(data, mime)),
DataValue::Float64Array(arr) => {
Ok(DataValue::Vector(arr.iter().map(|v| *v as f32).collect()))
}
DataValue::Int64Array(arr) => {
Ok(DataValue::Vector(arr.iter().map(|v| *v as f32).collect()))
}
other => Err(LaurusError::invalid_argument(format!(
"field '{field_name}': vector fields accept Vector, Text, Bytes, \
or numeric arrays; got {}",
describe(&other)
))),
}
}
fn describe(value: &DataValue) -> &'static str {
match value {
DataValue::Null => "null",
DataValue::Bool(_) => "bool",
DataValue::Int64(_) => "integer",
DataValue::Float64(_) => "float",
DataValue::Text(_) => "text",
DataValue::Bytes(_, _) => "bytes",
DataValue::Vector(_) => "vector",
DataValue::DateTime(_) => "datetime",
DataValue::Geo(_) => "geo",
DataValue::GeoEcef(_) => "geo3d",
DataValue::Int64Array(_) => "integer array",
DataValue::Float64Array(_) => "float array",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexical::core::field::{
BooleanOption, FloatOption, Geo3dOption, GeoOption, IntegerOption, TextOption,
};
fn integer() -> FieldOption {
FieldOption::Integer(IntegerOption::default())
}
fn float() -> FieldOption {
FieldOption::Float(FloatOption::default())
}
fn boolean() -> FieldOption {
FieldOption::Boolean(BooleanOption::default())
}
fn text() -> FieldOption {
FieldOption::Text(TextOption::default())
}
fn geo() -> FieldOption {
FieldOption::Geo(GeoOption::default())
}
fn geo3d() -> FieldOption {
FieldOption::Geo3d(Geo3dOption::default())
}
#[test]
fn integer_passthrough() {
assert_eq!(
coerce_value("n", &integer(), DataValue::Int64(5)).unwrap(),
DataValue::Int64(5)
);
}
#[test]
fn integer_truncates_float() {
assert_eq!(
coerce_value("n", &integer(), DataValue::Float64(4.7)).unwrap(),
DataValue::Int64(4)
);
assert_eq!(
coerce_value("n", &integer(), DataValue::Float64(-3.9)).unwrap(),
DataValue::Int64(-3)
);
}
#[test]
fn integer_parses_text() {
assert_eq!(
coerce_value("n", &integer(), DataValue::Text("42".into())).unwrap(),
DataValue::Int64(42)
);
}
#[test]
fn integer_rejects_unparseable_text() {
assert!(coerce_value("n", &integer(), DataValue::Text("abc".into())).is_err());
}
#[test]
fn integer_from_bool() {
assert_eq!(
coerce_value("n", &integer(), DataValue::Bool(true)).unwrap(),
DataValue::Int64(1)
);
}
#[test]
fn float_from_integer() {
assert_eq!(
coerce_value("x", &float(), DataValue::Int64(42)).unwrap(),
DataValue::Float64(42.0)
);
}
#[test]
fn float_parses_text() {
assert_eq!(
coerce_value("x", &float(), DataValue::Text("4.5".into())).unwrap(),
DataValue::Float64(4.5)
);
}
#[test]
fn float_rejects_unparseable_text() {
assert!(coerce_value("x", &float(), DataValue::Text("abc".into())).is_err());
}
#[test]
fn boolean_from_int_zero_one() {
assert_eq!(
coerce_value("b", &boolean(), DataValue::Int64(0)).unwrap(),
DataValue::Bool(false)
);
assert_eq!(
coerce_value("b", &boolean(), DataValue::Int64(1)).unwrap(),
DataValue::Bool(true)
);
}
#[test]
fn boolean_rejects_other_ints() {
assert!(coerce_value("b", &boolean(), DataValue::Int64(2)).is_err());
}
#[test]
fn boolean_from_text() {
assert_eq!(
coerce_value("b", &boolean(), DataValue::Text("true".into())).unwrap(),
DataValue::Bool(true)
);
assert_eq!(
coerce_value("b", &boolean(), DataValue::Text("False".into())).unwrap(),
DataValue::Bool(false)
);
}
#[test]
fn boolean_rejects_yes() {
assert!(coerce_value("b", &boolean(), DataValue::Text("yes".into())).is_err());
}
#[test]
fn text_stringifies_any_scalar() {
assert_eq!(
coerce_value("t", &text(), DataValue::Int64(42)).unwrap(),
DataValue::Text("42".into())
);
assert_eq!(
coerce_value("t", &text(), DataValue::Float64(4.5)).unwrap(),
DataValue::Text("4.5".into())
);
assert_eq!(
coerce_value("t", &text(), DataValue::Bool(true)).unwrap(),
DataValue::Text("true".into())
);
}
#[test]
fn geo_passthrough_only() {
assert_eq!(
coerce_value(
"g",
&geo(),
DataValue::Geo(crate::data::GeoPoint::new(35.1, 139.0))
)
.unwrap(),
DataValue::Geo(crate::data::GeoPoint::new(35.1, 139.0))
);
assert!(coerce_value("g", &geo(), DataValue::Int64(35)).is_err());
}
#[test]
fn geo3d_passthrough_only() {
let p = crate::data::GeoEcefPoint::new(1_234_567.0, 2_345_678.0, 3_456_789.0);
assert_eq!(
coerce_value("g3", &geo3d(), DataValue::GeoEcef(p)).unwrap(),
DataValue::GeoEcef(p)
);
let p2d = crate::data::GeoPoint::new(35.1, 139.0);
assert!(coerce_value("g3", &geo3d(), DataValue::Geo(p2d)).is_err());
assert!(coerce_value("g3", &geo3d(), DataValue::Int64(35)).is_err());
}
}