use std::fmt;
use crate::planning::semantics::{
BoundValueKind, DataPath, LemmaType, LiteralValue, SemanticDateTime, SemanticTime,
TypeSpecification,
};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum VetoType {
MissingData {
data: DataPath,
suggestion: Option<String>,
},
MissingRowData { key: String },
UserDefined { message: Option<String> },
Computation { message: String },
}
impl fmt::Display for VetoType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VetoType::MissingData {
data,
suggestion: Some(suggestion),
} => write!(
f,
"Required data '{data}' was not provided. Did you mean '{suggestion}'?"
),
VetoType::MissingData {
data,
suggestion: None,
} => write!(f, "Required data '{data}' was not provided."),
VetoType::MissingRowData { key } => match split_numbered_row_field_key(key) {
Some((list, number, field)) => write!(
f,
"Required field '{field}' was not provided on row {number} of list '{list}'."
),
None => write!(f, "Required data '{key}' was not provided."),
},
VetoType::UserDefined { message: Some(msg) } => write!(f, "{msg}"),
VetoType::UserDefined { message: None } => write!(f, "This rule was vetoed."),
VetoType::Computation { message } => write!(f, "{message}"),
}
}
}
fn split_numbered_row_field_key(key: &str) -> Option<(&str, &str, &str)> {
let (list_and_number, field) = key.rsplit_once('.')?;
let (list, number) = list_and_number.rsplit_once('.')?;
if number.is_empty() || !number.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
if list.is_empty() || field.is_empty() {
return None;
}
Some((list, number, field))
}
impl VetoType {
#[must_use]
pub fn computation(message: impl Into<String>) -> Self {
VetoType::Computation {
message: message.into(),
}
}
#[must_use]
pub fn missing_data(data: DataPath, suggestion: Option<String>) -> Self {
VetoType::MissingData { data, suggestion }
}
}
impl Serialize for VetoType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationResult {
Value(BoundValueKind),
Veto(VetoType),
}
impl OperationResult {
pub fn from_literal(value: LiteralValue) -> Self {
Self::Value(BoundValueKind::unbound(value.value))
}
pub fn from_bound(bound: BoundValueKind) -> Self {
Self::Value(bound)
}
pub fn vetoed(&self) -> bool {
matches!(self, OperationResult::Veto(_))
}
#[must_use]
pub fn is_missing_data(&self) -> bool {
matches!(
self,
OperationResult::Veto(VetoType::MissingData { .. } | VetoType::MissingRowData { .. })
)
}
#[must_use]
pub fn value(&self) -> Option<&BoundValueKind> {
match self {
OperationResult::Value(value) => Some(value),
OperationResult::Veto(_) => None,
}
}
#[must_use]
pub fn literal_value(&self) -> Option<LiteralValue> {
self.value().map(BoundValueKind::to_literal)
}
pub fn number(number: rust_decimal::Decimal) -> Self {
Self::from_literal(LiteralValue::number_from_decimal(number))
}
pub fn measure(
value: rust_decimal::Decimal,
unit: impl Into<String>,
lemma_type: Option<LemmaType>,
) -> Self {
use crate::computation::rational::checked_mul;
let lemma_type = std::sync::Arc::new(
lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::measure())),
);
let unit_name = unit.into();
let rational = crate::literals::rational_from_parsed_decimal(value)
.expect("BUG: operation result measure must lift at boundary");
let factor = if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
units
.get(&unit_name)
.map(|u| u.factor.clone())
.unwrap_or_else(|_| {
panic!(
"BUG: OperationResult::measure unit '{}' not declared on type",
unit_name
)
})
} else {
crate::computation::rational::rational_one()
};
let canonical = checked_mul(&rational, &factor)
.expect("BUG: measure canonicalization overflow in OperationResult::measure");
Self::from_bound(BoundValueKind {
value: crate::planning::semantics::ValueKind::Measure(canonical),
unit: Some(std::sync::Arc::from(unit_name)),
})
}
pub fn text(text: impl Into<String>) -> Self {
Self::from_literal(LiteralValue::text(text.into()))
}
pub fn date(date: impl Into<SemanticDateTime>) -> Self {
Self::from_literal(LiteralValue::date(date.into()))
}
pub fn time(time: impl Into<SemanticTime>) -> Self {
Self::from_literal(LiteralValue::time(time.into()))
}
pub fn boolean(boolean: bool) -> Self {
Self::from_literal(LiteralValue::from_bool(boolean))
}
pub fn ratio(rational: rust_decimal::Decimal) -> Self {
Self::from_literal(LiteralValue::ratio_from_decimal(rational))
}
pub fn veto(veto: impl Into<String>) -> Self {
Self::Veto(VetoType::UserDefined {
message: Some(veto.into()),
})
}
}
#[cfg(test)]
mod tests {
use super::VetoType;
use crate::planning::semantics::DataPath;
#[test]
fn veto_type_serializes_as_display_string() {
let v = VetoType::missing_data(DataPath::new(vec![], "product".to_string()), None);
let json = serde_json::to_string(&v).expect("serialize");
assert_eq!(json, "\"Required data 'product' was not provided.\"");
}
#[test]
fn missing_row_data_display_names_list_row_and_field() {
let v = VetoType::MissingRowData {
key: "menu.2.price".to_string(),
};
assert_eq!(
v.to_string(),
"Required field 'price' was not provided on row 2 of list 'menu'."
);
}
}