aboutsummaryrefslogtreecommitdiffstats
path: root/src/document/typed.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-02 11:04:15 +0100
committertv <tv@krebsco.de>2026-11-02 11:04:15 +0100
commit694661d7477e4bd1e8d7ec96dcc37742d1b5ca3f (patch)
treeb9b4766e689913b6322bb356782756d66383be5f /src/document/typed.rs
parent177c0a5b3b80fe2d502f31b7badd220a1cf5ed7c (diff)
api: add type casts
Diffstat (limited to 'src/document/typed.rs')
-rw-r--r--src/document/typed.rs165
1 files changed, 165 insertions, 0 deletions
diff --git a/src/document/typed.rs b/src/document/typed.rs
index cf7f49b..7f6e0cc 100644
--- a/src/document/typed.rs
+++ b/src/document/typed.rs
@@ -73,6 +73,26 @@ impl Kind {
}
}
+ /// The kind `name` names (`name`).
+ pub(crate) fn named(name: &str) -> Option<Self> {
+ [
+ Kind::Map,
+ Kind::List,
+ Kind::Text,
+ Kind::String,
+ Kind::Bytes,
+ Kind::Int,
+ Kind::Uint,
+ Kind::Float,
+ Kind::Boolean,
+ Kind::Null,
+ Kind::Counter,
+ Kind::Timestamp,
+ ]
+ .into_iter()
+ .find(|kind| kind.name() == name)
+ }
+
/// The media type a value of this kind is shown as natively, if any.
pub(crate) fn media_type(self) -> Option<&'static str> {
Some(match self {
@@ -364,6 +384,93 @@ pub(crate) fn from_native(
})
}
+/// `value` (a scalar, or a text's characters as a string), held as `from`, cast to
+/// `to` (`store::cast_path`): numbers (integers, floats, counters, timestamps as
+/// milliseconds since 1970) to numbers where they are exactly that; anything but bytes
+/// to a string or a text as its type's own media type shows it (`native`, timestamps
+/// in `timestamps`), and back (`from_native`); bytes to a string or a text and back as
+/// UTF-8. An error says why not (maps, lists and nulls are cast to nothing, and nothing
+/// to them).
+pub(crate) fn cast(
+ from: Kind,
+ value: &ScalarValue,
+ to: Kind,
+ timestamps: TimestampFormat,
+) -> Result<Val, String> {
+ let string = |value: &ScalarValue| -> Result<String, String> {
+ match value {
+ ScalarValue::Str(text) => Ok(text.to_string()),
+ ScalarValue::Bytes(bytes) => {
+ String::from_utf8(bytes.clone()).map_err(|_| "not UTF-8".to_owned())
+ }
+ other => native(other, timestamps)
+ .and_then(|(_, body)| String::from_utf8(body).ok())
+ .ok_or_else(|| "it has no text form".to_owned()),
+ }
+ };
+ match to {
+ Kind::String => {
+ return string(value).map(|text| Val::Scalar(ScalarValue::Str(text.into())));
+ }
+ Kind::Text => return string(value).map(Val::Text),
+ Kind::Map | Kind::List | Kind::Null | Kind::Unknown => {
+ return Err(format!("nothing is cast to {}", to.name()));
+ }
+ _ => {}
+ }
+ if let ScalarValue::Str(text) = value {
+ if to == Kind::Bytes {
+ return Ok(Val::Scalar(ScalarValue::Bytes(text.as_bytes().to_vec())));
+ }
+ let media = to.media_type().ok_or("it has no text form")?;
+ let params = format!("; format={}", timestamps.name());
+ return from_native(media, &params, text.as_bytes())
+ .unwrap_or_else(|| Err("it has no text form".to_owned()))
+ .map(Val::Scalar);
+ }
+ // Numbers: exactly, as an integer if they are one, else as a float.
+ let number = match value {
+ ScalarValue::Int(i) => Ok(i128::from(*i)),
+ ScalarValue::Uint(u) => Ok(i128::from(*u)),
+ ScalarValue::Counter(c) => Ok(i128::from(i64::from(c))),
+ ScalarValue::Timestamp(t) => Ok(i128::from(*t)),
+ ScalarValue::F64(f) => Err(*f),
+ _ => return Err(format!("{} isn't a number", from.name())),
+ };
+ let integer = || -> Result<i128, String> {
+ match number {
+ Ok(integer) => Ok(integer),
+ // Integral floats up to 2^64 convert exactly (`as` saturates beyond).
+ Err(float) if float.fract() == 0.0 && float.abs() < 2f64.powi(64) => Ok(float as i128),
+ Err(float) => Err(format!("{float} isn't an integer that fits")),
+ }
+ };
+ let signed = || -> Result<i64, String> {
+ let integer = integer()?;
+ i64::try_from(integer).map_err(|_| format!("{integer} doesn't fit a 64-bit integer"))
+ };
+ let scalar = match to {
+ Kind::Int => ScalarValue::Int(signed()?),
+ Kind::Counter => ScalarValue::counter(signed()?),
+ Kind::Timestamp => ScalarValue::Timestamp(signed()?),
+ Kind::Uint => {
+ let integer = integer()?;
+ ScalarValue::Uint(
+ u64::try_from(integer)
+ .map_err(|_| format!("{integer} doesn't fit an unsigned 64-bit integer"))?,
+ )
+ }
+ Kind::Float => match number {
+ Err(float) => ScalarValue::F64(float),
+ // Exactly: up to 2^53 every integer is a float.
+ Ok(integer) if integer.unsigned_abs() <= 1 << 53 => ScalarValue::F64(integer as f64),
+ Ok(integer) => return Err(format!("{integer} isn't exactly a float")),
+ },
+ _ => return Err(format!("numbers aren't cast to {}", to.name())),
+ };
+ Ok(Val::Scalar(scalar))
+}
+
/// The value of parameter `name` among a media type's `params` (`; a=b; c="d"`).
pub(crate) fn param(params: &str, name: &str) -> Option<String> {
params.split(';').find_map(|param| {
@@ -520,3 +627,61 @@ impl Val {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn cast_to(from: ScalarValue, to: Kind) -> Result<Val, String> {
+ cast(Kind::of(&from), &from, to, TimestampFormat::default())
+ }
+
+ #[test]
+ fn numbers_cast_where_exact() {
+ assert_eq!(
+ cast_to(ScalarValue::F64(2.0), Kind::Int),
+ Ok(Val::Scalar(ScalarValue::Int(2)))
+ );
+ assert!(cast_to(ScalarValue::F64(2.5), Kind::Int).is_err());
+ assert!(cast_to(ScalarValue::Int(-1), Kind::Uint).is_err());
+ assert_eq!(
+ cast_to(ScalarValue::Int(5), Kind::Counter),
+ Ok(Val::Scalar(ScalarValue::counter(5)))
+ );
+ assert!(cast_to(ScalarValue::Int(i64::MAX), Kind::Float).is_err());
+ assert!(cast_to(ScalarValue::Boolean(true), Kind::Int).is_err());
+ }
+
+ #[test]
+ fn strings_cast_as_media_types_show_them() {
+ assert_eq!(
+ cast_to(
+ ScalarValue::Str("2026-11-01T21:50:20.345Z".into()),
+ Kind::Timestamp
+ ),
+ Ok(Val::Scalar(ScalarValue::Timestamp(1_793_569_820_345)))
+ );
+ assert!(cast_to(ScalarValue::Str("soon".into()), Kind::Timestamp).is_err());
+ assert_eq!(
+ cast_to(ScalarValue::Timestamp(1_793_569_820_345), Kind::String),
+ Ok(Val::Scalar(ScalarValue::Str(
+ "2026-11-01T21:50:20.345Z".into()
+ )))
+ );
+ assert_eq!(
+ cast_to(ScalarValue::Str("true".into()), Kind::Boolean),
+ Ok(Val::Scalar(ScalarValue::Boolean(true)))
+ );
+ assert_eq!(
+ cast_to(ScalarValue::Str("hé".into()), Kind::Text),
+ Ok(Val::Text("hé".to_owned()))
+ );
+ assert_eq!(
+ cast_to(ScalarValue::Bytes(b"hi".to_vec()), Kind::String),
+ Ok(Val::Scalar(ScalarValue::Str("hi".into())))
+ );
+ assert!(cast_to(ScalarValue::Bytes(vec![0xff]), Kind::String).is_err());
+ assert!(cast_to(ScalarValue::Null, Kind::String).is_err());
+ assert!(cast_to(ScalarValue::Int(1), Kind::Map).is_err());
+ }
+}