ion_schema/isl/
util.rs

1use crate::ion_extension::ElementExtensions;
2use crate::isl::ranges::{NumberRange, TimestampRange};
3use crate::isl::IslVersion;
4use crate::isl_require;
5use crate::result::{invalid_schema_error, IonSchemaError, IonSchemaResult};
6use ion_rs::TimestampPrecision as Precision;
7use ion_rs::{Element, IonResult, Value, ValueWriter, WriteAsIon};
8use ion_rs::{IonType, Timestamp};
9use num_traits::abs;
10use std::cmp::Ordering;
11use std::fmt;
12use std::fmt::{Display, Formatter};
13
14/// Represents an annotation for `annotations` constraint.
15/// ```ion
16/// Grammar: <ANNOTATION> ::= <SYMBOL>
17///                | required::<SYMBOL>
18///                | optional::<SYMBOL>
19/// ```
20/// `annotations`: `<https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#annotations>`
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Annotation {
23    isl_version: IslVersion,
24    value: String,
25    is_required: bool, // Specifies whether an annotation's occurrence is required or optional
26}
27
28impl Annotation {
29    pub fn new(value: String, is_required: bool, isl_version: IslVersion) -> Self {
30        Self {
31            value,
32            is_required,
33            isl_version,
34        }
35    }
36
37    pub fn value(&self) -> &String {
38        &self.value
39    }
40
41    pub fn is_required(&self) -> bool {
42        self.is_required
43    }
44
45    // Returns a bool value that represents if an annotation is required or not
46    pub(crate) fn is_annotation_required(value: &Element, list_level_required: bool) -> bool {
47        if value.annotations().contains("required") {
48            true
49        } else if list_level_required {
50            // if the value is annotated with `optional` then it overrides the list-level `required` behavior
51            !value.annotations().contains("optional")
52        } else {
53            // for any value the default annotation is `optional`
54            false
55        }
56    }
57}
58
59impl WriteAsIon for Annotation {
60    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
61        if self.isl_version == IslVersion::V1_0 {
62            if self.is_required {
63                writer
64                    .with_annotations(["required"])?
65                    .write_symbol(self.value.as_str())
66            } else {
67                writer
68                    .with_annotations(["optional"])?
69                    .write_symbol(self.value.as_str())
70            }
71        } else {
72            writer.write_symbol(self.value.as_str())
73        }
74    }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum TimestampPrecision {
79    Year,
80    Month,
81    Day,
82    Minute,
83    Second,
84    Millisecond,
85    Microsecond,
86    Nanosecond,
87    OtherFractionalSeconds(i64),
88}
89
90impl TimestampPrecision {
91    pub fn from_timestamp(timestamp_value: &Timestamp) -> TimestampPrecision {
92        use TimestampPrecision::*;
93        let precision_value = timestamp_value.precision();
94        match precision_value {
95            Precision::Year => Year,
96            Precision::Month => Month,
97            Precision::Day => Day,
98            Precision::HourAndMinute => Minute,
99            // `unwrap_or(0)` is a default to set second as timestamp precision.
100            // currently `fractional_seconds_scale` doesn't return 0 for Precision::Second,
101            // once that is fixed in ion-rust we can remove unwrap_or from here
102            Precision::Second => match timestamp_value.fractional_seconds_scale().unwrap_or(0) {
103                0 => Second,
104                3 => Millisecond,
105                6 => Microsecond,
106                9 => Nanosecond,
107                scale => OtherFractionalSeconds(scale),
108            },
109        }
110    }
111
112    fn string_value(&self) -> String {
113        match self {
114            TimestampPrecision::Year => "year".to_string(),
115            TimestampPrecision::Month => "month".to_string(),
116            TimestampPrecision::Day => "day".to_string(),
117            TimestampPrecision::Minute => "minute".to_string(),
118            TimestampPrecision::Second => "second".to_string(),
119            TimestampPrecision::Millisecond => "millisecond".to_string(),
120            TimestampPrecision::Microsecond => "microsecond".to_string(),
121            TimestampPrecision::Nanosecond => "nanosecond".to_string(),
122            TimestampPrecision::OtherFractionalSeconds(i) => format!("fractional second (10e{i})"),
123        }
124    }
125
126    pub(crate) fn int_value(&self) -> i64 {
127        use TimestampPrecision::*;
128        match self {
129            Year => -4,
130            Month => -3,
131            Day => -2,
132            Minute => -1,
133            Second => 0,
134            Millisecond => 3,
135            Microsecond => 6,
136            Nanosecond => 9,
137            OtherFractionalSeconds(scale) => *scale,
138        }
139    }
140}
141
142impl TryFrom<&str> for TimestampPrecision {
143    type Error = IonSchemaError;
144
145    fn try_from(value: &str) -> Result<Self, Self::Error> {
146        Ok(match value {
147            "year" => TimestampPrecision::Year,
148            "month" => TimestampPrecision::Month,
149            "day" => TimestampPrecision::Day,
150            "minute" => TimestampPrecision::Minute,
151            "second" => TimestampPrecision::Second,
152            "millisecond" => TimestampPrecision::Millisecond,
153            "microsecond" => TimestampPrecision::Microsecond,
154            "nanosecond" => TimestampPrecision::Nanosecond,
155            _ => {
156                return invalid_schema_error(format!(
157                    "Invalid timestamp precision specified {value}"
158                ))
159            }
160        })
161    }
162}
163
164impl PartialOrd for TimestampPrecision {
165    fn partial_cmp(&self, other: &TimestampPrecision) -> Option<Ordering> {
166        let self_value = self.int_value();
167        let other_value = other.int_value();
168
169        Some(self_value.cmp(&other_value))
170    }
171}
172
173impl WriteAsIon for TimestampPrecision {
174    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
175        writer.write_symbol(self.string_value().as_str())
176    }
177}
178
179impl From<TimestampPrecision> for Element {
180    fn from(value: TimestampPrecision) -> Self {
181        Element::symbol(value.string_value())
182    }
183}
184
185impl Display for TimestampPrecision {
186    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
187        f.write_str(&self.string_value())
188    }
189}
190
191/// Represents a valid value to be ued within `valid_values` constraint
192/// ValidValue could either be a range or Element
193/// ```ion
194/// Grammar: <VALID_VALUE> ::= <VALUE>
195///                | <RANGE<TIMESTAMP>>
196///                | <RANGE<NUMBER>>
197/// ```
198/// `valid_values`: `<https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#valid_values>`
199#[derive(Debug, Clone, PartialEq)]
200pub enum ValidValue {
201    NumberRange(NumberRange),
202    TimestampRange(TimestampRange),
203    Element(Value),
204}
205
206impl ValidValue {
207    pub fn from_ion_element(element: &Element, isl_version: IslVersion) -> IonSchemaResult<Self> {
208        let annotation = element.annotations();
209        if element.annotations().contains("range") {
210            isl_require!(annotation.len() == 1 => "Unexpected annotation(s) on valid values argument: {element}")?;
211            // Does it contain any timestamps
212            let has_timestamp = element.as_sequence().map_or(false, |s| {
213                s.elements().any(|it| it.ion_type() == IonType::Timestamp)
214            });
215            let range = if has_timestamp {
216                ValidValue::TimestampRange(TimestampRange::from_ion_element(element, |e| {
217                    e.as_timestamp()
218                        .filter(|t| isl_version != IslVersion::V1_0 || t.offset().is_some())
219                })?)
220            } else {
221                ValidValue::NumberRange(NumberRange::from_ion_element(
222                    element,
223                    Element::any_number_as_decimal,
224                )?)
225            };
226            Ok(range)
227        } else {
228            isl_require!(annotation.is_empty() => "Unexpected annotation(s) on valid values argument: {element}")?;
229            Ok(ValidValue::Element(element.value().to_owned()))
230        }
231    }
232}
233
234impl Display for ValidValue {
235    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
236        match self {
237            ValidValue::Element(element) => write!(f, "{element}"),
238            ValidValue::NumberRange(r) => write!(f, "{r}"),
239            ValidValue::TimestampRange(r) => write!(f, "{r}"),
240        }
241    }
242}
243
244impl WriteAsIon for ValidValue {
245    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
246        match self {
247            ValidValue::Element(value) => writer.write(value),
248            ValidValue::NumberRange(r) => writer.write(r),
249            ValidValue::TimestampRange(r) => writer.write(r),
250        }
251    }
252}
253
254impl From<NumberRange> for ValidValue {
255    fn from(number_range: NumberRange) -> Self {
256        ValidValue::NumberRange(number_range)
257    }
258}
259
260impl From<TimestampRange> for ValidValue {
261    fn from(timestamp_range: TimestampRange) -> Self {
262        ValidValue::TimestampRange(timestamp_range)
263    }
264}
265
266impl<T: Into<Value>> From<T> for ValidValue {
267    fn from(value: T) -> Self {
268        ValidValue::Element(value.into())
269    }
270}
271
272/// Represent a timestamp offset
273/// Known timestamp offset value is stored in minutes as i32 value
274/// For example, "+07::00" wil be stored as `TimestampOffset::Known(420)`
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub enum TimestampOffset {
277    Known(i32), // represents known timestamp offset in minutes
278    Unknown,    // represents unknown timestamp offset "-00:00"
279}
280
281impl TryFrom<&str> for TimestampOffset {
282    type Error = IonSchemaError;
283
284    fn try_from(string_value: &str) -> Result<Self, Self::Error> {
285        // unknown offset will be stored as None
286        if string_value == "-00:00" {
287            Ok(TimestampOffset::Unknown)
288        } else {
289            if string_value.len() != 6 || string_value.chars().nth(3).unwrap() != ':' {
290                return invalid_schema_error(
291                    "`timestamp_offset` values must be of the form \"[+|-]hh:mm\"",
292                );
293            }
294            // convert string offset value into an i32 value of offset in minutes
295            let h = &string_value[1..3];
296            let m = &string_value[4..6];
297            let sign = match &string_value[..1] {
298                "-" => -1,
299                "+" => 1,
300                _ => {
301                    return invalid_schema_error(format!(
302                        "unrecognized `timestamp_offset` sign '{}'",
303                        &string_value[..1]
304                    ))
305                }
306            };
307            match (h.parse::<i32>(), m.parse::<i32>()) {
308                (Ok(hours), Ok(minutes))
309                    if (0..24).contains(&hours) && (0..60).contains(&minutes) =>
310                {
311                    Ok(TimestampOffset::Known(sign * (hours * 60 + minutes)))
312                }
313                _ => invalid_schema_error(format!("invalid timestamp offset {string_value}")),
314            }
315        }
316    }
317}
318
319impl From<Option<i32>> for TimestampOffset {
320    fn from(value: Option<i32>) -> Self {
321        use TimestampOffset::*;
322        match value {
323            None => Unknown,
324            Some(offset_in_minutes) => Known(offset_in_minutes),
325        }
326    }
327}
328
329impl Display for TimestampOffset {
330    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
331        use TimestampOffset::*;
332        match &self {
333            Unknown => write!(f, "-00:00"),
334            Known(offset) => {
335                let sign = if offset < &0 { "-" } else { "+" };
336                let hours = abs(*offset) / 60;
337                let minutes = abs(*offset) - hours * 60;
338                write!(f, "{sign}{hours:02}:{minutes:02}")
339            }
340        }
341    }
342}
343
344impl WriteAsIon for TimestampOffset {
345    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
346        match &self {
347            TimestampOffset::Known(offset) => {
348                let sign = if offset < &0 { "-" } else { "+" };
349                let hours = abs(*offset) / 60;
350                let minutes = abs(*offset) - hours * 60;
351                writer.write_string(format!("{sign}{hours:02}:{minutes:02}"))
352            }
353            TimestampOffset::Unknown => writer.write_string("-00:00"),
354        }
355    }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum Ieee754InterchangeFormat {
360    Binary16,
361    Binary32,
362    Binary64,
363}
364
365impl TryFrom<&str> for Ieee754InterchangeFormat {
366    type Error = IonSchemaError;
367    fn try_from(string_value: &str) -> Result<Self, Self::Error> {
368        use Ieee754InterchangeFormat::*;
369        match string_value {
370            "binary16" => Ok(Binary16),
371            "binary32" => Ok(Binary32),
372            "binary64" => Ok(Binary64),
373            _ => invalid_schema_error(format!(
374                "unrecognized `ieee754_float` value {}",
375                &string_value
376            )),
377        }
378    }
379}
380
381impl Display for Ieee754InterchangeFormat {
382    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
383        write!(
384            f,
385            "{}",
386            match self {
387                Ieee754InterchangeFormat::Binary16 => "binary16",
388                Ieee754InterchangeFormat::Binary32 => "binary32",
389                Ieee754InterchangeFormat::Binary64 => "binary64",
390            }
391        )
392    }
393}
394
395impl WriteAsIon for Ieee754InterchangeFormat {
396    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
397        writer.write_symbol(match self {
398            Ieee754InterchangeFormat::Binary16 => "binary16",
399            Ieee754InterchangeFormat::Binary32 => "binary32",
400            Ieee754InterchangeFormat::Binary64 => "binary64",
401        })
402    }
403}