Skip to main content

lemma/api/
types.rs

1//! LemmaType and related JSON shapes matching today's domain serde.
2
3use crate::computation::rational::RationalInteger;
4use crate::literals::{
5    BaseMeasureVector, DateTimeValue, MeasureUnit as DomainMeasureUnit, MeasureUnits,
6    RatioUnit as DomainRatioUnit, RatioUnits, TimeValue,
7};
8use crate::planning::semantics::{
9    LemmaType as DomainLemmaType, MeasureTrait as DomainMeasureTrait,
10    TypeDefiningSpec as DomainTypeDefiningSpec, TypeExtends as DomainTypeExtends,
11    TypeSpecification as DomainTypeSpecification,
12};
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15
16/// Exact rational factor as reduced numer/denom integer strings.
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct RationalFactor {
19    pub numer: String,
20    pub denom: String,
21}
22
23impl RationalFactor {
24    fn from_ratio(value: &RationalInteger) -> Self {
25        let reduced = value
26            .clone()
27            .try_reduce()
28            .expect("BUG: stored measure unit factor must reduce");
29        Self {
30            numer: reduced.numer_to_string(),
31            denom: reduced.denom_to_string(),
32        }
33    }
34}
35
36/// Unit-scoped bound `{ value, unit }` (decimal magnitude + unit name).
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct NamedBound {
39    pub value: Decimal,
40    pub unit: String,
41}
42
43fn planned_bound_decimal(rational: &RationalInteger) -> Decimal {
44    rational
45        .try_to_decimal()
46        .expect("BUG: planned bound must convert to decimal")
47}
48
49fn named_bound_from(bound: &(RationalInteger, String)) -> NamedBound {
50    NamedBound {
51        value: planned_bound_decimal(&bound.0),
52        unit: bound.1.clone(),
53    }
54}
55
56fn optional_named_bound(bound: &Option<(RationalInteger, String)>) -> Option<NamedBound> {
57    bound.as_ref().map(named_bound_from)
58}
59
60fn optional_decimal_bound(bound: &Option<RationalInteger>) -> Option<Decimal> {
61    bound.as_ref().map(planned_bound_decimal)
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum MeasureTrait {
67    Duration,
68    Calendar,
69}
70
71impl From<DomainMeasureTrait> for MeasureTrait {
72    fn from(value: DomainMeasureTrait) -> Self {
73        match value {
74            DomainMeasureTrait::Duration => Self::Duration,
75            DomainMeasureTrait::Calendar => Self::Calendar,
76        }
77    }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct MeasureUnit {
82    pub name: String,
83    pub factor: RationalFactor,
84    pub derived_measure_factors: Vec<(String, i32)>,
85    pub decomposition: BaseMeasureVector,
86    #[serde(skip_serializing_if = "Option::is_none", default)]
87    pub minimum: Option<Decimal>,
88    #[serde(skip_serializing_if = "Option::is_none", default)]
89    pub maximum: Option<Decimal>,
90    #[serde(
91        rename = "suggestion",
92        skip_serializing_if = "Option::is_none",
93        default
94    )]
95    pub suggestion_magnitude: Option<Decimal>,
96}
97
98impl From<&DomainMeasureUnit> for MeasureUnit {
99    fn from(unit: &DomainMeasureUnit) -> Self {
100        Self {
101            name: unit.name.clone(),
102            factor: RationalFactor::from_ratio(&unit.factor),
103            derived_measure_factors: unit.derived_measure_factors.clone(),
104            decomposition: unit.decomposition.clone(),
105            minimum: unit.minimum.as_ref().map(|minimum| {
106                minimum
107                    .try_to_decimal()
108                    .expect("BUG: planned measure unit minimum must convert to decimal")
109            }),
110            maximum: unit.maximum.as_ref().map(|maximum| {
111                maximum
112                    .try_to_decimal()
113                    .expect("BUG: planned measure unit maximum must convert to decimal")
114            }),
115            suggestion_magnitude: unit.suggestion_magnitude.as_ref().map(|suggestion| {
116                suggestion
117                    .try_to_decimal()
118                    .expect("BUG: planned measure unit suggestion must convert to decimal")
119            }),
120        }
121    }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct RatioUnit {
126    pub name: String,
127    pub value: RationalFactor,
128    #[serde(skip_serializing_if = "Option::is_none", default)]
129    pub minimum: Option<Decimal>,
130    #[serde(skip_serializing_if = "Option::is_none", default)]
131    pub maximum: Option<Decimal>,
132    #[serde(
133        rename = "suggestion",
134        skip_serializing_if = "Option::is_none",
135        default
136    )]
137    pub suggestion_magnitude: Option<Decimal>,
138}
139
140impl From<&DomainRatioUnit> for RatioUnit {
141    fn from(unit: &DomainRatioUnit) -> Self {
142        Self {
143            name: unit.name.clone(),
144            value: RationalFactor::from_ratio(&unit.value),
145            minimum: unit.minimum.as_ref().map(|minimum| {
146                minimum
147                    .try_to_decimal()
148                    .expect("BUG: planned ratio unit minimum must convert to decimal")
149            }),
150            maximum: unit.maximum.as_ref().map(|maximum| {
151                maximum
152                    .try_to_decimal()
153                    .expect("BUG: planned ratio unit maximum must convert to decimal")
154            }),
155            suggestion_magnitude: unit.suggestion_magnitude.as_ref().map(|suggestion| {
156                suggestion
157                    .try_to_decimal()
158                    .expect("BUG: planned ratio unit suggestion must convert to decimal")
159            }),
160        }
161    }
162}
163
164fn measure_units_from(units: &MeasureUnits) -> Vec<MeasureUnit> {
165    units.iter().map(MeasureUnit::from).collect()
166}
167
168fn ratio_units_from(units: &RatioUnits) -> Vec<RatioUnit> {
169    units.iter().map(RatioUnit::from).collect()
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
173#[serde(tag = "kind", rename_all = "snake_case")]
174pub enum TypeDefiningSpec {
175    Local,
176    Import,
177}
178
179impl From<&DomainTypeDefiningSpec> for TypeDefiningSpec {
180    fn from(value: &DomainTypeDefiningSpec) -> Self {
181        match value {
182            DomainTypeDefiningSpec::Local => Self::Local,
183            DomainTypeDefiningSpec::Import => Self::Import,
184        }
185    }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[serde(tag = "kind", rename_all = "snake_case")]
190pub enum TypeExtends {
191    Primitive,
192    Custom {
193        parent: String,
194        family: String,
195        defining_spec: TypeDefiningSpec,
196    },
197}
198
199impl From<&DomainTypeExtends> for TypeExtends {
200    fn from(value: &DomainTypeExtends) -> Self {
201        match value {
202            DomainTypeExtends::Primitive => Self::Primitive,
203            DomainTypeExtends::Custom {
204                parent,
205                family,
206                defining_spec,
207            } => Self::Custom {
208                parent: parent.clone(),
209                family: family.clone(),
210                defining_spec: TypeDefiningSpec::from(defining_spec),
211            },
212        }
213    }
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(tag = "kind", rename_all = "lowercase")]
218pub enum TypeSpecification {
219    Boolean {
220        help: String,
221    },
222    Measure {
223        #[serde(default)]
224        minimum: Option<NamedBound>,
225        #[serde(default)]
226        maximum: Option<NamedBound>,
227        decimals: Option<u8>,
228        units: Vec<MeasureUnit>,
229        #[serde(default)]
230        traits: Vec<MeasureTrait>,
231        #[serde(default)]
232        decomposition: Option<BaseMeasureVector>,
233        help: String,
234    },
235    Number {
236        #[serde(default)]
237        minimum: Option<Decimal>,
238        #[serde(default)]
239        maximum: Option<Decimal>,
240        decimals: Option<u8>,
241        help: String,
242    },
243    NumberRange {
244        #[serde(default)]
245        lower: Option<Decimal>,
246        #[serde(default)]
247        upper: Option<Decimal>,
248        #[serde(default)]
249        minimum: Option<Decimal>,
250        #[serde(default)]
251        maximum: Option<Decimal>,
252        help: String,
253    },
254    Ratio {
255        #[serde(default)]
256        minimum: Option<Decimal>,
257        #[serde(default)]
258        maximum: Option<Decimal>,
259        decimals: Option<u8>,
260        units: Vec<RatioUnit>,
261        help: String,
262    },
263    RatioRange {
264        #[serde(default)]
265        lower: Option<Decimal>,
266        #[serde(default)]
267        upper: Option<Decimal>,
268        #[serde(default)]
269        minimum: Option<Decimal>,
270        #[serde(default)]
271        maximum: Option<Decimal>,
272        units: Vec<RatioUnit>,
273        help: String,
274    },
275    Text {
276        length: Option<usize>,
277        options: Vec<String>,
278        help: String,
279    },
280    Date {
281        minimum: Option<DateTimeValue>,
282        maximum: Option<DateTimeValue>,
283        help: String,
284    },
285    DateRange {
286        lower: Option<DateTimeValue>,
287        upper: Option<DateTimeValue>,
288        #[serde(default)]
289        minimum: Option<NamedBound>,
290        #[serde(default)]
291        maximum: Option<NamedBound>,
292        help: String,
293    },
294    Time {
295        minimum: Option<TimeValue>,
296        maximum: Option<TimeValue>,
297        help: String,
298    },
299    TimeRange {
300        lower: Option<TimeValue>,
301        upper: Option<TimeValue>,
302        #[serde(default)]
303        minimum: Option<NamedBound>,
304        #[serde(default)]
305        maximum: Option<NamedBound>,
306        help: String,
307    },
308    MeasureRange {
309        #[serde(default)]
310        lower: Option<NamedBound>,
311        #[serde(default)]
312        upper: Option<NamedBound>,
313        #[serde(default)]
314        minimum: Option<NamedBound>,
315        #[serde(default)]
316        maximum: Option<NamedBound>,
317        units: Vec<MeasureUnit>,
318        #[serde(default)]
319        decomposition: Option<BaseMeasureVector>,
320        help: String,
321    },
322    Veto {
323        message: Option<String>,
324    },
325    Undetermined,
326}
327
328impl From<&DomainTypeSpecification> for TypeSpecification {
329    fn from(spec: &DomainTypeSpecification) -> Self {
330        match spec {
331            DomainTypeSpecification::Boolean { help } => Self::Boolean { help: help.clone() },
332            DomainTypeSpecification::Measure {
333                minimum,
334                maximum,
335                decimals,
336                units,
337                traits,
338                decomposition,
339                help,
340            } => Self::Measure {
341                minimum: optional_named_bound(minimum),
342                maximum: optional_named_bound(maximum),
343                decimals: *decimals,
344                units: measure_units_from(units),
345                traits: traits.iter().copied().map(MeasureTrait::from).collect(),
346                decomposition: decomposition.clone(),
347                help: help.clone(),
348            },
349            DomainTypeSpecification::Number {
350                minimum,
351                maximum,
352                decimals,
353                help,
354            } => Self::Number {
355                minimum: optional_decimal_bound(minimum),
356                maximum: optional_decimal_bound(maximum),
357                decimals: *decimals,
358                help: help.clone(),
359            },
360            DomainTypeSpecification::NumberRange {
361                lower,
362                upper,
363                minimum,
364                maximum,
365                help,
366            } => Self::NumberRange {
367                lower: optional_decimal_bound(lower),
368                upper: optional_decimal_bound(upper),
369                minimum: optional_decimal_bound(minimum),
370                maximum: optional_decimal_bound(maximum),
371                help: help.clone(),
372            },
373            DomainTypeSpecification::Ratio {
374                minimum,
375                maximum,
376                decimals,
377                units,
378                help,
379            } => Self::Ratio {
380                minimum: optional_decimal_bound(minimum),
381                maximum: optional_decimal_bound(maximum),
382                decimals: *decimals,
383                units: ratio_units_from(units),
384                help: help.clone(),
385            },
386            DomainTypeSpecification::RatioRange {
387                lower,
388                upper,
389                minimum,
390                maximum,
391                units,
392                help,
393            } => Self::RatioRange {
394                lower: optional_decimal_bound(lower),
395                upper: optional_decimal_bound(upper),
396                minimum: optional_decimal_bound(minimum),
397                maximum: optional_decimal_bound(maximum),
398                units: ratio_units_from(units),
399                help: help.clone(),
400            },
401            DomainTypeSpecification::Text {
402                length,
403                options,
404                help,
405            } => Self::Text {
406                length: *length,
407                options: options.clone(),
408                help: help.clone(),
409            },
410            DomainTypeSpecification::Date {
411                minimum,
412                maximum,
413                help,
414            } => Self::Date {
415                minimum: minimum.clone(),
416                maximum: maximum.clone(),
417                help: help.clone(),
418            },
419            DomainTypeSpecification::DateRange {
420                lower,
421                upper,
422                minimum,
423                maximum,
424                help,
425            } => Self::DateRange {
426                lower: lower.clone(),
427                upper: upper.clone(),
428                minimum: optional_named_bound(minimum),
429                maximum: optional_named_bound(maximum),
430                help: help.clone(),
431            },
432            DomainTypeSpecification::Time {
433                minimum,
434                maximum,
435                help,
436            } => Self::Time {
437                minimum: minimum.clone(),
438                maximum: maximum.clone(),
439                help: help.clone(),
440            },
441            DomainTypeSpecification::TimeRange {
442                lower,
443                upper,
444                minimum,
445                maximum,
446                help,
447            } => Self::TimeRange {
448                lower: lower.clone(),
449                upper: upper.clone(),
450                minimum: optional_named_bound(minimum),
451                maximum: optional_named_bound(maximum),
452                help: help.clone(),
453            },
454            DomainTypeSpecification::MeasureRange {
455                lower,
456                upper,
457                minimum,
458                maximum,
459                units,
460                decomposition,
461                help,
462            } => Self::MeasureRange {
463                lower: optional_named_bound(lower),
464                upper: optional_named_bound(upper),
465                minimum: optional_named_bound(minimum),
466                maximum: optional_named_bound(maximum),
467                units: measure_units_from(units),
468                decomposition: decomposition.clone(),
469                help: help.clone(),
470            },
471            DomainTypeSpecification::Veto { message } => Self::Veto {
472                message: message.clone(),
473            },
474            DomainTypeSpecification::Undetermined => Self::Undetermined,
475        }
476    }
477}
478
479#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
480pub struct LemmaType {
481    pub name: Option<String>,
482    #[serde(flatten)]
483    pub specifications: TypeSpecification,
484    pub extends: TypeExtends,
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub measure_binding_unit: Option<String>,
487}
488
489impl From<&DomainLemmaType> for LemmaType {
490    fn from(lemma_type: &DomainLemmaType) -> Self {
491        Self {
492            name: lemma_type.name.clone(),
493            specifications: TypeSpecification::from(&lemma_type.specifications),
494            extends: TypeExtends::from(&lemma_type.extends),
495            measure_binding_unit: lemma_type.measure_binding_unit.clone(),
496        }
497    }
498}