use chrono::NaiveDate;
use num_bigint::BigInt;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::BTreeSet;
use std::error::Error;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum Value {
Bool(bool),
Int(i64),
Decimal(#[serde(with = "exact_decimal_serde")] Decimal),
String(String),
Date(NaiveDate),
Duration(i64),
Enum {
type_name: String,
variant: String,
},
EntityRef {
type_name: String,
id: String,
},
}
impl Value {
#[must_use]
pub fn decimal(value: Decimal) -> Self {
Self::Decimal(value.normalize())
}
#[must_use]
pub const fn kind_name(&self) -> &'static str {
match self {
Self::Bool(_) => "Bool",
Self::Int(_) => "Int",
Self::Decimal(_) => "Decimal",
Self::String(_) => "String",
Self::Date(_) => "Date",
Self::Duration(_) => "Duration",
Self::Enum { .. } => "Enum",
Self::EntityRef { .. } => "Entity",
}
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
if let Self::Bool(value) = self {
Some(*value)
} else {
None
}
}
#[must_use]
pub const fn as_int(&self) -> Option<i64> {
if let Self::Int(value) = self {
Some(*value)
} else {
None
}
}
#[must_use]
pub const fn as_decimal(&self) -> Option<Decimal> {
if let Self::Decimal(value) = self {
Some(*value)
} else {
None
}
}
#[must_use]
pub const fn as_date(&self) -> Option<NaiveDate> {
if let Self::Date(value) = self {
Some(*value)
} else {
None
}
}
#[must_use]
pub const fn as_duration_seconds(&self) -> Option<i64> {
if let Self::Duration(value) = self {
Some(*value)
} else {
None
}
}
pub fn to_json(&self) -> Result<JsonValue, serde_json::Error> {
serde_json::to_value(self)
}
pub fn from_json(value: JsonValue) -> Result<Self, ValueConversionError> {
serde_json::from_value(value)
.map(Self::normalized)
.map_err(|error| ValueConversionError::InvalidTaggedValue(error.to_string()))
}
#[must_use]
pub fn to_plain_json(&self) -> JsonValue {
match self {
Self::Bool(value) => JsonValue::Bool(*value),
Self::Int(value) | Self::Duration(value) => JsonValue::Number((*value).into()),
Self::Decimal(value) => JsonValue::String(value.normalize().to_string()),
Self::String(value) | Self::EntityRef { id: value, .. } => {
JsonValue::String(value.clone())
}
Self::Date(value) => JsonValue::String(value.format("%Y-%m-%d").to_string()),
Self::Enum { variant, .. } => JsonValue::String(variant.clone()),
}
}
pub fn from_plain_json(
value: &JsonValue,
expected: &ValueKind,
) -> Result<Self, ValueConversionError> {
match expected {
ValueKind::Bool => value
.as_bool()
.map(Self::Bool)
.ok_or_else(|| type_mismatch(expected, value)),
ValueKind::Int => value
.as_i64()
.map(Self::Int)
.ok_or_else(|| type_mismatch(expected, value)),
ValueKind::Decimal => {
let text = match value {
JsonValue::Number(number) => number.to_string(),
JsonValue::String(text) => text.clone(),
_ => return Err(type_mismatch(expected, value)),
};
parse_decimal(&text)
.map(Self::Decimal)
.map_err(|error| ValueConversionError::InvalidDecimal(error.to_string()))
}
ValueKind::String => value
.as_str()
.map(|value| Self::String(value.to_owned()))
.ok_or_else(|| type_mismatch(expected, value)),
ValueKind::Date => {
let text = value
.as_str()
.ok_or_else(|| type_mismatch(expected, value))?;
NaiveDate::parse_from_str(text, "%Y-%m-%d")
.map(Self::Date)
.map_err(|_| ValueConversionError::InvalidDate(text.to_owned()))
}
ValueKind::Duration => value
.as_i64()
.map(Self::Duration)
.ok_or_else(|| type_mismatch(expected, value)),
ValueKind::Enum(type_name) => value
.as_str()
.map(|variant| Self::Enum {
type_name: type_name.clone(),
variant: variant.to_owned(),
})
.ok_or_else(|| type_mismatch(expected, value)),
ValueKind::EntityRef(type_name) => value
.as_str()
.map(|id| Self::EntityRef {
type_name: type_name.clone(),
id: id.to_owned(),
})
.ok_or_else(|| type_mismatch(expected, value)),
}
}
fn normalized(self) -> Self {
match self {
Self::Decimal(value) => Self::Decimal(value.normalize()),
other => other,
}
}
}
impl fmt::Display for Value {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bool(value) => value.fmt(formatter),
Self::Int(value) => formatter.write_str(&group_integer_digits(&value.to_string())),
Self::Decimal(value) => {
let normalized = value.normalize().to_string();
let (integer, fraction) = normalized.split_once('.').unwrap_or((&normalized, ""));
formatter.write_str(&group_integer_digits(integer))?;
if !fraction.is_empty() {
write!(formatter, ".{fraction}")?;
}
Ok(())
}
Self::String(value) => write!(formatter, "{value:?}"),
Self::Date(value) => write!(formatter, "{}", value.format("%Y-%m-%d")),
Self::Duration(seconds) => {
write!(formatter, "{}s", group_integer_digits(&seconds.to_string()))
}
Self::Enum { type_name, variant } => write!(formatter, "{type_name}.{variant}"),
Self::EntityRef { type_name, id } => write!(formatter, "{type_name}#{id}"),
}
}
}
pub(crate) fn group_integer_digits(value: &str) -> String {
let (sign, digits) = value
.strip_prefix('-')
.map_or(("", value), |digits| ("-", digits));
let separator_count = digits.len().saturating_sub(1) / 3;
let mut grouped = String::with_capacity(value.len() + separator_count);
grouped.push_str(sign);
for (index, digit) in digits.chars().enumerate() {
if index != 0 && (digits.len() - index) % 3 == 0 {
grouped.push('_');
}
grouped.push(digit);
}
grouped
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "name", rename_all = "snake_case")]
pub enum ValueKind {
Bool,
Int,
Decimal,
String,
Date,
Duration,
Enum(String),
EntityRef(String),
}
impl fmt::Display for ValueKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bool => formatter.write_str("Bool"),
Self::Int => formatter.write_str("Int"),
Self::Decimal => formatter.write_str("Decimal"),
Self::String => formatter.write_str("String"),
Self::Date => formatter.write_str("Date"),
Self::Duration => formatter.write_str("Duration"),
Self::Enum(name) | Self::EntityRef(name) => formatter.write_str(name),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ValueConversionError {
TypeMismatch {
expected: ValueKind,
found: &'static str,
},
InvalidDecimal(String),
InvalidDate(String),
InvalidTaggedValue(String),
}
impl fmt::Display for ValueConversionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TypeMismatch { expected, found } => {
write!(formatter, "expected {expected}, found JSON {found}")
}
Self::InvalidDecimal(message) => write!(formatter, "invalid Decimal: {message}"),
Self::InvalidDate(value) => {
write!(formatter, "invalid Date `{value}`; expected YYYY-MM-DD")
}
Self::InvalidTaggedValue(message) => {
write!(formatter, "invalid tagged Tess value: {message}")
}
}
}
}
impl Error for ValueConversionError {}
fn type_mismatch(expected: &ValueKind, value: &JsonValue) -> ValueConversionError {
let found = match value {
JsonValue::Null => "null",
JsonValue::Bool(_) => "boolean",
JsonValue::Number(_) => "number",
JsonValue::String(_) => "string",
JsonValue::Array(_) => "array",
JsonValue::Object(_) => "object",
};
ValueConversionError::TypeMismatch {
expected: expected.clone(),
found,
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum TruthValue {
True,
False,
#[default]
Unknown,
Conflict,
}
impl TruthValue {
#[must_use]
pub const fn not(self) -> Self {
match self {
Self::True => Self::False,
Self::False => Self::True,
Self::Unknown => Self::Unknown,
Self::Conflict => Self::Conflict,
}
}
#[must_use]
pub const fn and(self, other: Self) -> Self {
if matches!(self, Self::False) || matches!(other, Self::False) {
Self::False
} else if matches!(self, Self::Conflict) || matches!(other, Self::Conflict) {
Self::Conflict
} else if matches!(self, Self::Unknown) || matches!(other, Self::Unknown) {
Self::Unknown
} else {
Self::True
}
}
#[must_use]
pub const fn or(self, other: Self) -> Self {
if matches!(self, Self::True) || matches!(other, Self::True) {
Self::True
} else if matches!(self, Self::Conflict) || matches!(other, Self::Conflict) {
Self::Conflict
} else if matches!(self, Self::Unknown) || matches!(other, Self::Unknown) {
Self::Unknown
} else {
Self::False
}
}
#[must_use]
pub const fn is_true(self) -> bool {
matches!(self, Self::True)
}
#[must_use]
pub const fn is_false(self) -> bool {
matches!(self, Self::False)
}
#[must_use]
pub const fn is_known(self) -> bool {
matches!(self, Self::True | Self::False)
}
}
impl From<bool> for TruthValue {
fn from(value: bool) -> Self {
if value { Self::True } else { Self::False }
}
}
impl std::ops::Not for TruthValue {
type Output = Self;
fn not(self) -> Self::Output {
self.not()
}
}
impl std::ops::BitAnd for TruthValue {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
self.and(rhs)
}
}
impl std::ops::BitOr for TruthValue {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
self.or(rhs)
}
}
impl fmt::Display for TruthValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::True => "true",
Self::False => "false",
Self::Unknown => "unknown",
Self::Conflict => "conflict",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(tag = "status", content = "value", rename_all = "snake_case")]
pub enum EvalValue {
Known(Value),
Unknown {
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
missing: BTreeSet<String>,
},
Conflict {
reasons: Vec<String>,
},
}
impl EvalValue {
#[must_use]
pub const fn known(value: Value) -> Self {
Self::Known(value)
}
#[must_use]
pub fn unknown(missing: impl IntoIterator<Item = String>) -> Self {
Self::Unknown {
missing: missing.into_iter().collect(),
}
}
#[must_use]
pub fn missing(name: impl Into<String>) -> Self {
Self::Unknown {
missing: BTreeSet::from([name.into()]),
}
}
#[must_use]
pub fn conflict(reasons: impl IntoIterator<Item = String>) -> Self {
let mut reasons = reasons.into_iter().collect::<Vec<_>>();
reasons.sort();
reasons.dedup();
Self::Conflict { reasons }
}
#[must_use]
pub const fn as_known(&self) -> Option<&Value> {
if let Self::Known(value) = self {
Some(value)
} else {
None
}
}
#[must_use]
pub fn into_known(self) -> Option<Value> {
if let Self::Known(value) = self {
Some(value)
} else {
None
}
}
#[must_use]
pub fn truth_value(&self) -> Option<TruthValue> {
match self {
Self::Known(Value::Bool(value)) => Some((*value).into()),
Self::Unknown { .. } => Some(TruthValue::Unknown),
Self::Conflict { .. } => Some(TruthValue::Conflict),
Self::Known(_) => None,
}
}
#[must_use]
pub fn map(self, map: impl FnOnce(Value) -> Value) -> Self {
match self {
Self::Known(value) => Self::known(map(value)),
Self::Unknown { missing } => Self::Unknown { missing },
Self::Conflict { reasons } => Self::Conflict { reasons },
}
}
}
impl From<Value> for EvalValue {
fn from(value: Value) -> Self {
Self::known(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DecimalError {
Invalid(String),
Overflow { operation: &'static str },
DivisionByZero,
Inexact { operation: &'static str },
}
impl fmt::Display for DecimalError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(value) => write!(formatter, "invalid decimal literal `{value}`"),
Self::Overflow { operation } => {
write!(formatter, "decimal overflow during {operation}")
}
Self::DivisionByZero => formatter.write_str("decimal division by zero"),
Self::Inexact { operation } => write!(
formatter,
"decimal {operation} is not exactly representable; explicit rounding is required"
),
}
}
}
impl Error for DecimalError {}
mod exact_decimal_serde {
use super::{Decimal, parse_decimal};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&value.to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
D: Deserializer<'de>,
{
let text = String::deserialize(deserializer)?;
parse_decimal(&text).map_err(serde::de::Error::custom)
}
}
pub fn parse_decimal(text: &str) -> Result<Decimal, DecimalError> {
let expanded = if text.contains(['e', 'E']) {
expand_scientific_decimal(text).ok_or_else(|| DecimalError::Invalid(text.to_owned()))?
} else {
text.to_owned()
};
Decimal::from_str_exact(&expanded)
.map(|value| value.normalize())
.map_err(|_| DecimalError::Invalid(text.to_owned()))
}
fn expand_scientific_decimal(text: &str) -> Option<String> {
let (negative, unsigned) = if let Some(value) = text.strip_prefix('-') {
(true, value)
} else if let Some(value) = text.strip_prefix('+') {
(false, value)
} else {
(false, text)
};
let mut parts = unsigned.split(['e', 'E']);
let significand = parts.next()?;
let exponent = parts.next()?.parse::<i32>().ok()?;
if parts.next().is_some() {
return None;
}
let mut digits = String::new();
let mut decimal_index = None;
for character in significand.chars() {
match character {
'0'..='9' => digits.push(character),
'_' => {}
'.' if decimal_index.is_none() => decimal_index = Some(digits.len()),
_ => return None,
}
}
if digits.is_empty() {
return None;
}
let decimal_index = decimal_index.unwrap_or(digits.len());
let shifted = i64::try_from(decimal_index).ok()? + i64::from(exponent);
let digit_count = i64::try_from(digits.len()).ok()?;
let padding = if shifted < 0 {
shifted.unsigned_abs()
} else if shifted > digit_count {
(shifted - digit_count).unsigned_abs()
} else {
0
};
if padding > 128 {
return None;
}
let mut expanded = String::new();
if negative {
expanded.push('-');
}
if shifted <= 0 {
expanded.push_str("0.");
expanded.extend(std::iter::repeat_n('0', usize::try_from(-shifted).ok()?));
expanded.push_str(&digits);
} else if shifted >= digit_count {
expanded.push_str(&digits);
expanded.extend(std::iter::repeat_n(
'0',
usize::try_from(shifted - digit_count).ok()?,
));
} else {
let split = usize::try_from(shifted).ok()?;
expanded.push_str(&digits[..split]);
expanded.push('.');
expanded.push_str(&digits[split..]);
}
Some(expanded)
}
fn aligned_decimal_mantissa(value: Decimal, scale: u32) -> BigInt {
let factor = BigInt::from(10_u8).pow(scale - value.scale());
BigInt::from(value.mantissa()) * factor
}
fn decimal_sum_is_exact(result: Decimal, left: Decimal, right: Decimal, subtract: bool) -> bool {
let scale = result.scale().max(left.scale()).max(right.scale());
let expected = if subtract {
aligned_decimal_mantissa(left, scale) - aligned_decimal_mantissa(right, scale)
} else {
aligned_decimal_mantissa(left, scale) + aligned_decimal_mantissa(right, scale)
};
aligned_decimal_mantissa(result, scale) == expected
}
fn decimal_product_is_exact(result: Decimal, left: Decimal, right: Decimal) -> bool {
let left_side =
BigInt::from(result.mantissa()) * BigInt::from(10_u8).pow(left.scale() + right.scale());
let right_side = BigInt::from(left.mantissa())
* BigInt::from(right.mantissa())
* BigInt::from(10_u8).pow(result.scale());
left_side == right_side
}
fn decimal_quotient_is_exact(result: Decimal, left: Decimal, right: Decimal) -> bool {
let left_side = BigInt::from(result.mantissa())
* BigInt::from(right.mantissa())
* BigInt::from(10_u8).pow(left.scale());
let right_side =
BigInt::from(left.mantissa()) * BigInt::from(10_u8).pow(right.scale() + result.scale());
left_side == right_side
}
pub fn checked_decimal_add(left: Decimal, right: Decimal) -> Result<Decimal, DecimalError> {
let result = left
.checked_add(right)
.ok_or(DecimalError::Overflow {
operation: "addition",
})?
.normalize();
if decimal_sum_is_exact(result, left, right, false) {
Ok(result)
} else {
Err(DecimalError::Inexact {
operation: "addition",
})
}
}
pub fn checked_decimal_sub(left: Decimal, right: Decimal) -> Result<Decimal, DecimalError> {
let result = left
.checked_sub(right)
.ok_or(DecimalError::Overflow {
operation: "subtraction",
})?
.normalize();
if decimal_sum_is_exact(result, left, right, true) {
Ok(result)
} else {
Err(DecimalError::Inexact {
operation: "subtraction",
})
}
}
pub fn checked_decimal_mul(left: Decimal, right: Decimal) -> Result<Decimal, DecimalError> {
let result = left
.checked_mul(right)
.ok_or(DecimalError::Overflow {
operation: "multiplication",
})?
.normalize();
if decimal_product_is_exact(result, left, right) {
Ok(result)
} else {
Err(DecimalError::Inexact {
operation: "multiplication",
})
}
}
pub fn checked_decimal_div(left: Decimal, right: Decimal) -> Result<Decimal, DecimalError> {
if right.is_zero() {
return Err(DecimalError::DivisionByZero);
}
let quotient = left
.checked_div(right)
.ok_or(DecimalError::Overflow {
operation: "division",
})?
.normalize();
if decimal_quotient_is_exact(quotient, left, right) {
Ok(quotient)
} else {
Err(DecimalError::Inexact {
operation: "division",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn truth_negation_preserves_unknown_and_conflict() {
assert_eq!(TruthValue::Unknown.not(), TruthValue::Unknown);
assert_eq!(TruthValue::Conflict.not(), TruthValue::Conflict);
assert_eq!(TruthValue::True.not(), TruthValue::False);
}
#[test]
fn truth_operators_preserve_conflict_over_unknown() {
assert_eq!(
TruthValue::False.and(TruthValue::Unknown),
TruthValue::False
);
assert_eq!(TruthValue::True.or(TruthValue::Unknown), TruthValue::True);
assert_eq!(
TruthValue::Conflict.and(TruthValue::True),
TruthValue::Conflict
);
assert_eq!(
TruthValue::Conflict.and(TruthValue::Unknown),
TruthValue::Conflict
);
assert_eq!(
TruthValue::Conflict.or(TruthValue::Unknown),
TruthValue::Conflict
);
}
#[test]
fn decimal_rendering_is_normalized() {
let value = Value::decimal(parse_decimal("1.2300").unwrap());
assert_eq!(value.to_string(), "1.23");
}
#[test]
fn decimal_parsing_rejects_values_that_require_rounding() {
assert_eq!(
parse_decimal("0.0000000000000000000000000001").unwrap(),
Decimal::new(1, 28)
);
assert_eq!(parse_decimal("1e-28").unwrap(), Decimal::new(1, 28));
assert!(parse_decimal("0.00000000000000000000000000001").is_err());
assert!(parse_decimal("1e-29").is_err());
}
#[test]
fn tagged_decimal_json_rejects_values_that_require_rounding() {
let value = json!({
"type": "decimal",
"value": "0.00000000000000000000000000001",
});
assert!(matches!(
Value::from_json(value),
Err(ValueConversionError::InvalidTaggedValue(_))
));
}
#[test]
fn typed_json_preserves_full_decimal_number_precision() {
let json: JsonValue = serde_json::from_str("0.1234567890123456789012345679").unwrap();
assert_eq!(
Value::from_plain_json(&json, &ValueKind::Decimal).unwrap(),
Value::decimal(parse_decimal("0.1234567890123456789012345679").unwrap())
);
}
#[test]
fn human_readable_numbers_use_digit_separators() {
assert_eq!(Value::Int(1_234_567).to_string(), "1_234_567");
assert_eq!(Value::Int(-1_234_567).to_string(), "-1_234_567");
assert_eq!(
Value::decimal(parse_decimal("1234567.8900").unwrap()).to_string(),
"1_234_567.89"
);
assert_eq!(Value::Duration(86_400).to_string(), "86_400s");
assert_eq!(Value::Int(1_234_567).to_plain_json(), json!(1_234_567));
assert_eq!(
Value::decimal(parse_decimal("1234567.8900").unwrap()).to_plain_json(),
json!("1234567.89")
);
}
#[test]
fn decimal_division_rejects_rounded_result() {
assert_eq!(
checked_decimal_div(parse_decimal("1").unwrap(), parse_decimal("3").unwrap()),
Err(DecimalError::Inexact {
operation: "division"
})
);
}
#[test]
fn decimal_multiplication_rejects_underflow_rounding() {
assert_eq!(
checked_decimal_mul(
parse_decimal("0.0000000000000000000000000001").unwrap(),
parse_decimal("0.1").unwrap()
),
Err(DecimalError::Inexact {
operation: "multiplication"
})
);
}
#[test]
fn decimal_addition_and_subtraction_reject_precision_rounding() {
let tenth = Decimal::new(1, 1);
assert_eq!(
checked_decimal_add(Decimal::MAX, tenth),
Err(DecimalError::Inexact {
operation: "addition"
})
);
assert_eq!(
checked_decimal_sub(Decimal::MAX, tenth),
Err(DecimalError::Inexact {
operation: "subtraction"
})
);
}
#[test]
fn decimal_division_accepts_exact_result() {
assert_eq!(
checked_decimal_div(parse_decimal("1").unwrap(), parse_decimal("4").unwrap()).unwrap(),
parse_decimal("0.25").unwrap()
);
}
#[test]
fn tagged_json_round_trips_enum() {
let value = Value::Enum {
type_name: "등급".to_owned(),
variant: "A".to_owned(),
};
assert_eq!(Value::from_json(value.to_json().unwrap()).unwrap(), value);
}
#[test]
fn typed_json_parses_date_and_duration() {
assert_eq!(
Value::from_plain_json(&json!("2026-07-11"), &ValueKind::Date).unwrap(),
Value::Date(NaiveDate::from_ymd_opt(2026, 7, 11).unwrap())
);
assert_eq!(
Value::from_plain_json(&json!(3_600), &ValueKind::Duration).unwrap(),
Value::Duration(3_600)
);
}
#[test]
fn conflict_reasons_are_stable() {
assert_eq!(
EvalValue::conflict(["z".to_owned(), "a".to_owned(), "a".to_owned()]),
EvalValue::Conflict {
reasons: vec!["a".to_owned(), "z".to_owned()]
}
);
}
}