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