1use chrono::{Datelike, Timelike};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum BooleanValue {
16 True,
17 False,
18 Yes,
19 No,
20 Accept,
21 Reject,
22}
23
24impl From<BooleanValue> for bool {
25 fn from(value: BooleanValue) -> bool {
26 matches!(
27 value,
28 BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept
29 )
30 }
31}
32
33impl From<&BooleanValue> for bool {
34 fn from(value: &BooleanValue) -> bool {
35 (*value).into() }
37}
38
39impl From<bool> for BooleanValue {
40 fn from(value: bool) -> BooleanValue {
41 if value {
42 BooleanValue::True
43 } else {
44 BooleanValue::False
45 }
46 }
47}
48
49impl std::ops::Not for BooleanValue {
50 type Output = BooleanValue;
51
52 fn not(self) -> Self::Output {
53 if self.into() {
54 BooleanValue::False
55 } else {
56 BooleanValue::True
57 }
58 }
59}
60
61impl std::ops::Not for &BooleanValue {
62 type Output = BooleanValue;
63
64 fn not(self) -> Self::Output {
65 if (*self).into() {
66 BooleanValue::False
67 } else {
68 BooleanValue::True
69 }
70 }
71}
72
73impl std::str::FromStr for BooleanValue {
74 type Err = String;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 match s.trim().to_lowercase().as_str() {
78 "true" => Ok(BooleanValue::True),
79 "false" => Ok(BooleanValue::False),
80 "yes" => Ok(BooleanValue::Yes),
81 "no" => Ok(BooleanValue::No),
82 "accept" => Ok(BooleanValue::Accept),
83 "reject" => Ok(BooleanValue::Reject),
84 _ => Err(format!("Invalid boolean: '{}'", s)),
85 }
86 }
87}
88
89impl BooleanValue {
90 #[must_use]
91 pub fn as_str(&self) -> &'static str {
92 match self {
93 BooleanValue::True => "true",
94 BooleanValue::False => "false",
95 BooleanValue::Yes => "yes",
96 BooleanValue::No => "no",
97 BooleanValue::Accept => "accept",
98 BooleanValue::Reject => "reject",
99 }
100 }
101}
102
103impl fmt::Display for BooleanValue {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "{}", self.as_str())
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110pub enum DurationUnit {
111 Year,
112 Month,
113 Week,
114 Day,
115 Hour,
116 Minute,
117 Second,
118 Millisecond,
119 Microsecond,
120}
121
122impl Serialize for DurationUnit {
123 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124 where
125 S: serde::Serializer,
126 {
127 serializer.serialize_str(&self.to_string())
128 }
129}
130
131impl<'de> Deserialize<'de> for DurationUnit {
132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133 where
134 D: serde::Deserializer<'de>,
135 {
136 let s = String::deserialize(deserializer)?;
137 s.parse().map_err(serde::de::Error::custom)
138 }
139}
140
141impl fmt::Display for DurationUnit {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let s = match self {
144 DurationUnit::Year => "years",
145 DurationUnit::Month => "months",
146 DurationUnit::Week => "weeks",
147 DurationUnit::Day => "days",
148 DurationUnit::Hour => "hours",
149 DurationUnit::Minute => "minutes",
150 DurationUnit::Second => "seconds",
151 DurationUnit::Millisecond => "milliseconds",
152 DurationUnit::Microsecond => "microseconds",
153 };
154 write!(f, "{}", s)
155 }
156}
157
158impl std::str::FromStr for DurationUnit {
159 type Err = String;
160
161 fn from_str(s: &str) -> Result<Self, Self::Err> {
162 match s.trim().to_lowercase().as_str() {
163 "year" | "years" => Ok(DurationUnit::Year),
164 "month" | "months" => Ok(DurationUnit::Month),
165 "week" | "weeks" => Ok(DurationUnit::Week),
166 "day" | "days" => Ok(DurationUnit::Day),
167 "hour" | "hours" => Ok(DurationUnit::Hour),
168 "minute" | "minutes" => Ok(DurationUnit::Minute),
169 "second" | "seconds" => Ok(DurationUnit::Second),
170 "millisecond" | "milliseconds" => Ok(DurationUnit::Millisecond),
171 "microsecond" | "microseconds" => Ok(DurationUnit::Microsecond),
172 _ => Err(format!("Unknown duration unit: '{}'", s)),
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
178pub struct TimezoneValue {
179 pub offset_hours: i8,
180 pub offset_minutes: u8,
181}
182
183impl fmt::Display for TimezoneValue {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 if self.offset_hours == 0 && self.offset_minutes == 0 {
186 write!(f, "Z")
187 } else {
188 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
189 let hours = self.offset_hours.abs();
190 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
191 }
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
196pub struct TimeValue {
197 pub hour: u8,
198 pub minute: u8,
199 pub second: u8,
200 pub timezone: Option<TimezoneValue>,
201}
202
203impl fmt::Display for TimeValue {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
210pub struct DateTimeValue {
211 pub year: i32,
212 pub month: u32,
213 pub day: u32,
214 pub hour: u32,
215 pub minute: u32,
216 pub second: u32,
217 #[serde(default)]
218 pub microsecond: u32,
219 pub timezone: Option<TimezoneValue>,
220}
221
222impl DateTimeValue {
223 pub fn now() -> Self {
224 let now = chrono::Local::now();
225 let offset_secs = now.offset().local_minus_utc();
226 Self {
227 year: now.year(),
228 month: now.month(),
229 day: now.day(),
230 hour: now.time().hour(),
231 minute: now.time().minute(),
232 second: now.time().second(),
233 microsecond: now.time().nanosecond() / 1000 % 1_000_000,
234 timezone: Some(TimezoneValue {
235 offset_hours: (offset_secs / 3600) as i8,
236 offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
237 }),
238 }
239 }
240
241 fn parse_iso_week(s: &str) -> Option<Self> {
242 let parts: Vec<&str> = s.split("-W").collect();
243 if parts.len() != 2 {
244 return None;
245 }
246 let year: i32 = parts[0].parse().ok()?;
247 let week: u32 = parts[1].parse().ok()?;
248 if week == 0 || week > 53 {
249 return None;
250 }
251 let date = chrono::NaiveDate::from_isoywd_opt(year, week, chrono::Weekday::Mon)?;
252 Some(Self {
253 year: date.year(),
254 month: date.month(),
255 day: date.day(),
256 hour: 0,
257 minute: 0,
258 second: 0,
259 microsecond: 0,
260 timezone: None,
261 })
262 }
263}
264
265impl fmt::Display for DateTimeValue {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 let has_time = self.hour != 0
268 || self.minute != 0
269 || self.second != 0
270 || self.microsecond != 0
271 || self.timezone.is_some();
272 if !has_time {
273 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
274 } else {
275 write!(
276 f,
277 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
278 self.year, self.month, self.day, self.hour, self.minute, self.second
279 )?;
280 if self.microsecond != 0 {
281 write!(f, ".{:06}", self.microsecond)?;
282 }
283 if let Some(tz) = &self.timezone {
284 write!(f, "{}", tz)?;
285 }
286 Ok(())
287 }
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum Value {
295 Number(Decimal),
296 Scale(Decimal, String),
297 Text(String),
298 Date(DateTimeValue),
299 Time(TimeValue),
300 Boolean(BooleanValue),
301 Duration(Decimal, DurationUnit),
302 Ratio(Decimal, Option<String>),
303}
304
305impl fmt::Display for Value {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 match self {
308 Value::Number(n) => write!(f, "{}", n),
309 Value::Text(s) => write!(f, "{}", s),
310 Value::Date(dt) => write!(f, "{}", dt),
311 Value::Boolean(b) => write!(f, "{}", b),
312 Value::Time(time) => write!(f, "{}", time),
313 Value::Scale(n, u) => write!(f, "{} {}", n, u),
314 Value::Duration(n, u) => write!(f, "{} {}", n, u),
315 Value::Ratio(n, u) => match u.as_deref() {
316 Some("percent") => {
317 let display_value = *n * Decimal::from(100);
318 let norm = display_value.normalize();
319 let s = if norm.fract().is_zero() {
320 norm.trunc().to_string()
321 } else {
322 norm.to_string()
323 };
324 write!(f, "{}%", s)
325 }
326 Some("permille") => {
327 let display_value = *n * Decimal::from(1000);
328 let norm = display_value.normalize();
329 let s = if norm.fract().is_zero() {
330 norm.trunc().to_string()
331 } else {
332 norm.to_string()
333 };
334 write!(f, "{}%%", s)
335 }
336 Some(unit) => {
337 let norm = n.normalize();
338 let s = if norm.fract().is_zero() {
339 norm.trunc().to_string()
340 } else {
341 norm.to_string()
342 };
343 write!(f, "{} {}", s, unit)
344 }
345 None => {
346 let norm = n.normalize();
347 let s = if norm.fract().is_zero() {
348 norm.trunc().to_string()
349 } else {
350 norm.to_string()
351 };
352 write!(f, "{}", s)
353 }
354 },
355 }
356 }
357}
358
359impl std::str::FromStr for DateTimeValue {
364 type Err = String;
365
366 fn from_str(s: &str) -> Result<Self, Self::Err> {
367 if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
368 let offset = dt.offset().local_minus_utc();
369 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
370 return Ok(DateTimeValue {
371 year: dt.year(),
372 month: dt.month(),
373 day: dt.day(),
374 hour: dt.hour(),
375 minute: dt.minute(),
376 second: dt.second(),
377 microsecond,
378 timezone: Some(TimezoneValue {
379 offset_hours: (offset / 3600) as i8,
380 offset_minutes: ((offset.abs() % 3600) / 60) as u8,
381 }),
382 });
383 }
384 if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
385 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
386 return Ok(DateTimeValue {
387 year: dt.year(),
388 month: dt.month(),
389 day: dt.day(),
390 hour: dt.hour(),
391 minute: dt.minute(),
392 second: dt.second(),
393 microsecond,
394 timezone: None,
395 });
396 }
397 if let Ok(d) = s.parse::<chrono::NaiveDate>() {
398 return Ok(DateTimeValue {
399 year: d.year(),
400 month: d.month(),
401 day: d.day(),
402 hour: 0,
403 minute: 0,
404 second: 0,
405 microsecond: 0,
406 timezone: None,
407 });
408 }
409 if let Some(week_val) = Self::parse_iso_week(s) {
410 return Ok(week_val);
411 }
412 if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
413 return Ok(Self {
414 year: ym.year(),
415 month: ym.month(),
416 day: 1,
417 hour: 0,
418 minute: 0,
419 second: 0,
420 microsecond: 0,
421 timezone: None,
422 });
423 }
424 if let Ok(year) = s.parse::<i32>() {
425 if (1..=9999).contains(&year) {
426 return Ok(Self {
427 year,
428 month: 1,
429 day: 1,
430 hour: 0,
431 minute: 0,
432 second: 0,
433 microsecond: 0,
434 timezone: None,
435 });
436 }
437 }
438 Err(format!("Invalid date format: '{}'", s))
439 }
440}
441
442impl std::str::FromStr for TimeValue {
443 type Err = String;
444
445 fn from_str(s: &str) -> Result<Self, Self::Err> {
446 if let Ok(t) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
447 let offset = t.offset().local_minus_utc();
448 return Ok(TimeValue {
449 hour: t.hour() as u8,
450 minute: t.minute() as u8,
451 second: t.second() as u8,
452 timezone: Some(TimezoneValue {
453 offset_hours: (offset / 3600) as i8,
454 offset_minutes: ((offset.abs() % 3600) / 60) as u8,
455 }),
456 });
457 }
458 if let Ok(t) = s.parse::<chrono::NaiveTime>() {
459 return Ok(TimeValue {
460 hour: t.hour() as u8,
461 minute: t.minute() as u8,
462 second: t.second() as u8,
463 timezone: None,
464 });
465 }
466 Err(format!("Invalid time format: '{}'", s))
467 }
468}
469
470pub(crate) struct NumberLiteral(pub Decimal);
472
473impl std::str::FromStr for NumberLiteral {
474 type Err = String;
475
476 fn from_str(s: &str) -> Result<Self, Self::Err> {
477 let clean = s.trim().replace(['_', ','], "");
478 let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
479 if digit_count > crate::limits::MAX_NUMBER_DIGITS {
480 return Err(format!(
481 "Number has too many digits (max {})",
482 crate::limits::MAX_NUMBER_DIGITS
483 ));
484 }
485 Decimal::from_str(&clean)
486 .map_err(|_| format!("Invalid number: '{}'", s))
487 .map(NumberLiteral)
488 }
489}
490
491pub(crate) struct TextLiteral(pub String);
493
494impl std::str::FromStr for TextLiteral {
495 type Err = String;
496
497 fn from_str(s: &str) -> Result<Self, Self::Err> {
498 if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
499 return Err(format!(
500 "Text value exceeds maximum length (max {} characters)",
501 crate::limits::MAX_TEXT_VALUE_LENGTH
502 ));
503 }
504 Ok(TextLiteral(s.to_string()))
505 }
506}
507
508pub(crate) struct DurationLiteral(pub Decimal, pub DurationUnit);
510
511impl std::str::FromStr for DurationLiteral {
512 type Err = String;
513
514 fn from_str(s: &str) -> Result<Self, Self::Err> {
515 let trimmed = s.trim();
516 let mut parts: Vec<&str> = trimmed.split_whitespace().collect();
517 if parts.len() < 2 {
518 return Err(format!(
519 "Invalid duration: '{}'. Expected format: <number> <unit> (e.g. 10 hours, 2 weeks)",
520 s
521 ));
522 }
523 let unit_str = parts.pop().unwrap();
524 let number_str = parts.join(" ");
525 let n = number_str
526 .parse::<NumberLiteral>()
527 .map_err(|_| format!("Invalid duration number: '{}'", number_str))?
528 .0;
529 let unit = unit_str.parse()?;
530 Ok(DurationLiteral(n, unit))
531 }
532}
533
534pub(crate) struct NumberWithUnit(pub Decimal, pub String);
536
537impl std::str::FromStr for NumberWithUnit {
538 type Err = String;
539
540 fn from_str(s: &str) -> Result<Self, Self::Err> {
541 let trimmed = s.trim();
542 let mut parts = trimmed.split_whitespace();
543 let number_part = parts.next().ok_or_else(|| {
544 if trimmed.is_empty() {
545 "Scale value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
546 .to_string()
547 } else {
548 format!(
549 "Invalid scale value: '{}'. Scale value must be a number followed by a unit (e.g. '10 eur').",
550 s
551 )
552 }
553 })?;
554 let unit_part = parts.next().ok_or_else(|| {
555 format!(
556 "Scale value must include a unit (e.g. '{} eur').",
557 number_part
558 )
559 })?;
560 let n = number_part
561 .parse::<NumberLiteral>()
562 .map_err(|_| format!("Invalid scale: '{}'", s))?
563 .0;
564 Ok(NumberWithUnit(n, unit_part.to_string()))
565 }
566}