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        #[serde(default, skip_serializing_if = "Option::is_none")]
234        unit: Option<String>,
235        help: String,
236    },
237    Number {
238        #[serde(default)]
239        minimum: Option<Decimal>,
240        #[serde(default)]
241        maximum: Option<Decimal>,
242        decimals: Option<u8>,
243        help: String,
244    },
245    NumberRange {
246        #[serde(default)]
247        lower: Option<Decimal>,
248        #[serde(default)]
249        upper: Option<Decimal>,
250        #[serde(default)]
251        minimum: Option<Decimal>,
252        #[serde(default)]
253        maximum: Option<Decimal>,
254        help: String,
255    },
256    Ratio {
257        #[serde(default)]
258        minimum: Option<Decimal>,
259        #[serde(default)]
260        maximum: Option<Decimal>,
261        decimals: Option<u8>,
262        units: Vec<RatioUnit>,
263        #[serde(default, skip_serializing_if = "Option::is_none")]
264        unit: Option<String>,
265        help: String,
266    },
267    RatioRange {
268        #[serde(default)]
269        lower: Option<Decimal>,
270        #[serde(default)]
271        upper: Option<Decimal>,
272        #[serde(default)]
273        minimum: Option<Decimal>,
274        #[serde(default)]
275        maximum: Option<Decimal>,
276        units: Vec<RatioUnit>,
277        help: String,
278    },
279    Text {
280        length: Option<usize>,
281        options: Vec<String>,
282        help: String,
283    },
284    Date {
285        minimum: Option<DateTimeValue>,
286        maximum: Option<DateTimeValue>,
287        help: String,
288    },
289    DateRange {
290        lower: Option<DateTimeValue>,
291        upper: Option<DateTimeValue>,
292        #[serde(default)]
293        minimum: Option<NamedBound>,
294        #[serde(default)]
295        maximum: Option<NamedBound>,
296        help: String,
297    },
298    Time {
299        minimum: Option<TimeValue>,
300        maximum: Option<TimeValue>,
301        help: String,
302    },
303    TimeRange {
304        lower: Option<TimeValue>,
305        upper: Option<TimeValue>,
306        #[serde(default)]
307        minimum: Option<NamedBound>,
308        #[serde(default)]
309        maximum: Option<NamedBound>,
310        help: String,
311    },
312    MeasureRange {
313        #[serde(default)]
314        lower: Option<NamedBound>,
315        #[serde(default)]
316        upper: Option<NamedBound>,
317        #[serde(default)]
318        minimum: Option<NamedBound>,
319        #[serde(default)]
320        maximum: Option<NamedBound>,
321        units: Vec<MeasureUnit>,
322        #[serde(default)]
323        decomposition: Option<BaseMeasureVector>,
324        help: String,
325    },
326    Veto {
327        message: Option<String>,
328    },
329    Undetermined,
330    Spec {
331        repository: Option<String>,
332        spec: String,
333        effective: String,
334    },
335}
336
337impl From<&DomainTypeSpecification> for TypeSpecification {
338    fn from(spec: &DomainTypeSpecification) -> Self {
339        match spec {
340            DomainTypeSpecification::Boolean { help } => Self::Boolean { help: help.clone() },
341            DomainTypeSpecification::Measure {
342                minimum,
343                maximum,
344                decimals,
345                units,
346                traits,
347                decomposition,
348                unit,
349                help,
350            } => Self::Measure {
351                minimum: optional_named_bound(minimum),
352                maximum: optional_named_bound(maximum),
353                decimals: *decimals,
354                units: measure_units_from(units),
355                traits: traits.iter().copied().map(MeasureTrait::from).collect(),
356                decomposition: decomposition.clone(),
357                unit: unit.clone(),
358                help: help.clone(),
359            },
360            DomainTypeSpecification::Number {
361                minimum,
362                maximum,
363                decimals,
364                help,
365            } => Self::Number {
366                minimum: optional_decimal_bound(minimum),
367                maximum: optional_decimal_bound(maximum),
368                decimals: *decimals,
369                help: help.clone(),
370            },
371            DomainTypeSpecification::NumberRange {
372                lower,
373                upper,
374                minimum,
375                maximum,
376                help,
377            } => Self::NumberRange {
378                lower: optional_decimal_bound(lower),
379                upper: optional_decimal_bound(upper),
380                minimum: optional_decimal_bound(minimum),
381                maximum: optional_decimal_bound(maximum),
382                help: help.clone(),
383            },
384            DomainTypeSpecification::Ratio {
385                minimum,
386                maximum,
387                decimals,
388                units,
389                unit,
390                help,
391            } => Self::Ratio {
392                minimum: optional_decimal_bound(minimum),
393                maximum: optional_decimal_bound(maximum),
394                decimals: *decimals,
395                units: ratio_units_from(units),
396                unit: unit.clone(),
397                help: help.clone(),
398            },
399            DomainTypeSpecification::RatioRange {
400                lower,
401                upper,
402                minimum,
403                maximum,
404                units,
405                help,
406            } => Self::RatioRange {
407                lower: optional_decimal_bound(lower),
408                upper: optional_decimal_bound(upper),
409                minimum: optional_decimal_bound(minimum),
410                maximum: optional_decimal_bound(maximum),
411                units: ratio_units_from(units),
412                help: help.clone(),
413            },
414            DomainTypeSpecification::Text {
415                length,
416                options,
417                help,
418            } => Self::Text {
419                length: *length,
420                options: options.clone(),
421                help: help.clone(),
422            },
423            DomainTypeSpecification::Date {
424                minimum,
425                maximum,
426                help,
427            } => Self::Date {
428                minimum: minimum.clone(),
429                maximum: maximum.clone(),
430                help: help.clone(),
431            },
432            DomainTypeSpecification::DateRange {
433                lower,
434                upper,
435                minimum,
436                maximum,
437                help,
438            } => Self::DateRange {
439                lower: lower.clone(),
440                upper: upper.clone(),
441                minimum: optional_named_bound(minimum),
442                maximum: optional_named_bound(maximum),
443                help: help.clone(),
444            },
445            DomainTypeSpecification::Time {
446                minimum,
447                maximum,
448                help,
449            } => Self::Time {
450                minimum: minimum.clone(),
451                maximum: maximum.clone(),
452                help: help.clone(),
453            },
454            DomainTypeSpecification::TimeRange {
455                lower,
456                upper,
457                minimum,
458                maximum,
459                help,
460            } => Self::TimeRange {
461                lower: lower.clone(),
462                upper: upper.clone(),
463                minimum: optional_named_bound(minimum),
464                maximum: optional_named_bound(maximum),
465                help: help.clone(),
466            },
467            DomainTypeSpecification::MeasureRange {
468                lower,
469                upper,
470                minimum,
471                maximum,
472                units,
473                decomposition,
474                help,
475            } => Self::MeasureRange {
476                lower: optional_named_bound(lower),
477                upper: optional_named_bound(upper),
478                minimum: optional_named_bound(minimum),
479                maximum: optional_named_bound(maximum),
480                units: measure_units_from(units),
481                decomposition: decomposition.clone(),
482                help: help.clone(),
483            },
484            DomainTypeSpecification::Veto { message } => Self::Veto {
485                message: message.clone(),
486            },
487            DomainTypeSpecification::Undetermined => Self::Undetermined,
488            DomainTypeSpecification::Spec { spec } => Self::Spec {
489                repository: spec.repository.clone(),
490                spec: spec.spec.clone(),
491                effective: spec.effective.to_string(),
492            },
493        }
494    }
495}
496
497#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
498pub struct LemmaType {
499    pub name: Option<String>,
500    #[serde(flatten)]
501    pub specifications: TypeSpecification,
502    pub extends: TypeExtends,
503}
504
505impl From<&DomainLemmaType> for LemmaType {
506    fn from(lemma_type: &DomainLemmaType) -> Self {
507        Self {
508            name: lemma_type.name.clone(),
509            specifications: TypeSpecification::from(&lemma_type.specifications),
510            extends: TypeExtends::from(&lemma_type.extends),
511        }
512    }
513}