diff --git a/ion-schema/src/constraint.rs b/ion-schema/src/constraint.rs index 2e55aab..4a0c77b 100644 --- a/ion-schema/src/constraint.rs +++ b/ion-schema/src/constraint.rs @@ -1,13 +1,15 @@ +use crate::ion_extension::ElementExtensions; use crate::ion_path::{IonPath, IonPathElement}; use crate::isl::isl_constraint::{IslAnnotationsConstraint, IslConstraintImpl, IslRegexConstraint}; -use crate::isl::isl_range::{Range, RangeImpl}; use crate::isl::isl_type_reference::{ IslTypeRefImpl, IslVariablyOccurringTypeRef, NullabilityModifier, }; +use crate::isl::ranges::{I64Range, Limit, TimestampPrecisionRange, U64Range, UsizeRange}; use crate::isl::util::{ Annotation, Ieee754InterchangeFormat, TimestampOffset, TimestampPrecision, ValidValue, }; use crate::isl::IslVersion; +use crate::isl_require; use crate::nfa::{FinalState, NfaBuilder, NfaEvaluation}; use crate::result::{ invalid_schema_error, invalid_schema_error_raw, IonSchemaResult, ValidationResult, @@ -18,8 +20,9 @@ use crate::types::TypeValidator; use crate::violation::{Violation, ViolationCode}; use crate::IonSchemaElement; use ion_rs::element::Element; +use ion_rs::element::Value; use ion_rs::IonData; -use ion_rs::{Int, IonType}; +use ion_rs::IonType; use num_traits::ToPrimitive; use regex::{Regex, RegexBuilder}; use std::collections::{HashMap, HashSet}; @@ -130,7 +133,7 @@ impl Constraint { .map(|id| { VariablyOccurringTypeRef::new( TypeReference::new(*id, NullabilityModifier::Nothing), - Range::required(), + UsizeRange::new_single_value(1), ) }) .collect(); @@ -142,23 +145,19 @@ impl Constraint { Constraint::Contains(ContainsConstraint::new(values.into())) } - /// Creates a [Constraint::ContainerLength] from a [Range] specifying a length range. - pub fn container_length(length: RangeImpl) -> Constraint { - Constraint::ContainerLength(ContainerLengthConstraint::new(Range::NonNegativeInteger( - length, - ))) + /// Creates a [Constraint::ContainerLength] from a [UsizeRange] specifying a length range. + pub fn container_length(length: UsizeRange) -> Constraint { + Constraint::ContainerLength(ContainerLengthConstraint::new(length)) } - /// Creates a [Constraint::ByteLength] from a [Range] specifying a length range. - pub fn byte_length(length: RangeImpl) -> Constraint { - Constraint::ByteLength(ByteLengthConstraint::new(Range::NonNegativeInteger(length))) + /// Creates a [Constraint::ByteLength] from a [UsizeRange] specifying a length range. + pub fn byte_length(length: UsizeRange) -> Constraint { + Constraint::ByteLength(ByteLengthConstraint::new(length)) } - /// Creates a [Constraint::CodePointLength] from a [Range] specifying a length range. - pub fn codepoint_length(length: RangeImpl) -> Constraint { - Constraint::CodepointLength(CodepointLengthConstraint::new(Range::NonNegativeInteger( - length, - ))) + /// Creates a [Constraint::CodePointLength] from a [UsizeRange] specifying a length range. + pub fn codepoint_length(length: UsizeRange) -> Constraint { + Constraint::CodepointLength(CodepointLengthConstraint::new(length)) } /// Creates a [Constraint::Element] referring to the type represented by the provided [TypeId] and the boolean represents whether distinct elements are required or not. @@ -204,26 +203,22 @@ impl Constraint { } /// Creates a [Constraint::Precision] from a [Range] specifying a precision range. - pub fn precision(precision: RangeImpl) -> Constraint { - Constraint::Precision(PrecisionConstraint::new(Range::NonNegativeInteger( - precision, - ))) + pub fn precision(precision: U64Range) -> Constraint { + Constraint::Precision(PrecisionConstraint::new(precision)) } /// Creates a [Constraint::Scale] from a [Range] specifying a precision range. - pub fn scale(scale: RangeImpl) -> Constraint { - Constraint::Scale(ScaleConstraint::new(Range::Integer(scale))) + pub fn scale(scale: I64Range) -> Constraint { + Constraint::Scale(ScaleConstraint::new(scale)) } /// Creates a [Constraint::Exponent] from a [Range] specifying an exponent range. - pub fn exponent(exponent: RangeImpl) -> Constraint { - Constraint::Exponent(ExponentConstraint::new(Range::Integer(exponent))) + pub fn exponent(exponent: I64Range) -> Constraint { + Constraint::Exponent(ExponentConstraint::new(exponent)) } /// Creates a [Constraint::TimestampPrecision] from a [Range] specifying a precision range. - pub fn timestamp_precision(precision: RangeImpl) -> Constraint { - Constraint::TimestampPrecision(TimestampPrecisionConstraint::new( - Range::TimestampPrecision(precision), - )) + pub fn timestamp_precision(precision: TimestampPrecisionRange) -> Constraint { + Constraint::TimestampPrecision(TimestampPrecisionConstraint::new(precision)) } /// Creates an [Constraint::TimestampOffset] using the offset list specified in it @@ -232,10 +227,8 @@ impl Constraint { } /// Creates a [Constraint::Utf8ByteLength] from a [Range] specifying a length range. - pub fn utf8_byte_length(length: RangeImpl) -> Constraint { - Constraint::Utf8ByteLength(Utf8ByteLengthConstraint::new(Range::NonNegativeInteger( - length, - ))) + pub fn utf8_byte_length(length: UsizeRange) -> Constraint { + Constraint::Utf8ByteLength(Utf8ByteLengthConstraint::new(length)) } /// Creates a [Constraint::Fields] referring to the fields represented by the provided field name and [TypeId]s. @@ -250,7 +243,7 @@ impl Constraint { field_name, VariablyOccurringTypeRef::new( TypeReference::new(type_id, NullabilityModifier::Nothing), - Range::optional(), + UsizeRange::zero_or_one(), ), ) }) @@ -268,24 +261,14 @@ impl Constraint { /// Creates a [Constraint::ValidValues] using the [Element]s specified inside it /// Returns an IonSchemaError if any of the Elements have an annotation other than `range` - pub fn valid_values_with_values( - values: Vec, + pub fn valid_values( + valid_values: Vec, isl_version: IslVersion, ) -> IonSchemaResult { - let valid_values: IonSchemaResult> = values - .iter() - .map(|e| ValidValue::from_ion_element(e, isl_version)) - .collect(); - Ok(Constraint::ValidValues(ValidValuesConstraint { - valid_values: valid_values?, - })) - } - - /// Creates a [Constraint::ValidValues] using the [Range] specified inside it - pub fn valid_values_with_range(value: Range) -> Constraint { - Constraint::ValidValues(ValidValuesConstraint { - valid_values: vec![ValidValue::Range(value)], - }) + Ok(Constraint::ValidValues(ValidValuesConstraint::new( + valid_values, + isl_version, + )?)) } /// Creates a [Constraint::Regex] from the expression and flags (case_insensitive, multi_line) and also specify the ISL version @@ -482,9 +465,12 @@ impl Constraint { )?, )) } - IslConstraintImpl::Precision(precision_range) => Ok(Constraint::Precision( - PrecisionConstraint::new(precision_range.to_owned()), - )), + IslConstraintImpl::Precision(precision_range) => { + isl_require!(precision_range.lower() != &Limit::Inclusive(0) => "precision range must have non-zero values")?; + Ok(Constraint::Precision(PrecisionConstraint::new( + precision_range.to_owned(), + ))) + } IslConstraintImpl::Regex(regex) => Ok(Constraint::Regex(RegexConstraint::from_isl( regex, isl_version, @@ -872,10 +858,9 @@ impl OrderedElementsConstraint { let mut final_states = HashSet::new(); for (state_id, variably_occurring_type_reference) in type_ids.iter().enumerate() { let type_reference = variably_occurring_type_reference.type_ref(); - let occurs_range: &Range = variably_occurring_type_reference.occurs_range(); - - // unwrap here won't lead to panic as the check for non negative range was already done while parsing ordered_elements constraint - let (min, max) = occurs_range.non_negative_range_boundaries().unwrap(); + let (min, max) = variably_occurring_type_reference + .occurs_range() + .inclusive_endpoints(); // if the current state is required then that is the only final state till now if min > 0 { @@ -1050,10 +1035,10 @@ impl ConstraintValidator for FieldsConstraint { ion_path.push(IonPathElement::Field(field_name.to_owned())); // perform occurs validation for type_def for all values of the given field_name - let occurs_range: &Range = variably_occurring_type_ref.occurs_range(); + let occurs_range: &UsizeRange = variably_occurring_type_ref.occurs_range(); // verify if values follow occurs_range constraint - if !occurs_range.contains(&(values.len() as i64).into()) { + if !occurs_range.contains(&values.len()) { violations.push(Violation::new( "fields", ViolationCode::TypeMismatched, @@ -1242,15 +1227,15 @@ impl ConstraintValidator for ContainsConstraint { /// [container_length]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#container_length #[derive(Debug, Clone, PartialEq)] pub struct ContainerLengthConstraint { - length_range: Range, + length_range: UsizeRange, } impl ContainerLengthConstraint { - pub fn new(length_range: Range) -> Self { + pub fn new(length_range: UsizeRange) -> Self { Self { length_range } } - pub fn length(&self) -> &Range { + pub fn length(&self) -> &UsizeRange { &self.length_range } } @@ -1296,10 +1281,10 @@ impl ConstraintValidator for ContainerLengthConstraint { }; // get isl length as a range - let length_range: &Range = self.length(); + let length_range: &UsizeRange = self.length(); // return a Violation if the container size didn't follow container_length constraint - if !length_range.contains(&(size as i64).into()) { + if !length_range.contains(&size) { return Err(Violation::new( "container_length", ViolationCode::InvalidLength, @@ -1316,15 +1301,15 @@ impl ConstraintValidator for ContainerLengthConstraint { /// [byte_length]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#byte_length #[derive(Debug, Clone, PartialEq)] pub struct ByteLengthConstraint { - length_range: Range, + length_range: UsizeRange, } impl ByteLengthConstraint { - pub fn new(length_range: Range) -> Self { + pub fn new(length_range: UsizeRange) -> Self { Self { length_range } } - pub fn length(&self) -> &Range { + pub fn length(&self) -> &UsizeRange { &self.length_range } } @@ -1344,10 +1329,10 @@ impl ConstraintValidator for ByteLengthConstraint { .len(); // get isl length as a range - let length_range: &Range = self.length(); + let length_range: &UsizeRange = self.length(); // return a Violation if the clob/blob size didn't follow byte_length constraint - if !length_range.contains(&(size as i64).into()) { + if !length_range.contains(&size) { return Err(Violation::new( "byte_length", ViolationCode::InvalidLength, @@ -1364,15 +1349,15 @@ impl ConstraintValidator for ByteLengthConstraint { /// [codepoint_length]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#codepoint_length #[derive(Debug, Clone, PartialEq)] pub struct CodepointLengthConstraint { - length_range: Range, + length_range: UsizeRange, } impl CodepointLengthConstraint { - pub fn new(length_range: Range) -> Self { + pub fn new(length_range: UsizeRange) -> Self { Self { length_range } } - pub fn length(&self) -> &Range { + pub fn length(&self) -> &UsizeRange { &self.length_range } } @@ -1397,10 +1382,10 @@ impl ConstraintValidator for CodepointLengthConstraint { .count(); // get isl length as a range - let length_range: &Range = self.length(); + let length_range: &UsizeRange = self.length(); // return a Violation if the string/symbol codepoint size didn't follow codepoint_length constraint - if !length_range.contains(&(size as i64).into()) { + if !length_range.contains(&size) { return Err(Violation::new( "codepoint_length", ViolationCode::InvalidLength, @@ -1785,15 +1770,15 @@ impl ConstraintValidator for AnnotationsConstraint { /// [precision]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#precision #[derive(Debug, Clone, PartialEq)] pub struct PrecisionConstraint { - precision_range: Range, + precision_range: U64Range, } impl PrecisionConstraint { - pub fn new(precision_range: Range) -> Self { + pub fn new(precision_range: U64Range) -> Self { Self { precision_range } } - pub fn precision(&self) -> &Range { + pub fn precision(&self) -> &U64Range { &self.precision_range } } @@ -1813,10 +1798,10 @@ impl ConstraintValidator for PrecisionConstraint { .precision(); // get isl decimal precision as a range - let precision_range: &Range = self.precision(); + let precision_range: &U64Range = self.precision(); // return a Violation if the value didn't follow precision constraint - if !precision_range.contains(&(value_precision as i64).into()) { + if !precision_range.contains(&value_precision) { return Err(Violation::new( "precision", ViolationCode::InvalidLength, @@ -1833,15 +1818,15 @@ impl ConstraintValidator for PrecisionConstraint { /// [scale]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#scale #[derive(Debug, Clone, PartialEq)] pub struct ScaleConstraint { - scale_range: Range, + scale_range: I64Range, } impl ScaleConstraint { - pub fn new(scale_range: Range) -> Self { + pub fn new(scale_range: I64Range) -> Self { Self { scale_range } } - pub fn scale(&self) -> &Range { + pub fn scale(&self) -> &I64Range { &self.scale_range } } @@ -1861,10 +1846,10 @@ impl ConstraintValidator for ScaleConstraint { .scale(); // get isl decimal scale as a range - let scale_range: &Range = self.scale(); + let scale_range: &I64Range = self.scale(); // return a Violation if the value didn't follow scale constraint - if !scale_range.contains(&(value_scale).into()) { + if !scale_range.contains(&value_scale) { return Err(Violation::new( "scale", ViolationCode::InvalidLength, @@ -1881,15 +1866,15 @@ impl ConstraintValidator for ScaleConstraint { /// [exponent]: https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#exponent #[derive(Debug, Clone, PartialEq)] pub struct ExponentConstraint { - exponent_range: Range, + exponent_range: I64Range, } impl ExponentConstraint { - pub fn new(exponent_range: Range) -> Self { + pub fn new(exponent_range: I64Range) -> Self { Self { exponent_range } } - pub fn exponent(&self) -> &Range { + pub fn exponent(&self) -> &I64Range { &self.exponent_range } } @@ -1910,10 +1895,10 @@ impl ConstraintValidator for ExponentConstraint { .neg(); // get isl decimal exponent as a range - let exponent_range: &Range = self.exponent(); + let exponent_range: &I64Range = self.exponent(); // return a Violation if the value didn't follow exponent constraint - if !exponent_range.contains(&(value_exponent).into()) { + if !exponent_range.contains(&value_exponent) { return Err(Violation::new( "exponent", ViolationCode::InvalidLength, @@ -1930,17 +1915,17 @@ impl ConstraintValidator for ExponentConstraint { /// [timestamp_precision]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#timestamp_precision #[derive(Debug, Clone, PartialEq)] pub struct TimestampPrecisionConstraint { - timestamp_precision_range: Range, + timestamp_precision_range: TimestampPrecisionRange, } impl TimestampPrecisionConstraint { - pub fn new(scale_range: Range) -> Self { + pub fn new(scale_range: TimestampPrecisionRange) -> Self { Self { timestamp_precision_range: scale_range, } } - pub fn timestamp_precision(&self) -> &Range { + pub fn timestamp_precision(&self) -> &TimestampPrecisionRange { &self.timestamp_precision_range } } @@ -1959,18 +1944,14 @@ impl ConstraintValidator for TimestampPrecisionConstraint { .unwrap(); // get isl timestamp precision as a range - let precision_range: &Range = self.timestamp_precision(); - + let precision_range: &TimestampPrecisionRange = self.timestamp_precision(); + let precision = &TimestampPrecision::from_timestamp(timestamp_value); // return a Violation if the value didn't follow timestamp precision constraint - if !precision_range.contains(&(timestamp_value.to_owned()).into()) { + if !precision_range.contains(precision) { return Err(Violation::new( "precision", ViolationCode::InvalidLength, - format!( - "expected precision {} found {:?}", - precision_range, - timestamp_value.precision() - ), + format!("expected precision {precision_range} found {precision:?}"), ion_path, )); } @@ -1987,20 +1968,8 @@ pub struct ValidValuesConstraint { } impl ValidValuesConstraint { - /// Provides a way to programmatically construct valid_values constraint - /// Returns IonSchemaError whenever annotations are provided within ValidValue::Element - /// only `range` annotations are accepted for ValidValue::Element pub fn new(valid_values: Vec, isl_version: IslVersion) -> IonSchemaResult { - let valid_values: IonSchemaResult> = valid_values - .iter() - .map(|v| match v { - ValidValue::Range(r) => Ok(v.to_owned()), - ValidValue::Element(e) => ValidValue::from_ion_element(e, isl_version), - }) - .collect(); - Ok(Self { - valid_values: valid_values?, - }) + Ok(Self { valid_values }) } } @@ -2026,38 +1995,35 @@ impl ConstraintValidator for ValidValuesConstraint { ion_path: &mut IonPath, ) -> ValidationResult { match value { - IonSchemaElement::SingleElement(value) => { + IonSchemaElement::SingleElement(element) => { for valid_value in &self.valid_values { - match valid_value { - ValidValue::Range(range) => match value.ion_type() { - IonType::Int - | IonType::Float - | IonType::Decimal - | IonType::Timestamp => { - if range.contains(value) { - return Ok(()); - } - } - _ => {} - }, - ValidValue::Element(element) => { - // get value without annotations - let value: IonData<_> = value.value().into(); - let actual_value: IonData<_> = element.value().into(); - + let does_match = match valid_value { + ValidValue::Element(valid_value) => { // this comparison uses the Ion equivalence based on Ion specification - if actual_value == value { - return Ok(()); + IonData::eq(valid_value, element.value()) + } + ValidValue::NumberRange(range) => match element.any_number_as_decimal() { + Some(d) => range.contains(&d), + _ => false, + }, + ValidValue::TimestampRange(range) => { + if let Value::Timestamp(t) = element.value() { + range.contains(t) + } else { + false } } }; + if does_match { + return Ok(()); + } } Err(Violation::new( "valid_values", ViolationCode::InvalidValue, format!( "expected valid_values to be from {}, found {}", - &self, value + &self, element ), ion_path, )) @@ -2321,15 +2287,15 @@ impl PartialEq for RegexConstraint { /// [utf8_byte_length]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#utf8_byte_length #[derive(Debug, Clone, PartialEq)] pub struct Utf8ByteLengthConstraint { - length_range: Range, + length_range: UsizeRange, } impl Utf8ByteLengthConstraint { - pub fn new(length_range: Range) -> Self { + pub fn new(length_range: UsizeRange) -> Self { Self { length_range } } - pub fn length(&self) -> &Range { + pub fn length(&self) -> &UsizeRange { &self.length_range } } @@ -2353,10 +2319,10 @@ impl ConstraintValidator for Utf8ByteLengthConstraint { .len(); // get isl length as a range - let length_range: &Range = self.length(); + let length_range: &UsizeRange = self.length(); // return a Violation if the string/symbol size didn't follow utf8_byte_length constraint - if !length_range.contains(&(size as i64).into()) { + if !length_range.contains(&size) { return Err(Violation::new( "utf8_byte_length", ViolationCode::InvalidLength, diff --git a/ion-schema/src/ion_extension.rs b/ion-schema/src/ion_extension.rs new file mode 100644 index 0000000..5c5b1a5 --- /dev/null +++ b/ion-schema/src/ion_extension.rs @@ -0,0 +1,44 @@ +use ion_rs::element::{Element, Value}; +use ion_rs::external::bigdecimal::BigDecimal; +use ion_rs::{Decimal, Int}; +use num_traits::ToPrimitive; + +/// Trait for adding extensions to [`Element`] that are useful for implementing Ion Schema. +pub(crate) trait ElementExtensions { + /// Returns some `usize` if this `Element` is an Ion Int _and_ it can be represented as (fits in) a `usize`. + /// Returns `None` if `self` is not an Ion Int, or self is null.int, or self is out of bounds for `usize`. + fn as_usize(&self) -> Option; + /// Returns some `u64` if this `Element` is an Ion Int _and_ it can be represented as (fits in) a `u64`. + /// Returns `None` if `self` is not an Ion Int, or self is null.int, or self is out of bounds for `u64`. + fn as_u64(&self) -> Option; + /// Returns some [`Decimal`] if this `Element` is any Ion number type (`int`, `decimal`, or `float`) + /// _and_ it can be represented as (fits in) a `Decimal`. Returns `None` if `self` is not one + /// of the Ion number types or not a finite value. + fn any_number_as_decimal(&self) -> Option; +} +impl ElementExtensions for Element { + fn as_usize(&self) -> Option { + match self.value() { + Value::Int(Int::I64(i)) => i.to_usize(), + Value::Int(Int::BigInt(i)) => i.to_usize(), + _ => None, + } + } + fn as_u64(&self) -> Option { + match self.value() { + Value::Int(Int::I64(i)) => i.to_u64(), + Value::Int(Int::BigInt(i)) => i.to_u64(), + _ => None, + } + } + fn any_number_as_decimal(&self) -> Option { + match self.value() { + // TODO: Consolidate Int match arms once https://github.com/amazon-ion/ion-rust/issues/582 is resolved + Value::Int(Int::I64(i)) => Some(Decimal::from(*i)), + Value::Int(Int::BigInt(i)) => Some(Decimal::from(BigDecimal::from(i.clone()))), + Value::Float(f) => (*f).try_into().ok(), + Value::Decimal(d) => Some(d.clone()), + _ => None, + } + } +} diff --git a/ion-schema/src/isl/isl_constraint.rs b/ion-schema/src/isl/isl_constraint.rs index 4937a29..6eeefd1 100644 --- a/ion-schema/src/isl/isl_constraint.rs +++ b/ion-schema/src/isl/isl_constraint.rs @@ -1,14 +1,18 @@ -use crate::isl; +use crate::ion_extension::ElementExtensions; use crate::isl::isl_import::IslImportType; -use crate::isl::isl_range::{Range, RangeType}; use crate::isl::isl_type_reference::{IslTypeRefImpl, IslVariablyOccurringTypeRef}; -use crate::isl::util::{Annotation, Ieee754InterchangeFormat, TimestampOffset, ValidValue}; +use crate::isl::ranges::{I64Range, TimestampPrecisionRange, U64Range, UsizeRange}; +use crate::isl::util::{ + Annotation, Ieee754InterchangeFormat, TimestampOffset, TimestampPrecision, ValidValue, +}; use crate::isl::IslVersion; use crate::isl::WriteToIsl; use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult}; +use crate::{isl, isl_require}; use ion_rs::element::writer::ElementWriter; -use ion_rs::element::Element; -use ion_rs::{IonType, IonWriter}; +use ion_rs::element::{Element, Value}; +use ion_rs::types::IntAccess; +use ion_rs::{IonType, IonWriter, Symbol}; use std::collections::HashMap; use std::convert::TryInto; @@ -18,9 +22,9 @@ pub mod v_1_0 { IslAnnotationsConstraint, IslConstraint, IslConstraintImpl, IslRegexConstraint, IslSimpleAnnotationsConstraint, IslTimestampOffsetConstraint, IslValidValuesConstraint, }; - use crate::isl::isl_range::{IntegerRange, NonNegativeIntegerRange, Range, RangeImpl}; use crate::isl::isl_type_reference::{IslTypeRef, IslVariablyOccurringTypeRef}; - use crate::isl::util::{Annotation, TimestampOffset, TimestampPrecision, ValidValue}; + use crate::isl::ranges::{I64Range, TimestampPrecisionRange, U64Range, UsizeRange}; + use crate::isl::util::{Annotation, TimestampOffset, ValidValue}; use crate::isl::IslVersion; use crate::result::IonSchemaResult; use ion_rs::element::Element; @@ -87,19 +91,13 @@ pub mod v_1_0 { } /// Creates a `precision` constraint using the range specified in it - pub fn precision(precision: NonNegativeIntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::Precision(Range::NonNegativeInteger(precision)), - ) + pub fn precision(precision: U64Range) -> IslConstraint { + IslConstraint::new(IslVersion::V1_0, IslConstraintImpl::Precision(precision)) } /// Creates a `scale` constraint using the range specified in it - pub fn scale(scale: IntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::Scale(Range::Integer(scale)), - ) + pub fn scale(scale: I64Range) -> IslConstraint { + IslConstraint::new(IslVersion::V1_0, IslConstraintImpl::Scale(scale)) } /// Creates a `fields` constraint using the field names and [IslVariablyOccurringTypeRef]s referenced inside it @@ -127,34 +125,25 @@ pub mod v_1_0 { } /// Creates a `container_length` constraint using the range specified in it - pub fn container_length(length: NonNegativeIntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::ContainerLength(Range::NonNegativeInteger(length)), - ) + pub fn container_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V1_0, IslConstraintImpl::ContainerLength(length)) } /// Creates a `byte_length` constraint using the range specified in it - pub fn byte_length(length: RangeImpl) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::ByteLength(Range::NonNegativeInteger(length)), - ) + pub fn byte_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V1_0, IslConstraintImpl::ByteLength(length)) } /// Creates a `codepoint_length` constraint using the range specified in it - pub fn codepoint_length(length: RangeImpl) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::CodepointLength(Range::NonNegativeInteger(length)), - ) + pub fn codepoint_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V1_0, IslConstraintImpl::CodepointLength(length)) } /// Creates a `timestamp_precision` constraint using the range specified in it - pub fn timestamp_precision(precision: RangeImpl) -> IslConstraint { + pub fn timestamp_precision(precision: TimestampPrecisionRange) -> IslConstraint { IslConstraint::new( IslVersion::V1_0, - IslConstraintImpl::TimestampPrecision(Range::TimestampPrecision(precision)), + IslConstraintImpl::TimestampPrecision(precision), ) } @@ -167,11 +156,8 @@ pub mod v_1_0 { } /// Creates an `utf_byte_length` constraint using the range specified in it - pub fn utf8_byte_length(length: NonNegativeIntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::Utf8ByteLength(Range::NonNegativeInteger(length)), - ) + pub fn utf8_byte_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V1_0, IslConstraintImpl::Utf8ByteLength(length)) } /// Creates an `element` constraint using the [IslTypeRef] referenced inside it @@ -215,29 +201,13 @@ pub mod v_1_0 { } /// Creates a `valid_values` constraint using the [Element]s specified inside it - pub fn valid_values_with_values(values: Vec) -> IonSchemaResult { - let valid_values: IonSchemaResult> = values - .iter() - .map(|e| ValidValue::from_ion_element(e, IslVersion::V1_0)) - .collect(); + pub fn valid_values(valid_values: Vec) -> IonSchemaResult { Ok(IslConstraint::new( IslVersion::V1_0, - IslConstraintImpl::ValidValues(IslValidValuesConstraint { - valid_values: valid_values?, - }), + IslConstraintImpl::ValidValues(IslValidValuesConstraint { valid_values }), )) } - /// Creates a `valid_values` constraint using the [Range] specified inside it - pub fn valid_values_with_range(range: Range) -> IslConstraint { - IslConstraint::new( - IslVersion::V1_0, - IslConstraintImpl::ValidValues(IslValidValuesConstraint { - valid_values: vec![ValidValue::Range(range)], - }), - ) - } - /// Creates a `regex` constraint using the expression and flags (case_insensitive, multi_line) pub fn regex(case_insensitive: bool, multi_line: bool, expression: String) -> IslConstraint { IslConstraint::new( @@ -259,15 +229,12 @@ pub mod v_2_0 { use crate::isl::isl_constraint::{ IslConstraintImpl, IslTimestampOffsetConstraint, IslValidValuesConstraint, }; - use crate::isl::isl_range::{NonNegativeIntegerRange, Range, RangeImpl}; use crate::isl::isl_type_reference::{IslTypeRef, IslVariablyOccurringTypeRef}; - use crate::isl::util::{ - Annotation, Ieee754InterchangeFormat, TimestampOffset, TimestampPrecision, ValidValue, - }; + use crate::isl::ranges::{I64Range, TimestampPrecisionRange, U64Range, UsizeRange}; + use crate::isl::util::{Annotation, Ieee754InterchangeFormat, TimestampOffset, ValidValue}; use crate::isl::IslVersion; use crate::result::{invalid_schema_error, IonSchemaResult}; use ion_rs::element::Element; - use ion_rs::Int; /// Creates a `type` constraint using the [IslTypeRef] referenced inside it // type is rust keyword hence this method is named type_constraint unlike other ISL constraint methods @@ -331,19 +298,13 @@ pub mod v_2_0 { } /// Creates a `precision` constraint using the range specified in it - pub fn precision(precision: NonNegativeIntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::Precision(Range::NonNegativeInteger(precision)), - ) + pub fn precision(precision: U64Range) -> IslConstraint { + IslConstraint::new(IslVersion::V2_0, IslConstraintImpl::Precision(precision)) } /// Creates an `exponent` constraint from a [Range] specifying an exponent range. - pub fn exponent(exponent: RangeImpl) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::Exponent(Range::Integer(exponent)), - ) + pub fn exponent(exponent: I64Range) -> IslConstraint { + IslConstraint::new(IslVersion::V2_0, IslConstraintImpl::Exponent(exponent)) } /// Creates a `fields` constraint using the field names and [IslVariablyOccurringTypeRef]s referenced inside it @@ -379,34 +340,25 @@ pub mod v_2_0 { } /// Creates a `container_length` constraint using the range specified in it - pub fn container_length(length: NonNegativeIntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::ContainerLength(Range::NonNegativeInteger(length)), - ) + pub fn container_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V2_0, IslConstraintImpl::ContainerLength(length)) } /// Creates a `byte_length` constraint using the range specified in it - pub fn byte_length(length: RangeImpl) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::ByteLength(Range::NonNegativeInteger(length)), - ) + pub fn byte_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V2_0, IslConstraintImpl::ByteLength(length)) } /// Creates a `codepoint_length` constraint using the range specified in it - pub fn codepoint_length(length: RangeImpl) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::CodepointLength(Range::NonNegativeInteger(length)), - ) + pub fn codepoint_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V2_0, IslConstraintImpl::CodepointLength(length)) } /// Creates a `timestamp_precision` constraint using the range specified in it - pub fn timestamp_precision(precision: RangeImpl) -> IslConstraint { + pub fn timestamp_precision(precision: TimestampPrecisionRange) -> IslConstraint { IslConstraint::new( IslVersion::V2_0, - IslConstraintImpl::TimestampPrecision(Range::TimestampPrecision(precision)), + IslConstraintImpl::TimestampPrecision(precision), ) } @@ -419,11 +371,8 @@ pub mod v_2_0 { } /// Creates a `utf8_byte_length` constraint using the range specified in it - pub fn utf8_byte_length(length: NonNegativeIntegerRange) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::Utf8ByteLength(Range::NonNegativeInteger(length)), - ) + pub fn utf8_byte_length(length: UsizeRange) -> IslConstraint { + IslConstraint::new(IslVersion::V2_0, IslConstraintImpl::Utf8ByteLength(length)) } /// Creates an `element` constraint using the [IslTypeRef] referenced inside it and considers whether distinct elements are required or not @@ -476,30 +425,14 @@ pub mod v_2_0 { ) } - /// Creates a `valid_values` constraint using the [Element]s specified inside it - pub fn valid_values_with_values(values: Vec) -> IonSchemaResult { - let valid_values: IonSchemaResult> = values - .iter() - .map(|e| ValidValue::from_ion_element(e, IslVersion::V2_0)) - .collect(); + /// Creates a `valid_values` constraint using the [`ValidValue`]s specified inside it + pub fn valid_values(valid_values: Vec) -> IonSchemaResult { Ok(IslConstraint::new( IslVersion::V2_0, - IslConstraintImpl::ValidValues(IslValidValuesConstraint { - valid_values: valid_values?, - }), + IslConstraintImpl::ValidValues(IslValidValuesConstraint { valid_values }), )) } - /// Creates a `valid_values` constraint using the [Range] specified inside it - pub fn valid_values_with_range(range: Range) -> IslConstraint { - IslConstraint::new( - IslVersion::V2_0, - IslConstraintImpl::ValidValues(IslValidValuesConstraint { - valid_values: vec![ValidValue::Range(range)], - }), - ) - } - /// Creates a `regex` constraint using the expression and flags (case_insensitive, multi_line) pub fn regex(case_insensitive: bool, multi_line: bool, expression: String) -> IslConstraint { todo!() @@ -535,16 +468,16 @@ pub(crate) enum IslConstraintImpl { AllOf(Vec), Annotations(IslAnnotationsConstraint), AnyOf(Vec), - ByteLength(Range), - CodepointLength(Range), + ByteLength(UsizeRange), + CodepointLength(UsizeRange), Contains(Vec), ContentClosed, - ContainerLength(Range), + ContainerLength(UsizeRange), // Represents Element(type_reference, expected_distinct). // For ISL 2.0 true/false is specified based on whether `distinct` annotation is present or not. // For ISL 1.0 which doesn't support `distinct` elements this will be (type_reference, false). Element(IslTypeRefImpl, bool), - Exponent(Range), + Exponent(I64Range), // Represents Fields(fields, content_closed) // For ISL 2.0 true/false is specified based on whether `closed::` annotation is present or not // For ISL 1.0 this will always be (fields, false) as it doesn't support `closed::` annotation on fields constraint @@ -557,14 +490,14 @@ pub(crate) enum IslConstraintImpl { Not(IslTypeRefImpl), OneOf(Vec), OrderedElements(Vec), - Precision(Range), + Precision(U64Range), Regex(IslRegexConstraint), - Scale(Range), + Scale(I64Range), TimestampOffset(IslTimestampOffsetConstraint), - TimestampPrecision(Range), + TimestampPrecision(TimestampPrecisionRange), Type(IslTypeRefImpl), Unknown(String, Element), // Unknown constraint is used to store open contents - Utf8ByteLength(Range), + Utf8ByteLength(UsizeRange), ValidValues(IslValidValuesConstraint), } @@ -629,16 +562,13 @@ impl IslConstraintImpl { )?; Ok(IslConstraintImpl::AnyOf(types)) } - "byte_length" => Ok(IslConstraintImpl::ByteLength(Range::from_ion_element( - value, - RangeType::NonNegativeInteger, - isl_version, - )?)), - "codepoint_length" => Ok(IslConstraintImpl::CodepointLength(Range::from_ion_element( + "byte_length" => Ok(IslConstraintImpl::ByteLength(UsizeRange::from_ion_element( value, - RangeType::NonNegativeInteger, - isl_version, + Element::as_usize, )?)), + "codepoint_length" => Ok(IslConstraintImpl::CodepointLength( + UsizeRange::from_ion_element(value, Element::as_usize)?, + )), "contains" => { if value.is_null() { return invalid_schema_error( @@ -686,11 +616,9 @@ impl IslConstraintImpl { Ok(IslConstraintImpl::ContentClosed) } - "container_length" => Ok(IslConstraintImpl::ContainerLength(Range::from_ion_element( - value, - RangeType::NonNegativeInteger, - isl_version, - )?)), + "container_length" => Ok(IslConstraintImpl::ContainerLength( + UsizeRange::from_ion_element(value, Element::as_usize)?, + )), "element" => { let type_reference: IslTypeRefImpl = IslTypeRefImpl::from_ion_element(isl_version, value, inline_imported_types)?; @@ -844,10 +772,9 @@ impl IslConstraintImpl { .collect::>>()?; Ok(IslConstraintImpl::OrderedElements(types)) } - "precision" => Ok(IslConstraintImpl::Precision(Range::from_ion_element( + "precision" => Ok(IslConstraintImpl::Precision(U64Range::from_ion_element( value, - RangeType::Precision, - isl_version, + Element::as_u64, )?)), "regex" => { let case_insensitive = value.annotations().contains("i"); @@ -883,10 +810,9 @@ impl IslConstraintImpl { ))) } "scale" => match isl_version { - IslVersion::V1_0 => Ok(IslConstraintImpl::Scale(Range::from_ion_element( + IslVersion::V1_0 => Ok(IslConstraintImpl::Scale(I64Range::from_ion_element( value, - RangeType::Any, - isl_version, + Element::as_i64, )?)), IslVersion::V2_0 => { // for ISL 2.0 scale constraint does not exist hence `scale` will be considered as open content @@ -897,7 +823,10 @@ impl IslConstraintImpl { } }, "timestamp_precision" => Ok(IslConstraintImpl::TimestampPrecision( - Range::from_ion_element(value, RangeType::TimestampPrecision, isl_version)?, + TimestampPrecisionRange::from_ion_element(value, |e| { + let symbol_text = e.as_symbol().and_then(Symbol::text)?; + TimestampPrecision::try_from(symbol_text).ok() + })?, )), "exponent" => match isl_version { IslVersion::V1_0 => { @@ -907,10 +836,9 @@ impl IslConstraintImpl { value.to_owned(), )) } - IslVersion::V2_0 => Ok(IslConstraintImpl::Exponent(Range::from_ion_element( + IslVersion::V2_0 => Ok(IslConstraintImpl::Exponent(I64Range::from_ion_element( value, - RangeType::Any, - isl_version, + Element::as_i64, )?)), }, "timestamp_offset" => { @@ -938,14 +866,14 @@ impl IslConstraintImpl { .map(|e| { if e.is_null() { return invalid_schema_error( - "`timestamp_offset` values must be non-null strings, found null" - ); + "`timestamp_offset` values must be non-null strings, found null", + ); } if e.ion_type() != IonType::String { return invalid_schema_error(format!( - "`timestamp_offset` values must be non-null strings, found {e}" - )); + "`timestamp_offset` values must be non-null strings, found {e}" + )); } if !e.annotations().is_empty() { @@ -973,11 +901,9 @@ impl IslConstraintImpl { IslTimestampOffsetConstraint::new(valid_offsets), )) } - "utf8_byte_length" => Ok(IslConstraintImpl::Utf8ByteLength(Range::from_ion_element( - value, - RangeType::NonNegativeInteger, - isl_version, - )?)), + "utf8_byte_length" => Ok(IslConstraintImpl::Utf8ByteLength( + UsizeRange::from_ion_element(value, Element::as_usize)?, + )), "valid_values" => Ok(IslConstraintImpl::ValidValues( IslValidValuesConstraint::from_ion_element(value, isl_version)?, )), @@ -1316,10 +1242,10 @@ impl IslSimpleAnnotationsConstraint { if self.is_closed { isl_constraints.push(isl::isl_constraint::v_2_0::element( isl::isl_type_reference::v_2_0::anonymous_type_ref(vec![ - isl::isl_constraint::v_2_0::valid_values_with_values( + isl::isl_constraint::v_2_0::valid_values( self.annotations .iter() - .map(|a| Element::symbol(a.value())) + .map(|a| ValidValue::Element(Value::Symbol(a.value().into()))) .collect(), )?, ]), @@ -1372,19 +1298,8 @@ pub struct IslValidValuesConstraint { impl IslValidValuesConstraint { /// Provides a way to programmatically construct valid_values constraint - /// Returns IonSchemaError whenever annotations are provided within ValidValue::Element - /// only `range` annotations are accepted for ValidValue::Element - pub fn new(valid_values: Vec, isl_version: IslVersion) -> IonSchemaResult { - let valid_values: IonSchemaResult> = valid_values - .iter() - .map(|v| match v { - ValidValue::Range(r) => Ok(v.to_owned()), - ValidValue::Element(e) => ValidValue::from_ion_element(e, isl_version), - }) - .collect(); - Ok(Self { - valid_values: valid_values?, - }) + pub fn new(valid_values: Vec) -> IonSchemaResult { + Ok(Self { valid_values }) } pub fn values(&self) -> &Vec { @@ -1392,34 +1307,19 @@ impl IslValidValuesConstraint { } pub fn from_ion_element(value: &Element, isl_version: IslVersion) -> IonSchemaResult { - if value.annotations().contains("range") { - return IslValidValuesConstraint::new( - vec![ValidValue::Range(Range::from_ion_element( - value, - RangeType::NumberOrTimestamp, - isl_version, - )?)], - isl_version, - ); - } - if let Some(values) = value.as_sequence() { - if value.ion_type() == IonType::List { - let mut valid_values = vec![]; - let values: IonSchemaResult> = values - .elements() - .map(|e| { - valid_values.push(ValidValue::from_ion_element(e, isl_version)?); - Ok(()) - }) - .collect(); - values?; - return Ok(IslValidValuesConstraint { valid_values }); - } - } - invalid_schema_error(format!( - "Expected valid_values to be a range or a list of valid values, found {}", - value.ion_type() - )) + let valid_values = if value.annotations().contains("range") { + vec![ValidValue::from_ion_element(value, isl_version)?] + } else { + isl_require!(value.ion_type() == IonType::List && !value.is_null() => "Expected a list of valid values; found: {value}")?; + let valid_values: Result, _> = value + .as_sequence() + .unwrap() + .elements() + .map(|e| ValidValue::from_ion_element(e, isl_version)) + .collect(); + valid_values? + }; + IslValidValuesConstraint::new(valid_values) } } diff --git a/ion-schema/src/isl/isl_range.rs b/ion-schema/src/isl/isl_range.rs deleted file mode 100644 index b19a14f..0000000 --- a/ion-schema/src/isl/isl_range.rs +++ /dev/null @@ -1,1094 +0,0 @@ -use crate::isl::isl_range::RangeBoundaryValue::*; -use crate::isl::util::TimestampPrecision; -use crate::isl::IslVersion; -use crate::isl::WriteToIsl; -use crate::result::{ - invalid_schema_error, invalid_schema_error_raw, IonSchemaError, IonSchemaResult, -}; -use ion_rs::element::writer::ElementWriter; -use ion_rs::element::Element; -use ion_rs::external::bigdecimal::num_bigint::BigInt; -use ion_rs::external::bigdecimal::{BigDecimal, One}; -use ion_rs::types::IntAccess; -use ion_rs::{element, Decimal, Int, IonType, IonWriter, Timestamp}; -use std::cmp::Ordering; -use std::fmt::{Display, Formatter}; -use std::prelude::rust_2021::TryInto; -use std::str::FromStr; - -/// Provides a type to be used to create integer ranges. -pub type IntegerRange = RangeImpl; - -/// Provides a type to be used to create non negative integer ranges. -pub type NonNegativeIntegerRange = RangeImpl; - -/// Provides a type to be used to create float ranges. -pub type FloatRange = RangeImpl; - -/// Provides a type to be used to create decimal ranges. -pub type DecimalRange = RangeImpl; - -/// Provides a type to be used to create timestamp ranges. -pub type TimestampRange = RangeImpl; - -/// Provides a type to be used to create timestamp precision ranges. -pub type TimestampPrecisionRange = RangeImpl; - -/// Provides a type to be used to create number ranges. -pub type NumberRange = RangeImpl; - -/// Represents ISL [Range]s where some constraints can be defined using a range -/// ```ion -/// > ::= range::[ , ] -/// | range::[ min, ] -/// | range::[ , max ] -/// Grammar: ::= -/// | -/// | -/// | -/// | -/// | -/// ``` -/// For more information on [Range]: -// this is a wrapper around the RangeImpl generic implementation of ranges -#[derive(Debug, Clone, PartialEq)] -pub enum Range { - Integer(RangeImpl), - NonNegativeInteger(RangeImpl), - TimestampPrecision(RangeImpl), - Timestamp(RangeImpl), - Decimal(RangeImpl), - Float(RangeImpl), - Number(RangeImpl), -} - -impl Range { - pub fn non_negative_range_boundaries(&self) -> Option<(usize, usize)> { - match self { - Range::NonNegativeInteger(RangeImpl { start, end }) => { - let start = match start { - RangeBoundaryValue::Max => usize::MAX, - RangeBoundaryValue::Min => usize::MIN, - RangeBoundaryValue::Value(val, range_boundary_type) => { - match range_boundary_type { - RangeBoundaryType::Inclusive => *val, - RangeBoundaryType::Exclusive => *val + 1, - } - } - }; - let end = match end { - RangeBoundaryValue::Max => usize::MAX, - RangeBoundaryValue::Min => usize::MIN, - RangeBoundaryValue::Value(val, range_boundary_type) => { - match range_boundary_type { - RangeBoundaryType::Inclusive => *val, - RangeBoundaryType::Exclusive => *val + 1, - } - } - }; - Some((start, end)) - } - _ => None, - } - } - - /// Provides a boolean value to specify whether the given value is within the range or not - pub fn contains(&self, value: &Element) -> bool { - if value.is_null() { - // if the provided Element is null, then return false - return false; - } - match self { - Range::Integer(int_range) if value.ion_type() == IonType::Int => { - int_range.contains(value.as_int().unwrap().to_owned()) - } - Range::NonNegativeInteger(int_non_neg_range) if value.ion_type() == IonType::Int => { - int_non_neg_range.contains(value.as_int().unwrap().as_i64().unwrap() as usize) - } - Range::TimestampPrecision(timestamp_precision_range) - if value.ion_type() == IonType::Timestamp => - { - let value = TimestampPrecision::from_timestamp(value.as_timestamp().unwrap()); - timestamp_precision_range.contains(value) - } - Range::Timestamp(timestamp_range) if value.ion_type() == IonType::Timestamp => { - timestamp_range.contains(value.as_timestamp().unwrap().to_owned()) - } - Range::Float(float_range) if value.ion_type() == IonType::Float => { - float_range.contains(value.as_float().unwrap()) - } - Range::Decimal(decimal_range) if value.ion_type() == IonType::Decimal => { - decimal_range.contains(value.as_decimal().unwrap().to_owned()) - } - Range::Number(number_range) - if value.ion_type() == IonType::Int - || value.ion_type() == IonType::Float - || value.ion_type() == IonType::Decimal => - { - let value: Number = match value.ion_type() { - IonType::Int => value.as_int().unwrap().into(), - IonType::Float => { - if let Ok(number_val) = value.as_float().unwrap().try_into() { - number_val - } else { - return false; - } - } - IonType::Decimal => { - if let Ok(number_val) = value.as_decimal().unwrap().try_into() { - number_val - } else { - return false; - } - } - _ => { - return false; - } - }; - number_range.contains(value) - } - _ => false, // if the provided Element of a different type than the given range type, contains returns false - } - } - - /// Provides optional non negative integer range - pub fn optional() -> Self { - Range::NonNegativeInteger( - RangeImpl::range( - RangeBoundaryValue::Value(0usize, RangeBoundaryType::Inclusive), - RangeBoundaryValue::Value(1usize, RangeBoundaryType::Inclusive), - ) - .unwrap(), - ) - } - - /// Provides required non negative integer range - pub fn required() -> Self { - Range::NonNegativeInteger( - RangeImpl::range( - RangeBoundaryValue::Value(1usize, RangeBoundaryType::Inclusive), - RangeBoundaryValue::Value(1usize, RangeBoundaryType::Inclusive), - ) - .unwrap(), - ) - } - - /// Parse an [Element] into a [Range] using the [RangeType] - // `range_type` is used to determine range type for integer non negative ranges or number ranges - pub fn from_ion_element( - value: &Element, - range_type: RangeType, - isl_version: IslVersion, - ) -> IonSchemaResult { - // if an integer value is passed here then convert it into a range - // eg. if `1` is passed as value then return a range [1,1] - return if let Some(integer_value) = value.as_int() { - match range_type { - RangeType::Precision | RangeType::NonNegativeInteger => { - let non_negative_integer_value = - Range::validate_non_negative_integer_range_boundary_value( - value.as_int().unwrap(), - &range_type, - )?; - Ok(Range::NonNegativeInteger(non_negative_integer_value.into())) - } - RangeType::TimestampPrecision => invalid_schema_error(format!( - "Timestamp precision ranges can not be constructed from value of type {}", - value.ion_type() - )), - RangeType::Any => Ok(Range::Integer(integer_value.to_owned().into())), - RangeType::NumberOrTimestamp => Ok(NumberRange::new( - RangeBoundaryValue::Value(integer_value.into(), RangeBoundaryType::Inclusive), - RangeBoundaryValue::Value(integer_value.into(), RangeBoundaryType::Inclusive), - )? - .into()), - } - } else if let Some(timestamp_precision_symbol) = value.as_symbol() { - let timestamp_precision = timestamp_precision_symbol.text().ok_or_else(|| { - invalid_schema_error_raw( - "Range can not be constructed from symbol with unknown text", - ) - })?; - if range_type == RangeType::TimestampPrecision { - Ok(TimestampPrecisionRange::new( - RangeBoundaryValue::Value( - timestamp_precision.try_into()?, - RangeBoundaryType::Inclusive, - ), - RangeBoundaryValue::Value( - timestamp_precision.try_into()?, - RangeBoundaryType::Inclusive, - ), - )? - .into()) - } else { - invalid_schema_error(format!( - "{:?} ranges can not be constructed from value of type {}", - range_type, - value.ion_type() - )) - } - } else if let element::Value::List(range) = value.value() { - // verify if the value has annotation range - if !value.annotations().contains("range") { - return invalid_schema_error( - "An element representing a range must have the annotation `range`.", - ); - } - - // verify that the range sequence has only two values i.e. start and end range boundary values - if range.len() != 2 { - return invalid_schema_error( - "Ranges must contain two values representing the minimum and maximum ends of range.", - ); - } - - let start = try_to!(range.get(0)); - let end = try_to!(range.get(1)); - - // this match statement determines that no range types other then the below range types are allowed - match start.ion_type() { - IonType::Symbol - | IonType::Int - | IonType::Float - | IonType::Decimal - | IonType::Timestamp => Ok(Self::validate_and_construct_range( - TypedRangeBoundaryValue::from_ion_element( - start, - range_type.to_owned(), - isl_version, - )?, - TypedRangeBoundaryValue::from_ion_element(end, range_type, isl_version)?, - )?), - _ => invalid_schema_error("Unsupported range type specified"), - } - } else { - invalid_schema_error(format!( - "Ranges can not be constructed for type {}", - value.ion_type() - )) - }; - } - - // helper method to which validates a non negative integer range boundary value - pub(crate) fn validate_non_negative_integer_range_boundary_value( - value: &Int, - range_type: &RangeType, - ) -> IonSchemaResult { - // minimum precision must be greater than or equal to 1 - // for more information: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#precision - let min_value = i64::from(range_type == &RangeType::Precision); - match value.as_i64() { - Some(v) => { - if v >= min_value { - match v.try_into() { - Err(_) => invalid_schema_error(format!( - "Expected non negative integer greater than {min_value} for range boundary values, found {v}" - )), - Ok(non_negative_int_value) => Ok(non_negative_int_value), - } - } else { - invalid_schema_error(format!( - "Expected non negative integer greater than {min_value} for range boundary values, found {v}" - )) - } - } - None => match value.as_big_int() { - None => { - unreachable!("Expected range boundary values must be a non negative integer") - } - Some(v) => { - if v >= &BigInt::from(min_value) { - match v.try_into() { - Err(_) => invalid_schema_error(format!( - "Expected non negative integer greater than {min_value} for range boundary values, found {v}" - )), - Ok(non_negative_int_value) => Ok(non_negative_int_value), - } - } else { - invalid_schema_error(format!( - "Expected non negative integer greater than {min_value} for range boundary values, found {v}" - )) - } - } - }, - } - } - - // helper method to validate range boundary values and construct a `Range` - pub(crate) fn validate_and_construct_range( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult { - // validate the range boundary values : `start` and `end` - match (&start, &end) { - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::Max) => { - invalid_schema_error("Range boundaries can not be min and max together (i.e. range::[min, max] is not allowed)") - } - (TypedRangeBoundaryValue::Max, _) => { - invalid_schema_error("Lower range boundary value must not be max") - } - (_, TypedRangeBoundaryValue::Min) => { - invalid_schema_error("Upper range boundary value must not be min") - } - (_, TypedRangeBoundaryValue::Integer(_)) | (TypedRangeBoundaryValue::Integer(_), _) => { - Ok(Range::Integer(RangeImpl::::int_range_from_typed_boundary_value(start, end)?)) - } - (_, TypedRangeBoundaryValue::NonNegativeInteger(_)) | - (TypedRangeBoundaryValue::NonNegativeInteger(_), _) => { - Ok(Range::NonNegativeInteger(RangeImpl::::int_non_negative_range_from_typed_boundary_value(start, end)?)) - } - (_, TypedRangeBoundaryValue::TimestampPrecision(_)) | - (TypedRangeBoundaryValue::TimestampPrecision(_), _) => { - Ok(Range::TimestampPrecision(RangeImpl::::timestamp_precision_range_from_typed_boundary_value(start, end)?)) - } - (_, TypedRangeBoundaryValue::Float(_)) | - (TypedRangeBoundaryValue::Float(_), _) => { - Ok(Range::Float(RangeImpl::::float_range_from_typed_boundary_value(start, end)?)) - } - (_, TypedRangeBoundaryValue::Decimal(_)) | - (TypedRangeBoundaryValue::Decimal(_), _) => { - Ok(Range::Decimal(RangeImpl::::decimal_range_from_typed_boundary_value(start, end)?)) - } - (_, TypedRangeBoundaryValue::Number(_)) | - (TypedRangeBoundaryValue::Number(_), _) => { - Ok(Range::Number(RangeImpl::::number_range_from_typed_boundary_value(start, end)?)) - } - (_, TypedRangeBoundaryValue::Timestamp(_)) | - (TypedRangeBoundaryValue::Timestamp(_), _) => { - Ok(Range::Timestamp(RangeImpl::::timestamp_range_from_typed_boundary_value(start, end)?)) - } - } - } -} - -impl From for Range { - fn from(value: IntegerRange) -> Self { - Range::Integer(value) - } -} - -impl From for Range { - fn from(value: NonNegativeIntegerRange) -> Self { - Range::NonNegativeInteger(value) - } -} - -impl From for Range { - fn from(value: FloatRange) -> Self { - Range::Float(value) - } -} - -impl From for Range { - fn from(value: DecimalRange) -> Self { - Range::Decimal(value) - } -} - -impl From for Range { - fn from(value: TimestampRange) -> Self { - Range::Timestamp(value) - } -} - -impl From for Range { - fn from(value: TimestampPrecisionRange) -> Self { - Range::TimestampPrecision(value) - } -} - -impl From for Range { - fn from(value: NumberRange) -> Self { - Range::Number(value) - } -} - -impl Display for Range { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match &self { - Range::Integer(integer) => write!(f, "{integer}"), - Range::NonNegativeInteger(non_negative_integer) => { - write!(f, "{non_negative_integer}") - } - Range::TimestampPrecision(timestamp_precision) => write!(f, "{timestamp_precision}"), - Range::Timestamp(timestamp) => write!(f, "{timestamp}"), - Range::Decimal(decimal) => write!(f, "{decimal}"), - Range::Float(float) => write!(f, "{float}"), - Range::Number(number) => write!(f, "{number}"), - } - } -} - -impl WriteToIsl for Range { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_annotations(["range"]); - match &self { - Range::Integer(integer) => integer.write_to(writer)?, - Range::NonNegativeInteger(non_negative_integer) => { - non_negative_integer.write_to(writer)? - } - Range::TimestampPrecision(timestamp_precision) => { - timestamp_precision.write_to(writer)? - } - Range::Timestamp(timestamp) => timestamp.write_to(writer)?, - Range::Decimal(decimal) => decimal.write_to(writer)?, - Range::Float(float) => float.write_to(writer)?, - Range::Number(number) => number.write_to(writer)?, - } - Ok(()) - } -} - -/// Represents a generic range where some constraints can be defined using this range -// this is a generic implementation of ranges -#[derive(Debug, Clone, PartialEq)] -pub struct RangeImpl { - start: RangeBoundaryValue, - end: RangeBoundaryValue, -} - -impl RangeImpl { - /// Provides a way to generate generic range using the start and end values - pub fn range( - start: RangeBoundaryValue, - end: RangeBoundaryValue, - ) -> IonSchemaResult { - if start == end - && (start.range_boundary_type() == &RangeBoundaryType::Exclusive - || end.range_boundary_type() == &RangeBoundaryType::Exclusive) - { - return invalid_schema_error("Empty ranges are not allowed"); - } - if start > end { - return invalid_schema_error( - "Lower range boundary value can not be bigger than upper range boundary", - ); - } - Ok(RangeImpl { start, end }) - } - - /// Provides a boolean value to specify whether the given value is within the range or not - pub fn contains(&self, value: T) -> bool { - let is_in_lower_bound = match &self.start { - Min => true, - Value(start_value, boundary_type) => match boundary_type { - RangeBoundaryType::Inclusive => start_value <= &value, - RangeBoundaryType::Exclusive => start_value < &value, - }, - Max => unreachable!("Cannot have 'Max' as the lower range boundary"), - }; - - let is_in_upper_bound = match &self.end { - Max => true, - Min => unreachable!("Cannot have 'Min' as the upper range boundary"), - Value(end_value, boundary_type) => match boundary_type { - RangeBoundaryType::Inclusive => end_value >= &value, - RangeBoundaryType::Exclusive => end_value > &value, - }, - }; - is_in_upper_bound && is_in_lower_bound - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::Integer - pub(crate) fn int_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - ( - TypedRangeBoundaryValue::Integer(RangeBoundaryValue::Value(v1, v1_type)), - TypedRangeBoundaryValue::Integer(RangeBoundaryValue::Value(v2, v2_type)), - ) => { - // Safe unwrap since as_big_int returns None for i64 value - let v1_as_big_int = v1 - .as_big_int() - .map(|v| v.to_owned()) - .unwrap_or(BigInt::from(v1.as_i64().unwrap())); - - let v2_as_big_int = v2 - .as_big_int() - .map(|v| v.to_owned()) - .unwrap_or(BigInt::from(v2.as_i64().unwrap())); - - // verify this is not an empty range for which there is no valid integer values - if (v2_as_big_int - v1_as_big_int).is_one() - && v1_type == RangeBoundaryType::Exclusive - && v2_type == RangeBoundaryType::Exclusive - { - return invalid_schema_error("No valid values in the Integer range"); - } - RangeImpl::range( - RangeBoundaryValue::Value(v1, v1_type), - RangeBoundaryValue::Value(v2, v2_type), - ) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::Integer(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::Integer(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::Integer(Value(v1, _)), _) - | (_, TypedRangeBoundaryValue::Integer(Value(v1, _))) => { - invalid_schema_error("Range boundaries must have the same types") - } - _ => unreachable!( - "Integer ranges can not be constructed with non integer range boundary types" - ), - } - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::NonNegativeInteger - pub(crate) fn int_non_negative_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - (TypedRangeBoundaryValue::NonNegativeInteger(Value(v1, v1_type)), TypedRangeBoundaryValue::NonNegativeInteger(Value(v2, v2_type))) => { - // verify this is not an empty range (i.e. one for which there are no valid non-negative integer values) - if v2 > v1 && v2 - v1 == 1 - && v1_type == RangeBoundaryType::Exclusive - && v2_type == RangeBoundaryType::Exclusive - { - return invalid_schema_error("No valid values in the Integer range"); - } - RangeImpl::range( - Value(v1, v1_type), - Value(v2, v2_type), - ) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::NonNegativeInteger(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::NonNegativeInteger(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::NonNegativeInteger(Value(v1, _)), _) | (_, TypedRangeBoundaryValue::NonNegativeInteger(Value(v1, _)))=> { - invalid_schema_error("Range boundaries should have same types") - } - _ => unreachable!( - "NonNegativeInteger ranges can not be constructed with non integer non negative range boundary types" - ), - } - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::Number - pub(crate) fn number_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - (TypedRangeBoundaryValue::Number(v1), TypedRangeBoundaryValue::Number(v2)) => { - RangeImpl::range(v1, v2) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::Number(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::Number(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::Number(Value(v1, _)), _) - | (_, TypedRangeBoundaryValue::Number(Value(v1, _))) => { - invalid_schema_error("Range boundaries should have same types") - } - _ => unreachable!( - "Number ranges can not be constructed with non number range boundary types" - ), - } - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::TimestampPrecision - pub(crate) fn timestamp_precision_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - (TypedRangeBoundaryValue::TimestampPrecision(v1), TypedRangeBoundaryValue::TimestampPrecision(v2)) => { - RangeImpl::range(v1, v2) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::TimestampPrecision(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::TimestampPrecision(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::TimestampPrecision(Value(v1, _)), _) | (_, TypedRangeBoundaryValue::TimestampPrecision(Value(v1, _)))=> { - invalid_schema_error("Range boundaries should have same types") - } - _ => unreachable!( - "TimestampPrecision ranges can not be constructed with non timestamp precision range boundary types" - ), - } - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::Decimal - pub(crate) fn decimal_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - (TypedRangeBoundaryValue::Decimal(v1), TypedRangeBoundaryValue::Decimal(v2)) => { - RangeImpl::range(v1, v2) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::Decimal(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::Decimal(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::Decimal(Value(v1, _)), _) - | (_, TypedRangeBoundaryValue::Decimal(Value(v1, _))) => { - invalid_schema_error("Range boundaries should have same types") - } - _ => unreachable!( - "Decimal ranges can not be constructed with non decimal range boundary types" - ), - } - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::Float - pub(crate) fn float_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - (TypedRangeBoundaryValue::Float(v1), TypedRangeBoundaryValue::Float(v2)) => { - RangeImpl::range(v1, v2) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::Float(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::Float(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::Float(Value(v1, _)), _) - | (_, TypedRangeBoundaryValue::Float(Value(v1, _))) => { - invalid_schema_error("Range boundaries should have same types") - } - _ => unreachable!( - "Float ranges can not be constructed with non float range boundary types" - ), - } - } - - /// Provides `RangeImpl` for given `TypedRangeBoundaryValue` - // this method requires a prior check for TypedRangeBoundaryValue::Timestamp - pub(crate) fn timestamp_range_from_typed_boundary_value( - start: TypedRangeBoundaryValue, - end: TypedRangeBoundaryValue, - ) -> IonSchemaResult> { - match (start, end) { - (TypedRangeBoundaryValue::Timestamp(v1), TypedRangeBoundaryValue::Timestamp(v2)) => { - RangeImpl::range(v1, v2) - } - (TypedRangeBoundaryValue::Min, TypedRangeBoundaryValue::Timestamp(v2)) => { - RangeImpl::range(RangeBoundaryValue::Min, v2) - } - (TypedRangeBoundaryValue::Timestamp(v1), TypedRangeBoundaryValue::Max) => { - RangeImpl::range(v1, RangeBoundaryValue::Max) - } - (TypedRangeBoundaryValue::Timestamp(Value(v1, _)), _) - | (_, TypedRangeBoundaryValue::Timestamp(Value(v1, _))) => { - invalid_schema_error("Range boundaries should have same types") - } - _ => unreachable!( - "Timestamp ranges can not be constructed with non timestamp range boundary types" - ), - } - } -} - -/// Provides `Range` for given `usize` -impl From for RangeImpl { - fn from(non_negative_int_value: usize) -> Self { - RangeImpl::range( - RangeBoundaryValue::Value(non_negative_int_value, RangeBoundaryType::Inclusive), - RangeBoundaryValue::Value(non_negative_int_value, RangeBoundaryType::Inclusive), - ) - .unwrap() - } -} - -/// Provides `Range` for given `Integer` -impl From for RangeImpl { - fn from(int_value: Int) -> Self { - RangeImpl::range( - RangeBoundaryValue::Value(int_value.to_owned(), RangeBoundaryType::Inclusive), - RangeBoundaryValue::Value(int_value, RangeBoundaryType::Inclusive), - ) - .unwrap() - } -} - -/// Provides `Range` for given `&str` -impl TryFrom<&str> for RangeImpl { - type Error = IonSchemaError; - - fn try_from(value: &str) -> Result { - let timestamp_precision: TimestampPrecision = value.try_into()?; - RangeImpl::range( - RangeBoundaryValue::Value(timestamp_precision.to_owned(), RangeBoundaryType::Inclusive), - RangeBoundaryValue::Value(timestamp_precision, RangeBoundaryType::Inclusive), - ) - } -} - -impl RangeImpl { - pub fn new(start: S, end: E) -> IonSchemaResult - where - S: Into>, - E: Into>, - { - let start = start.into(); - let end = end.into(); - RangeImpl::range(start, end) - } -} - -impl Display for RangeImpl { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "range::[ {}, {} ]", &self.start, &self.end) - } -} - -impl WriteToIsl for RangeImpl { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.step_in(IonType::List)?; - self.start.write_to(writer)?; - self.end.write_to(writer)?; - writer.step_out()?; - Ok(()) - } -} - -// This lets us turn any `T` into a RangeBoundaryValue::Value(_, Inclusive) -impl From for RangeBoundaryValue { - fn from(value: T) -> RangeBoundaryValue { - RangeBoundaryValue::Value(value, RangeBoundaryType::Inclusive) - } -} - -/// Provides typed range boundary values -// this is a wrapper around generic `RangeBoundaryValue` and is used when generating ranges from an ion IonElement -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub(crate) enum TypedRangeBoundaryValue { - Min, - Max, - Integer(RangeBoundaryValue), - NonNegativeInteger(RangeBoundaryValue), - TimestampPrecision(RangeBoundaryValue), - Float(RangeBoundaryValue), - Decimal(RangeBoundaryValue), - Number(RangeBoundaryValue), - Timestamp(RangeBoundaryValue), -} - -impl TypedRangeBoundaryValue { - fn from_ion_element( - boundary: &Element, - range_type: RangeType, - isl_version: IslVersion, - ) -> IonSchemaResult { - let range_boundary_type = if boundary.annotations().contains("exclusive") { - RangeBoundaryType::Exclusive - } else { - RangeBoundaryType::Inclusive - }; - match boundary.ion_type() { - IonType::Symbol => { - let sym = try_to!(try_to!(boundary.as_symbol()).text()); - - if (sym == "min" || sym == "max") - && range_boundary_type == RangeBoundaryType::Exclusive - { - return invalid_schema_error( - "Exclusive min or max are not allowed for range boundary values", - ); - } - - match sym { - "min" => Ok(TypedRangeBoundaryValue::Min), - "max" => Ok(TypedRangeBoundaryValue::Max), - _ => Ok(TypedRangeBoundaryValue::TimestampPrecision( - RangeBoundaryValue::Value(sym.try_into()?, range_boundary_type), - )), - } - } - IonType::Int => { - return match range_type { - RangeType::Precision | RangeType::NonNegativeInteger => { - Ok(TypedRangeBoundaryValue::NonNegativeInteger( - RangeBoundaryValue::Value( - Range::validate_non_negative_integer_range_boundary_value( - boundary.as_int().unwrap(), - &range_type, - )?, - range_boundary_type, - ), - )) - }, - RangeType::Any => { - Ok(TypedRangeBoundaryValue::Integer(RangeBoundaryValue::Value( - boundary.as_int().unwrap().to_owned(), - range_boundary_type, - ))) - }, - RangeType::TimestampPrecision => invalid_schema_error( - "Timestamp precision ranges can not be constructed for integer boundary values", - ), - RangeType::NumberOrTimestamp => Ok(TypedRangeBoundaryValue::Number(RangeBoundaryValue::Value( - boundary.as_int().unwrap().into(), - range_boundary_type, - ))), - }; - } - IonType::Decimal => match range_type { - RangeType::NumberOrTimestamp => { - Ok(TypedRangeBoundaryValue::Number(RangeBoundaryValue::Value( - boundary.as_decimal().unwrap().into(), - range_boundary_type, - ))) - } - RangeType::Any => Ok(TypedRangeBoundaryValue::Decimal(RangeBoundaryValue::Value( - boundary.as_decimal().unwrap().to_owned(), - range_boundary_type, - ))), - _ => invalid_schema_error(format!( - "{range_type:?} ranges can not be constructed for decimal boundary values" - )), - }, - IonType::Float => match range_type { - RangeType::NumberOrTimestamp => { - Ok(TypedRangeBoundaryValue::Number(RangeBoundaryValue::Value( - boundary.as_float().unwrap().to_owned().try_into()?, - range_boundary_type, - ))) - } - RangeType::Any => Ok(TypedRangeBoundaryValue::Float(RangeBoundaryValue::Value( - boundary.as_float().unwrap().to_owned(), - range_boundary_type, - ))), - _ => invalid_schema_error(format!( - "{range_type:?} ranges can not be constructed for float boundary values" - )), - }, - IonType::Timestamp => match range_type { - RangeType::NumberOrTimestamp | RangeType::Any => { - // For ISL 1.0, verify that range boundary here doesn't have an unknown offset - // For timestamp ranges neither boundaries should have an unknown offset - if isl_version == IslVersion::V1_0 - && boundary.as_timestamp().unwrap().offset().is_none() - { - return invalid_schema_error( - "Timestamp range boundary can not have an unknown offset", - ); - } - Ok(TypedRangeBoundaryValue::Timestamp( - RangeBoundaryValue::Value( - boundary.as_timestamp().unwrap().to_owned(), - range_boundary_type, - ), - )) - } - _ => invalid_schema_error(format!( - "{range_type:?} ranges can not be constructed for timestamp boundary values" - )), - }, - _ => invalid_schema_error(format!( - "Unsupported range boundary type specified {}", - boundary.ion_type() - )), - } - } -} -/// Represents a range boundary value (i.e. min, max or a value in terms of [RangeBoundaryType]) -#[derive(Debug, Clone)] -pub enum RangeBoundaryValue { - Max, - Min, - Value(T, RangeBoundaryType), -} - -impl RangeBoundaryValue { - pub fn range_boundary_type(&self) -> &RangeBoundaryType { - match self { - Value(_, range_boundary_type) => range_boundary_type, - _ => &RangeBoundaryType::Inclusive, - } - } - - pub fn range_boundary_value(&self) -> Option<&T> { - match self { - Value(v, _) => Some(v), - _ => None, - } - } -} - -// This PartialEq implementation doesn't consider RangeBoundaryType for equivalence -impl PartialEq for RangeBoundaryValue { - fn eq(&self, other: &Self) -> bool { - match (&self, other) { - (Max, Max) => true, - (Max, _) => false, - (Min, Min) => true, - (Min, _) => false, - (Value(v1, _), Value(v2, _)) => v1 == v2, - (Value(_, _), _) => false, - } - } -} - -impl PartialOrd for RangeBoundaryValue { - fn partial_cmp(&self, other: &Self) -> Option { - match (&self, other) { - (Max, _) => Some(Ordering::Greater), - (_, Min) => Some(Ordering::Greater), - (_, Max) => Some(Ordering::Less), - (Min, _) => Some(Ordering::Less), - (Value(v1, this_range_boundary_type), Value(v2, that_range_boundary_type)) => { - v1.partial_cmp(v2) - } - } - } -} - -impl Display for RangeBoundaryValue { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "{}", - match &self { - Max => "max".to_string(), - Min => "min".to_string(), - Value(value, range_boundary_type) => { - format!("{range_boundary_type}{value}") - } - } - ) - } -} - -impl WriteToIsl for RangeBoundaryValue { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - match &self { - Max => writer.write_symbol("max")?, - Min => writer.write_symbol("min")?, - Value(value, range_boundary_type) => { - if range_boundary_type == &RangeBoundaryType::Exclusive { - writer.set_annotations(["exclusive"]); - } - let element = Element::read_one(format!("{value}").as_bytes())?; - writer.write_element(&element)?; - } - } - Ok(()) - } -} - -/// Represents the range boundary types in terms of exclusivity (i.e. inclusive or exclusive) -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] -pub enum RangeBoundaryType { - Inclusive, - Exclusive, -} - -impl Display for RangeBoundaryType { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "{}", - match &self { - RangeBoundaryType::Inclusive => "", - RangeBoundaryType::Exclusive => "exclusive::", - } - ) - } -} - -/// Represents if the range is non negative integer range or not -/// This will be used while creating an integer range from Element -/// to explicitly state if its non negative or not -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RangeType { - Precision, // used by precision constraint to specify non negative integer precision with minimum value as `1` - NonNegativeInteger, // used by byte_length, container_length and codepoint_length to specify non negative integer range - TimestampPrecision, // used by timestamp_precision to specify timestamp precision range - NumberOrTimestamp, // used by valid_values constraint - Any, // used for any range types (e.g. Integer, Float, Timestamp, Decimal) -} - -/// Represents number boundary values -/// A number can be float, integer or decimal -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] -pub struct Number { - big_decimal_value: BigDecimal, -} - -impl Number { - pub fn new(big_decimal_value: BigDecimal) -> Self { - Self { big_decimal_value } - } - - pub fn big_decimal_value(&self) -> &BigDecimal { - &self.big_decimal_value - } -} - -impl TryFrom for Number { - type Error = IonSchemaError; - - fn try_from(value: f64) -> Result { - // Note: could not use BigDecimal's `try_from` method here as that uses `DIGITS` instead of `MANTISSA_DIGITS`. - // `DIGITS` gives an approximate number of significant digits, which failed a test from ion-schema-tests test suite - Ok(Number { - big_decimal_value: BigDecimal::from_str(&format!( - "{:.PRECISION$e}", - value, - PRECISION = f64::MANTISSA_DIGITS as usize - )) - .map_err(|err| { - invalid_schema_error_raw(format!("Cannot convert f64 to BigDecimal for {value}")) - })?, - }) - } -} - -impl From<&Decimal> for Number { - fn from(value: &Decimal) -> Self { - let mut value = value.to_owned(); - // When Decimal is converted to BigDecimal, it returns an Error if the Decimal being - // converted is a negative zero, which BigDecimal cannot represent. Otherwise returns Ok. - // hence if we detect negative zero we convert it to zero and make this infallible - if value.is_zero() { - value = Decimal::from(0); - } - Number { - big_decimal_value: value.try_into().unwrap(), - } - } -} - -impl From<&Int> for Number { - fn from(value: &Int) -> Self { - Number { - big_decimal_value: match value { - Int::I64(int_val) => int_val.to_owned().into(), - Int::BigInt(big_int_val) => big_int_val.to_owned().into(), - }, - } - } -} - -impl Display for Number { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", &self.big_decimal_value) - } -} diff --git a/ion-schema/src/isl/isl_type_reference.rs b/ion-schema/src/isl/isl_type_reference.rs index 8cd094a..80d3912 100644 --- a/ion-schema/src/isl/isl_type_reference.rs +++ b/ion-schema/src/isl/isl_type_reference.rs @@ -1,25 +1,27 @@ +use crate::ion_extension::ElementExtensions; use crate::isl::isl_import::{IslImport, IslImportType}; -use crate::isl::isl_range::{Range, RangeType}; use crate::isl::isl_type::IslTypeImpl; +use crate::isl::ranges::{Limit, UsizeRange}; use crate::isl::IslVersion; use crate::isl::WriteToIsl; +use crate::isl_require; use crate::result::{ invalid_schema_error, invalid_schema_error_raw, unresolvable_schema_error, IonSchemaResult, }; use crate::system::{PendingTypes, TypeId, TypeStore}; use crate::type_reference::{TypeReference, VariablyOccurringTypeRef}; use crate::types::TypeDefinitionImpl; -use ion_rs::element::Element; +use ion_rs::element::{Element, Value}; use ion_rs::{IonType, IonWriter}; /// Provides public facing APIs for constructing ISL type references programmatically for ISL 1.0 pub mod v_1_0 { use crate::isl::isl_constraint::{IslConstraint, IslConstraintImpl}; - use crate::isl::isl_range::Range; use crate::isl::isl_type::IslTypeImpl; use crate::isl::isl_type_reference::{ IslTypeRef, IslTypeRefImpl, IslVariablyOccurringTypeRef, NullabilityModifier, }; + use crate::isl::ranges::UsizeRange; use ion_rs::IonType; /// Creates a named [IslTypeRef] using the name of the type referenced inside it @@ -54,7 +56,7 @@ pub mod v_1_0 { /// Creates an [IslVariablyOccurringTypeRef] using the [IslConstraint]s and [Range] referenced inside it pub fn variably_occurring_type_ref( type_ref: IslTypeRef, - occurs: Range, + occurs: UsizeRange, ) -> IslVariablyOccurringTypeRef { IslVariablyOccurringTypeRef::new(type_ref, occurs) } @@ -63,10 +65,10 @@ pub mod v_1_0 { /// Provides public facing APIs for constructing ISL type references programmatically for ISL 2.0 pub mod v_2_0 { use crate::isl::isl_constraint::IslConstraint; - use crate::isl::isl_range::Range; use crate::isl::isl_type_reference::{ v_1_0, IslTypeRef, IslTypeRefImpl, IslVariablyOccurringTypeRef, NullabilityModifier, }; + use crate::isl::ranges::UsizeRange; /// Creates a named [IslTypeRef] using the name of the type referenced inside it pub fn named_type_ref>(name: A) -> IslTypeRef { @@ -89,7 +91,7 @@ pub mod v_2_0 { /// Creates an anonymous [IslTypeRef] using the [IslConstraint]s and [Range] referenced inside it pub fn variably_occurring_type_ref( type_ref: IslTypeRef, - occurs: Range, + occurs: UsizeRange, ) -> IslVariablyOccurringTypeRef { v_1_0::variably_occurring_type_ref(type_ref, occurs) } @@ -237,7 +239,7 @@ impl IslTypeRefImpl { } else { // for ISL 1.0 `occurs` field is a no op when used with constraints other than `fields` and `ordered_elements`. // Although ISL 1.0 will treat this `occurs` field as no op it still has to serialize the `occurs` range to see if its a valid range. - IslVariablyOccurringTypeRef::occurs_from_ion_element(occurs_field, isl_version)?; + IslVariablyOccurringTypeRef::occurs_from_ion_element(occurs_field)?; } } @@ -390,11 +392,11 @@ impl WriteToIsl for IslTypeRefImpl { #[derive(Debug, Clone, PartialEq)] pub struct IslVariablyOccurringTypeRef { type_ref: IslTypeRefImpl, - occurs: Range, + occurs: UsizeRange, } impl IslVariablyOccurringTypeRef { - pub(crate) fn new(type_ref: IslTypeRef, occurs: Range) -> Self { + pub(crate) fn new(type_ref: IslTypeRef, occurs: UsizeRange) -> Self { Self { type_ref: type_ref.type_reference, occurs, @@ -404,18 +406,18 @@ impl IslVariablyOccurringTypeRef { pub fn optional(type_ref: IslTypeRef) -> Self { Self { type_ref: type_ref.type_reference, - occurs: Range::optional(), + occurs: UsizeRange::zero_or_one(), } } pub fn required(type_ref: IslTypeRef) -> Self { Self { type_ref: type_ref.type_reference, - occurs: Range::required(), + occurs: UsizeRange::new_single_value(1), } } - pub fn occurs(&self) -> Range { + pub fn occurs(&self) -> UsizeRange { self.occurs.to_owned() } @@ -433,55 +435,35 @@ impl IslVariablyOccurringTypeRef { true, )?; - let occurs: Range = value + let occurs: UsizeRange = value .as_struct() .and_then(|s| { s.get("occurs") - .map(|r| IslVariablyOccurringTypeRef::occurs_from_ion_element(r, isl_version)) + .map(IslVariablyOccurringTypeRef::occurs_from_ion_element) }) .unwrap_or(if constraint_name == "fields" { - Ok(Range::optional()) + Ok(UsizeRange::zero_or_one()) } else { - Ok(Range::required()) + Ok(UsizeRange::new_single_value(1)) })?; + isl_require!(occurs.upper() != &Limit::Inclusive(0usize) => "Occurs cannot be 0")?; Ok(IslVariablyOccurringTypeRef { type_ref, occurs }) } - fn occurs_from_ion_element(value: &Element, isl_version: IslVersion) -> IonSchemaResult { - use IonType::*; + fn occurs_from_ion_element(value: &Element) -> IonSchemaResult { if value.is_null() { return invalid_schema_error( "expected an integer or integer range for an `occurs` constraint, found null", ); } - match value.ion_type() { - Symbol => { - let sym = try_to!(try_to!(value.as_symbol()).text()); - match sym { - "optional" => Ok(Range::optional()), - "required" => Ok(Range::required()), - _ => { - invalid_schema_error(format!( - "only optional and required symbols are supported with occurs constraint, found {sym}" - )) - } - } + match value.value() { + Value::Symbol(s) if s.text() == Some("optional") => Ok(UsizeRange::zero_or_one()), + Value::Symbol(s) if s.text() == Some("required") => Ok(UsizeRange::new_single_value(1)), + Value::List(_) | Value::Int(_) => { + UsizeRange::from_ion_element(value, Element::as_usize) } - List | Int => { - if value.ion_type() == Int && value.as_int().unwrap() <= &ion_rs::Int::I64(0) { - return invalid_schema_error("occurs constraint can not be 0"); - } - Ok(Range::from_ion_element( - value, - RangeType::NonNegativeInteger, - isl_version, - )?) - } - _ => invalid_schema_error(format!( - "ion type: {:?} is not supported with occurs constraint", - value.ion_type() - )), + _ => invalid_schema_error(format!("Invalid occurs value: {value}")), } } diff --git a/ion-schema/src/isl/mod.rs b/ion-schema/src/isl/mod.rs index a7aac87..35d477d 100644 --- a/ion-schema/src/isl/mod.rs +++ b/ion-schema/src/isl/mod.rs @@ -145,9 +145,9 @@ use std::fmt::{Display, Formatter}; pub mod isl_constraint; pub mod isl_import; -pub mod isl_range; pub mod isl_type; pub mod isl_type_reference; +pub mod ranges; pub mod util; /// Represents Ion Schema Language Versions @@ -368,34 +368,26 @@ impl IslSchemaImpl { #[cfg(test)] mod isl_tests { use crate::authority::FileSystemDocumentAuthority; + use crate::ion_extension::ElementExtensions; use crate::isl::isl_constraint::v_1_0::*; use crate::isl::isl_constraint::IslConstraint; - use crate::isl::isl_range::DecimalRange; - use crate::isl::isl_range::FloatRange; - use crate::isl::isl_range::IntegerRange; - use crate::isl::isl_range::Number; - use crate::isl::isl_range::NumberRange; - use crate::isl::isl_range::TimestampPrecisionRange; - use crate::isl::isl_range::TimestampRange; - use crate::isl::isl_range::{Range, RangeBoundaryValue, RangeType}; use crate::isl::isl_type::v_1_0::*; use crate::isl::isl_type::{IslType, IslTypeImpl}; use crate::isl::isl_type_reference::v_1_0::*; + use crate::isl::ranges::*; use crate::isl::util::Ieee754InterchangeFormat; use crate::isl::util::TimestampPrecision; + use crate::isl::util::ValidValue; use crate::isl::IslVersion; use crate::isl::*; use crate::result::IonSchemaResult; use crate::system::SchemaSystem; use ion_rs::element::Element; use ion_rs::types::Decimal; - use ion_rs::types::Int as IntegerValue; - use ion_rs::types::Timestamp; use ion_rs::Symbol; use ion_rs::{IonType, TextWriterBuilder}; use rstest::*; - use std::io::Write; - use std::path::{Path, MAIN_SEPARATOR}; + use std::path::Path; use test_generator::test_resources; // helper function to create NamedIslType for isl tests using ISL 1.0 @@ -553,13 +545,13 @@ mod isl_tests { load_anonymous_type(r#" // For a schema with ordered_elements constraint as below: { ordered_elements: [ symbol, { type: int }, ] } "#), - anonymous_type([ordered_elements([variably_occurring_type_ref(named_type_ref("symbol"), Range::required()), variably_occurring_type_ref(anonymous_type_ref([type_constraint(named_type_ref("int"))]), Range::required())])]) + anonymous_type([ordered_elements([variably_occurring_type_ref(named_type_ref("symbol"), UsizeRange::new_single_value(1)), variably_occurring_type_ref(anonymous_type_ref([type_constraint(named_type_ref("int"))]), UsizeRange::new_single_value(1))])]) ), case::fields_constraint( load_anonymous_type(r#" // For a schema with fields constraint as below: { fields: { name: string, id: int} } "#), - anonymous_type([fields(vec![("name".to_owned(), variably_occurring_type_ref(named_type_ref("string"), Range::optional())), ("id".to_owned(), variably_occurring_type_ref(named_type_ref("int"), Range::optional()))].into_iter())]), + anonymous_type([fields(vec![("name".to_owned(), variably_occurring_type_ref(named_type_ref("string"), UsizeRange::zero_or_one())), ("id".to_owned(), variably_occurring_type_ref(named_type_ref("int"), UsizeRange::zero_or_one()))].into_iter())]), ), case::field_names_constraint( load_anonymous_type_v2_0(r#" // For a schema with field_names constraint as below: @@ -625,37 +617,39 @@ mod isl_tests { load_anonymous_type(r#" // For a schema with scale constraint as below: { scale: 2 } "#), - anonymous_type([scale(IntegerValue::I64(2).into())]) + anonymous_type([scale(2.into())]) ), case::exponent_constraint( load_anonymous_type_v2_0(r#" // For a schema with exponent constraint as below: - { exponent: -2 } + { exponent: 2 } "#), - isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::exponent(IntegerValue::I64(-2).into())]) + isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::exponent(2.into())]) ), case::timestamp_precision_constraint( load_anonymous_type(r#" // For a schema with timestamp_precision constraint as below: { timestamp_precision: year } "#), - anonymous_type([timestamp_precision("year".try_into().unwrap())]) + anonymous_type([timestamp_precision(TimestampPrecisionRange::new_single_value(TimestampPrecision::Year))]) ), case::valid_values_constraint( load_anonymous_type(r#" // For a schema with valid_values constraint as below: { valid_values: [2, 3.5, 5e7, "hello", hi] } "#), - anonymous_type([valid_values_with_values(vec![2.into(), Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into()]).unwrap()]) + anonymous_type([valid_values(vec![2.into(), Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into()]).unwrap()]) ), case::valid_values_with_range_constraint( load_anonymous_type(r#" // For a schema with valid_values constraint as below: { valid_values: range::[1, 5.5] } "#), anonymous_type( - [valid_values_with_range( - NumberRange::new( - Number::from(&IntegerValue::I64(1)), - Number::from(&Decimal::new(55, -1)) - ).unwrap().into()) - ] + [valid_values(vec![ + ValidValue::NumberRange( + NumberRange::new_inclusive( + 1.into(), + Decimal::new(55, -1) + ).unwrap() + ) + ]).unwrap()] ) ), case::utf8_byte_length_constraint( @@ -698,30 +692,22 @@ mod isl_tests { assert_eq!(isl_type1, isl_type2); } - // helper function to create a range - fn load_range(text: &str, isl_version: IslVersion) -> IonSchemaResult { - Range::from_ion_element( - &Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"), - RangeType::Any, - isl_version, - ) - } - // helper function to create a timestamp precision range - fn load_timestamp_precision_range(text: &str) -> IonSchemaResult { - Range::from_ion_element( + fn load_timestamp_precision_range(text: &str) -> IonSchemaResult { + TimestampPrecisionRange::from_ion_element( &Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"), - RangeType::TimestampPrecision, - IslVersion::V1_0, + |e| { + let symbol_text = e.as_symbol().and_then(Symbol::text)?; + TimestampPrecision::try_from(symbol_text).ok() + }, ) } - // helper function to create a timestamp precision range - fn load_number_range(text: &str) -> IonSchemaResult { - Range::from_ion_element( + // helper function to create a number range + fn load_number_range(text: &str) -> IonSchemaResult { + NumberRange::from_ion_element( &Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"), - RangeType::NumberOrTimestamp, - IslVersion::V1_0, + Element::any_number_as_decimal, ) } @@ -730,338 +716,6 @@ mod isl_tests { values.iter().cloned().map(|v| v.into()).collect() } - #[rstest( - range1, - range2, - case::range_with_integer( - load_range( - r#" - range::[min, 5] - "#, - IslVersion::V1_0 - ).unwrap(), - IntegerRange::new( - RangeBoundaryValue::Min, - IntegerValue::I64(5) - ).unwrap() - ), - case::range_with_float( - load_range( - r#" - range::[2e1, 5e1] - "#, - IslVersion::V1_0 - ).unwrap(), - FloatRange::new( - 2e1, - 5e1 - ).unwrap() - ), - case::range_with_decimal( - load_range( - r#" - range::[20.4, 50.5] - "#, - IslVersion::V1_0 - ).unwrap(), - DecimalRange::new( - Decimal::new(204, -1), - Decimal::new(505, -1) - ).unwrap() - ), - case::range_with_timestamp( - load_range( - r#" - range::[2020-01-01T, 2021-01-01T] - "#, - IslVersion::V2_0 - ).unwrap(), - TimestampRange::new( - Timestamp::with_year(2020).with_month(1).with_day(1).build().unwrap(), - Timestamp::with_year(2021).with_month(1).with_day(1).build().unwrap() - ).unwrap() - ), - case::range_with_timestamp_precision( - load_timestamp_precision_range( - r#" - range::[year, month] - "# - ).unwrap(), - TimestampPrecisionRange::new( - TimestampPrecision::Year, - TimestampPrecision::Month - ).unwrap() - ), - case::range_with_number( - load_number_range( - r#" - range::[1, 5.5] - "# - ).unwrap(), - NumberRange::new( - Number::from(&IntegerValue::I64(1)), - Number::try_from(&Decimal::new(55, -1)).unwrap() - ).unwrap() - ) - )] - fn owned_struct_to_range(range1: Range, range2: impl Into) { - // assert if both the ranges are same - assert_eq!(range1, range2.into()); - } - - #[rstest( - range, - case::range_with_min_max(load_range( - r#" - range::[min, max] - "#, - IslVersion::V1_0 - )), - case::range_with_max_lower_bound(load_range( - r#" - range::[max, 5] - "#, - IslVersion::V1_0 - )), - case::range_with_min_upper_bound(load_range( - r#" - range::[5, min] - "#, - IslVersion::V1_0 - )), - case::range_with_mismatched_bounds(load_range( - r#" - range::[5, 7.834] - "#, - IslVersion::V1_0 - )), - case::range_with_lower_bound_greater_than_upper_bound(load_range( - r#" - range::[10, 5] - "#, - IslVersion::V1_0 - )) - )] - fn invalid_ranges(range: IonSchemaResult) { - // determine that the range is created with an error for an invalid range - assert!(range.is_err()); - } - - #[rstest( - range, - valid_values, - invalid_values, - case::int_range( - load_range( - r#" - range::[0, 10] - "#, - IslVersion::V1_0 - ), - elements(&[5, 0, 10]), - elements(&[-5, 11]) - ), - case::int_range_with_min( - load_range( - r#" - range::[min, 10] - "#, - IslVersion::V1_0 - ), - elements(&[5, -5, 0]), - elements(&[11]) - ), - case::int_range_with_max( - load_range( - r#" - range::[0, max] - "#, - IslVersion::V1_0 - ), - elements(&[5, 0, 11]), - elements(&[-5]) - ), - case::int_range_with_exclusive( - load_range( - r#" - range::[exclusive::0, exclusive::10] - "#, - IslVersion::V1_0 - ), - elements(&[5, 9]), - elements(&[-5, 0, 10]) - ), - case::decimal_range( - load_range( - r#" - range::[0.0, 10.0] - "#, - IslVersion::V1_0 - ), - elements(&[Decimal::new(55,-1), Decimal::new(0, 0), Decimal::new(100, -1)]), - elements(&[Decimal::new(-55, -1), Decimal::new(115, -1)]) - ), - case::decimal_range_with_min( - load_range( - r#" - range::[min, 10.0] - "#, - IslVersion::V1_0 - ), - elements(&[Decimal::new(50, -1), Decimal::new(-55, -1), Decimal::new(0, 0)]), - elements(&[Decimal::new(115, -1)]) - ), - case::decimal_range_with_max( - load_range( - r#" - range::[0.0, max] - "#, - IslVersion::V1_0 - ), - elements(&[Decimal::new(55, -1), Decimal::new(115, -1)]), - elements(&[Decimal::new(-55, -1)]) - ), - case::decimal_range_with_exclusive( - load_range( - r#" - range::[exclusive::1.0, exclusive::10.0] - "#, - IslVersion::V1_0 - ), - elements(&[Decimal::new(50, -1), Decimal::new(95, -1)]), - elements(&[Decimal::new(-55, -1), Decimal::new(10, -1), Decimal::new(100, -1)]) - ), - case::float_range( - load_range( - r#" - range::[1e2, 5e2] - "#, - IslVersion::V1_0 - ), - elements(&[2e2, 1e2, 5e2]), - elements(&[-1e2,1e1, 6e2, f64::NAN, 0e0, -0e0]) - ), - case::float_range_with_min( - load_range( - r#" - range::[min, 2e5] - "#, - IslVersion::V1_0 - ), - elements(&[f64::NEG_INFINITY, 2.2250738585072014e-308, 2e5, -2e5, 0e0, -0e0]), - elements(&[3e5, f64::NAN]) - ), - case::float_range_with_max( - load_range( - r#" - range::[1e5, max] - "#, - IslVersion::V1_0 - ), - elements(&[1e5, 5e5, 1e6, 1.7976931348623157e308, f64::INFINITY]), - elements(&[-5e5, 1e2, f64::NAN]) - ), - case::float_range_with_exclusive( - load_range( - r#" - range::[exclusive::1e2, exclusive::5e2] - "#, - IslVersion::V1_0 - ), - elements(&[2e2]), - elements(&[-1e2 ,1e1, 6e2, 1e2, 5e2, f64::NAN]) - ), - case::timestamp_precision_range( - load_timestamp_precision_range( - r#" - range::[minute, second] - "# - ), - elements(&[Timestamp::with_ymd(2020, 1, 1).with_hms(0, 1, 0).build_at_offset(4 * 60).unwrap(), - Timestamp::with_ymd(2020, 1, 1).with_hour_and_minute(0, 1).build_at_offset(4 * 60).unwrap()]), - elements(&[Timestamp::with_year(2020).build().unwrap(), - Timestamp::with_ymd(2020, 1, 1).with_hms(0, 1, 0).with_milliseconds(678).build_at_offset(4 * 60).unwrap()]) - ), - case::number_range( - load_number_range( - r#" - range::[-1, 5.5] - "# - ), - vec![0.into(), (-1).into(), 1.into(), Decimal::new(55, -1).into(), 5e0.into()], - vec![(-2).into() , Decimal::new(-15, -1).into(), Decimal::new(56, -1).into(), 5e1.into()] - ), - )] - fn range_contains( - range: IonSchemaResult, - valid_values: Vec, - invalid_values: Vec, - ) { - // verify if the range contains given valid values - for valid_value in valid_values { - let range_contains_result = range.as_ref().unwrap().contains(&valid_value); - assert!(range_contains_result) - } - - // verify that range doesn't contain the invalid values - for invalid_value in invalid_values { - let range_contains_result = range.as_ref().unwrap().contains(&invalid_value); - assert!(!range_contains_result) - } - } - - #[rstest( - range, - expected, - case::range_with_integer( - IntegerRange::new( - RangeBoundaryValue::Min, - IntegerValue::I64(5) - ).unwrap(), - "range::[ min, 5 ]" - ), - case::range_with_float( - FloatRange::new( - 2e1, - 5e1 - ).unwrap(), - "range::[ 20, 50 ]" - ), - case::range_with_decimal( - DecimalRange::new( - Decimal::new(204, -1), - Decimal::new(505, -1) - ).unwrap(), - "range::[ 20.4, 50.5 ]" - ), - case::range_with_timestamp( - TimestampRange::new( - Timestamp::with_year(2020).with_month(1).with_day(1).build().unwrap(), - Timestamp::with_year(2021).with_month(1).with_day(1).build().unwrap() - ).unwrap(), - "range::[ 2020-01-01T, 2021-01-01T ]" - ), - case::range_with_timestamp_precision( - TimestampPrecisionRange::new( - TimestampPrecision::Year, - TimestampPrecision::Month - ).unwrap(), - "range::[ year, month ]" - ), - case::range_with_number( - NumberRange::new( - Number::from(&IntegerValue::I64(1)), - Number::try_from(&Decimal::new(55, -1)).unwrap() - ).unwrap(), - "range::[ 1, 5.5 ]" - ) - )] - fn range_display(range: impl Into, expected: String) { - let mut buf = Vec::new(); - write!(&mut buf, "{}", range.into()).unwrap(); - assert_eq!(expected, String::from_utf8(buf).unwrap()); - } - const SKIP_LIST: [&str; 5] = [ "ion-schema-schemas/json/json.isl", // the file contains `nan` which fails on equivalence for two schemas "ion-schema-tests/ion_schema_1_0/nullable.isl", // Needs `nullable` annotation related fixes @@ -1075,14 +729,14 @@ mod isl_tests { fn is_skip_list_path(file_name: &str) -> bool { SKIP_LIST .iter() - .map(|p| p.replace('/', &MAIN_SEPARATOR.to_string())) + .map(|p| p.replace('/', std::path::MAIN_SEPARATOR_STR)) .any(|p| p == file_name) } #[test_resources("ion-schema-tests/**/*.isl")] #[test_resources("ion-schema-schemas/**/*.isl")] fn test_write_to_isl(file_name: &str) { - if is_skip_list_path(&file_name) { + if is_skip_list_path(file_name) { return; } diff --git a/ion-schema/src/isl/ranges.rs b/ion-schema/src/isl/ranges.rs new file mode 100644 index 0000000..4338d3c --- /dev/null +++ b/ion-schema/src/isl/ranges.rs @@ -0,0 +1,513 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Range types used in Ion Schema. +//! +//! ### Why so many range types? +//! - [`UsizeRange`] is used for `*_length` constraints and `occurs` +//! - [`U64Range`] is used for the `precision` constraint because [`Decimal`] precision is measured using `u64`. +//! - [`I64Range`] is used for `exponent` and `scale` constraints +//! - [`TimestampPrecisionRange`] is used for `timestamp_precision` constraint +//! - [`NumberRange`] and [`TimestampRange`] are used for valid_values constraint + +// It would be ideal to use [`NonZeroU64`][std::num::NonZeroU64] for the `precision` constraint, but +// `NonZeroU64` is difficult to work with because it doesn't implement core ops (such as `Add`) and +// as a result, it cannot even implement the checked ops traits (such as `CheckedAdd`) from the +// `num_traits` crate. See https://github.com/rust-num/num-traits/issues/274. + +use crate::isl::ranges::base::RangeValidation; +use crate::isl::util::TimestampPrecision; +use crate::{invalid_schema_error, invalid_schema_error_raw, isl_require}; +use crate::{IonSchemaResult, WriteToIsl}; +use ion_rs::element::writer::ElementWriter; +use ion_rs::element::Element; +use ion_rs::Decimal; +use ion_rs::{IonType, IonWriter, Timestamp}; +use num_traits::{CheckedAdd, One}; +use std::fmt::{Display, Formatter}; + +/// An end (upper or lower) of a [`Range`]. +#[derive(Debug, PartialEq, Clone)] +pub enum Limit { + /// Indicates that the end of a range has no limit or appears to have no limit. + /// For example, when `NumberRange::lower() == Unbounded`, there is no actual limit to the lower + /// end of the rangeā€”it is effectively negative infinity. On the other hand, for a finite type + /// such as `i64`, when `I64Range::upper() == Unbounded`, it appears that there is no limit to + /// the upper end of the range because then the upper limit of the range is effectively the + /// maximum value that can be represented by `i64`. + /// + /// `Unbounded` is represented in Ion Schema Language as `min` or `max`, depending on the + /// position in which it occurs. + Unbounded, + /// Indicates that the end of the range includes the given value. + Inclusive(T), + /// Indicates that the end of the range excludes the given value. + Exclusive(T), +} + +impl Limit { + /// Checks if a value is above this [`Limit`], assuming that this [`Limit`] is being used as the lower end of a [`Range`]. + fn is_above + Clone>(&self, other: &V) -> bool { + let other = &other.clone().into(); + match self { + Limit::Unbounded => true, + Limit::Exclusive(this) => this > other, + Limit::Inclusive(this) => this >= other, + } + } + + /// Checks if a value is below this [`Limit`], assuming that this [`Limit`] is being used as the upper end of a [`Range`]. + fn is_below + Clone>(&self, other: &V) -> bool { + let other = &other.clone().into(); + match self { + Limit::Unbounded => true, + Limit::Exclusive(this) => this < other, + Limit::Inclusive(this) => this <= other, + } + } +} +impl Limit { + fn fmt_for_display(&self, f: &mut Formatter<'_>, unbounded_text: &str) -> std::fmt::Result { + match self { + Limit::Unbounded => f.write_str(unbounded_text), + Limit::Inclusive(value) => value.fmt(f), + Limit::Exclusive(value) => write!(f, "exclusive::{value}"), + } + } +} + +pub type UsizeRange = base::Range; +impl RangeValidation for UsizeRange { + fn is_empty(start: &Limit, end: &Limit) -> bool { + base::is_int_range_empty(start, end) + } +} +impl UsizeRange { + /// Constructs a degenerate range that only contains 1. + pub fn one() -> Self { + Self::new_single_value(1) + } + + /// Constructs an inclusive range from 0 to 1. + pub fn zero_or_one() -> Self { + Self::new_inclusive(0, 1).expect("This is safe to unwrap because 0 <= 1") + } + + pub fn inclusive_endpoints(&self) -> (usize, usize) { + // There is no danger of under/overflow because of the range is known to be non-empty, which + // implies that the lower limit cannot be Exclusive(usize::MAX) and the upper limit cannot + // be Exclusive(0) + let lower = match self.lower() { + Limit::Unbounded => 0, + Limit::Inclusive(x) => *x, + Limit::Exclusive(x) => x + 1, + }; + let upper = match self.upper() { + Limit::Unbounded => usize::MAX, + Limit::Inclusive(x) => *x, + Limit::Exclusive(x) => x - 1, + }; + (lower, upper) + } +} + +pub type U64Range = base::Range; +impl RangeValidation for U64Range { + fn is_empty(start: &Limit, end: &Limit) -> bool { + base::is_int_range_empty(start, end) + } +} + +pub type I64Range = base::Range; +impl RangeValidation for I64Range { + fn is_empty(start: &Limit, end: &Limit) -> bool { + base::is_int_range_empty(start, end) + } +} + +pub type NumberRange = base::Range; +impl RangeValidation for NumberRange {} + +pub type TimestampRange = base::Range; +impl RangeValidation for TimestampRange {} + +pub type TimestampPrecisionRange = base::Range; +impl RangeValidation for TimestampPrecisionRange {} + +// usize does not implement Into +// TODO: Remove after https://github.com/amazon-ion/ion-rust/issues/573 is released +impl WriteToIsl for UsizeRange { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + match &self.lower() { + Limit::Inclusive(value) if self.lower() == self.upper() => { + writer.write_int(&value.to_owned().into())?; + Ok(()) + } + _ => { + writer.set_annotations(["range"]); + writer.step_in(IonType::List)?; + match &self.lower() { + Limit::Unbounded => writer.write_symbol("min")?, + Limit::Inclusive(value) => writer.write_int(&(*value).into())?, + Limit::Exclusive(value) => { + writer.set_annotations(["exclusive"]); + writer.write_int(&(*value).into())?; + } + } + match &self.upper() { + Limit::Unbounded => writer.write_symbol("max")?, + Limit::Inclusive(value) => writer.write_int(&(*value).into())?, + Limit::Exclusive(value) => { + writer.set_annotations(["exclusive"]); + writer.write_int(&(*value).into())?; + } + } + writer.step_out()?; + Ok(()) + } + } + } +} + +// u64 does not implement Into +// TODO: Remove after https://github.com/amazon-ion/ion-rust/issues/573 is released +impl WriteToIsl for U64Range { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + match &self.lower() { + Limit::Inclusive(value) if self.lower() == self.upper() => { + writer.write_int(&(*value).into())?; + Ok(()) + } + _ => { + writer.set_annotations(["range"]); + writer.step_in(IonType::List)?; + match &self.lower() { + Limit::Unbounded => writer.write_symbol("min")?, + Limit::Inclusive(value) => writer.write_int(&(*value).into())?, + Limit::Exclusive(value) => { + writer.set_annotations(["exclusive"]); + writer.write_int(&(*value).into())?; + } + } + match &self.upper() { + Limit::Unbounded => writer.write_symbol("max")?, + Limit::Inclusive(value) => writer.write_int(&(*value).into())?, + Limit::Exclusive(value) => { + writer.set_annotations(["exclusive"]); + writer.write_int(&(*value).into())?; + } + } + writer.step_out()?; + Ok(()) + } + } + } +} + +/// This module contains the generic base for all of the "real" range types that we expose. +mod base { + use super::*; + + /// Trait to allow type-specific implementations to inject type-specific logic into the + /// constructor for [`Range`]. + pub trait RangeValidation { + /// Checks if two limits would result in an empty range. + fn is_empty(start: &Limit, end: &Limit) -> bool { + match (start, end) { + (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper, + (Limit::Exclusive(lower), Limit::Exclusive(upper)) + | (Limit::Exclusive(lower), Limit::Inclusive(upper)) + | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper, + _ => false, + } + } + } + + /// Checks if two limits would result in an empty range of integers. Returns true if there are + /// no integer values between `start` and `end`, taking into consideration the exclusivity of + /// the limits. + pub fn is_int_range_empty( + start: &Limit, + end: &Limit, + ) -> bool { + match (start, end) { + (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper, + (Limit::Exclusive(lower), Limit::Inclusive(upper)) + | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper, + (Limit::Exclusive(lower), Limit::Exclusive(upper)) => { + // Checking for e.g. range::[exclusive::1, exclusive::2] which is empty. + let adjusted_lower = lower.checked_add(&T::one()); + // If the _lower_ bound wraps around when we add one, then we know it's empty. + if adjusted_lower.is_none() { + return true; + } + adjusted_lower.unwrap() >= *upper + } + _ => false, + } + } + + /// Represents an interval of values where the upper and lower ends can be open, closed, or unbounded. + /// + /// At least one of the ends must be [`Limit::Exclusive`] or [`Limit::Inclusive`]. A `Range` may not be + /// empty (i.e. there must be at least one value for which [`contains`] returns `true`). + #[derive(Debug, Clone, PartialEq)] + pub struct Range { + lower: Limit, + upper: Limit, + } + + // Note the trait bound! This allows us to inject a different non-empty check depending on the + // realized type of `T`. + impl Range + where + Self: RangeValidation, + { + /// Creates a new range. + /// At least one limit must be bounded, and the range must be non-empty. + pub fn new(start: Limit, end: Limit) -> IonSchemaResult { + isl_require!(start != Limit::Unbounded || end != Limit::Unbounded => "range may not contain both 'min' and 'max'")?; + isl_require!(!Self::is_empty(&start, &end) => "")?; + Ok(Self { + lower: start, + upper: end, + }) + } + + /// Creates a new range with inclusive endpoints. + /// [start] must be less than or equal to [end]. + pub fn new_inclusive(start: T, end: T) -> IonSchemaResult { + Self::new(Limit::Inclusive(start), Limit::Inclusive(end)) + } + + /// Creates a new range containing exactly one value. + pub fn new_single_value(value: T) -> Self { + // This is safe to unwrap because we know both limits will be Closed and start == end. + Self::new_inclusive(value.clone(), value).unwrap() + } + + pub fn lower(&self) -> &Limit { + &self.lower + } + + pub fn upper(&self) -> &Limit { + &self.upper + } + + /// Checks whether the given value is contained within this range. + pub fn contains + Clone>(&self, value: &V) -> bool { + self.lower.is_below(value) && self.upper.is_above(value) + } + + /// Reads a [`Range`] from an [`Element`] of Ion Schema Language. + pub fn from_ion_element Option>( + element: &Element, + value_fn: F, + ) -> IonSchemaResult> { + if element.annotations().contains("range") { + isl_require!(element.ion_type() == IonType::List => "range must be a non-null list; found: {element}")?; + isl_require!(!element.is_null() => "range must be a non-null list; found: {element}")?; + let seq = element.as_sequence().unwrap(); + isl_require!(seq.len() == 2 => "range must have a lower and upper bound; found: {element}")?; + + let lower_limit = + Self::read_range_bound(element, seq.get(0).unwrap(), "min", &value_fn)?; + let upper_limit = + Self::read_range_bound(element, seq.get(1).unwrap(), "max", &value_fn)?; + + Self::new(lower_limit, upper_limit) + } else { + let value = value_fn(element); + if let Some(value) = value { + Ok(Self::new_single_value(value)) + } else { + invalid_schema_error(format!("invalid value for range: {element}")) + } + } + } + + fn read_range_bound Option>( + element: &Element, + boundary_element: &Element, + unbounded_text: &str, + value_fn: F, + ) -> IonSchemaResult> { + let limit = if boundary_element.as_symbol() + == Some(&ion_rs::Symbol::from(unbounded_text)) + { + isl_require!(boundary_element.annotations().is_empty() => "'{unbounded_text}' may not have annotations: {element}")?; + Limit::Unbounded + } else { + let upper_value: T = value_fn(boundary_element).ok_or_else(|| { + invalid_schema_error_raw(format!("invalid value for range boundary: {element}")) + })?; + if boundary_element.annotations().contains("exclusive") { + isl_require!(boundary_element.annotations().len() == 1 => "invalid annotation(s) on range boundary {element}")?; + Limit::Exclusive(upper_value) + } else { + isl_require!(boundary_element.annotations().is_empty() => "invalid annotation(s) on range boundary {element}")?; + Limit::Inclusive(upper_value) + } + }; + Ok(limit) + } + } + + impl From for Range + where + Self: RangeValidation, + { + fn from(value: T) -> Self { + Range::new_single_value(value) + } + } + + impl + Clone + PartialEq> WriteToIsl for &Range { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + match &self.lower { + Limit::Inclusive(value) if self.lower == self.upper => { + writer.write_element(&value.clone().into())?; + Ok(()) + } + _ => { + writer.set_annotations(["range"]); + writer.step_in(IonType::List)?; + match &self.lower { + Limit::Unbounded => writer.write_symbol("min")?, + Limit::Inclusive(value) => writer.write_element(&value.clone().into())?, + Limit::Exclusive(value) => { + writer.set_annotations(["exclusive"]); + writer.write_element(&value.clone().into())?; + } + } + match &self.upper { + Limit::Unbounded => writer.write_symbol("max")?, + Limit::Inclusive(value) => writer.write_element(&value.clone().into())?, + Limit::Exclusive(value) => { + writer.set_annotations(["exclusive"]); + writer.write_element(&value.clone().into())?; + } + } + writer.step_out()?; + Ok(()) + } + } + } + } + + impl Display for Range { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.lower { + Limit::Inclusive(value) if self.lower == self.upper => value.fmt(f), + _ => { + f.write_str("range::[")?; + self.lower.fmt_for_display(f, "min")?; + f.write_str(",")?; + self.upper.fmt_for_display(f, "max")?; + f.write_str("]") + } + } + } + } +} + +#[cfg(test)] +mod tests { + //! These test are for the generic functionality of Range. + //! More specific test cases are handled as part of ion-schema-tests. + + use crate::isl::ranges::base::Range; + use crate::isl::ranges::{I64Range, Limit}; + use crate::IonSchemaResult; + use ion_rs::element::Element; + use ion_rs::types::IntAccess; + use rstest::*; + + #[rstest( + case::range_with_min_max("range::[min, max]"), + case::range_with_max_lower_bound("range::[max, 5]"), + case::range_with_min_upper_bound("range::[5, min]"), + case::range_with_lower_bound_greater_than_upper_bound("range::[10, 5]"), + case::empty_range_with_exclusive_lower_bound("range::[exclusive::5, 5]"), + case::empty_range_with_exclusive_upper_bound("range::[5, exclusive::5]"), + case::empty_range_with_mutually_excluding_bounds("range::[exclusive::5, exclusive::5]"), + case::range_with_unknown_annotation("range::[0, foo::5]"), + case::range_without_range_annotation("[5, 10]") + )] + fn invalid_ranges_from_isl(#[case] range_ion: &str) { + let range = + I64Range::from_ion_element(&Element::read_one(range_ion).unwrap(), Element::as_i64); + assert!(range.is_err()); + } + + #[rstest( + case::range_with_both_limits_unbounded(Range::new(Limit::Unbounded, Limit::Unbounded)), + case::range_with_lower_bound_greater_than_upper_bound(Range::new( + Limit::Inclusive(3), + Limit::Inclusive(1) + )), + case::range_with_lower_bound_greater_than_upper_bound(Range::new_inclusive(3, 1)), + case::empty_range_with_exclusive_lower_bound(Range::new( + Limit::Exclusive(3), + Limit::Inclusive(3) + )), + case::empty_range_with_exclusive_upper_bound(Range::new( + Limit::Inclusive(3), + Limit::Exclusive(3) + )) + )] + fn invalid_ranges_from_constructor(#[case] range_result: IonSchemaResult>) { + assert!(range_result.is_err()); + } + + #[rstest( + case::lower_is_unbounded( + Range::new(Limit::Unbounded, Limit::Inclusive(5)), + vec![-128, 0, 5], + vec![6, 127], + ), + case::upper_is_unbounded( + Range::new(Limit::Inclusive(5), Limit::Unbounded), + vec![5, 6, 127], + vec![-128, 0, 4], + ), + case::lower_bound_is_exclusive( + Range::new(Limit::Exclusive(5), Limit::Inclusive(10)), + vec![6, 9, 10], + vec![0, 5, 11], + ), + case::upper_bound_is_exclusive( + Range::new(Limit::Inclusive(5), Limit::Exclusive(10)), + vec![5, 6, 9], + vec![0, 4, 10], + ) + )] + fn range_contains( + #[case] range: IonSchemaResult>, + #[case] valid_values: Vec, + #[case] invalid_values: Vec, + ) { + for valid_value in valid_values { + let range_contains_result = range.as_ref().unwrap().contains(&valid_value); + assert!(range_contains_result) + } + for invalid_value in invalid_values { + let range_contains_result = range.as_ref().unwrap().contains(&invalid_value); + assert!(!range_contains_result) + } + } + + #[rstest( + case::a_very_simple_case("range::[0,1]", Range::new(Limit::Inclusive(0), Limit::Inclusive(1)).unwrap()), + case::lower_is_unbounded("range::[min,1]", Range::new(Limit::Unbounded, Limit::Inclusive(1)).unwrap()), + case::upper_is_unbounded("range::[1,max]", Range::new(Limit::Inclusive(1), Limit::Unbounded).unwrap()), + case::lower_equals_upper("1", Range::new_single_value(1)), + case::lower_is_exclusive("range::[exclusive::0,2]", Range::new(Limit::Exclusive(0), Limit::Inclusive(2)).unwrap()), + case::upper_is_exclusive("range::[0,exclusive::2]", Range::new(Limit::Inclusive(0), Limit::Exclusive(2)).unwrap()), + // In some cases, the range can be elided to a number + case::upper_is_exclusive("1", Range::new(Limit::Inclusive(1), Limit::Inclusive(1)).unwrap()), + )] + fn range_display(#[case] expected: &str, #[case] range: Range) { + assert_eq!(expected, format!("{range}")); + } +} diff --git a/ion-schema/src/isl/util.rs b/ion-schema/src/isl/util.rs index de466a6..1f2fa36 100644 --- a/ion-schema/src/isl/util.rs +++ b/ion-schema/src/isl/util.rs @@ -1,10 +1,12 @@ -use crate::isl::isl_range::{Range, RangeType}; +use crate::ion_extension::ElementExtensions; +use crate::isl::ranges::{NumberRange, TimestampRange}; use crate::isl::{IslVersion, WriteToIsl}; +use crate::isl_require; use crate::result::{invalid_schema_error, IonSchemaError, IonSchemaResult}; use ion_rs::element::writer::ElementWriter; -use ion_rs::element::Element; +use ion_rs::element::{Element, Value}; use ion_rs::types::Precision; -use ion_rs::{IonWriter, Symbol, Timestamp}; +use ion_rs::{IonType, IonWriter, Timestamp}; use num_traits::abs; use std::cmp::Ordering; use std::fmt; @@ -103,6 +105,20 @@ impl TimestampPrecision { }, } } + + fn string_value(&self) -> String { + match self { + TimestampPrecision::Year => "year".to_string(), + TimestampPrecision::Month => "month".to_string(), + TimestampPrecision::Day => "day".to_string(), + TimestampPrecision::Minute => "minute".to_string(), + TimestampPrecision::Second => "second".to_string(), + TimestampPrecision::Millisecond => "millisecond".to_string(), + TimestampPrecision::Microsecond => "microsecond".to_string(), + TimestampPrecision::Nanosecond => "nanosecond".to_string(), + TimestampPrecision::OtherFractionalSeconds(i) => format!("fractional second (10e{i})"), + } + } } impl TryFrom<&str> for TimestampPrecision { @@ -158,21 +174,22 @@ impl PartialOrd for TimestampPrecision { } } +impl WriteToIsl for TimestampPrecision { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + writer.write_symbol(self.string_value())?; + Ok(()) + } +} + +impl From for Element { + fn from(value: TimestampPrecision) -> Self { + Element::symbol(value.string_value()) + } +} + impl Display for TimestampPrecision { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match &self { - TimestampPrecision::Year => write!(f, "year"), - TimestampPrecision::Month => write!(f, "month"), - TimestampPrecision::Day => write!(f, "day"), - TimestampPrecision::Minute => write!(f, "minute"), - TimestampPrecision::Second => write!(f, "second"), - TimestampPrecision::Millisecond => write!(f, "millisecond"), - TimestampPrecision::Microsecond => write!(f, "microsecond"), - TimestampPrecision::Nanosecond => write!(f, "nanosecond"), - TimestampPrecision::OtherFractionalSeconds(scale) => { - write!(f, "fractional second (10e{})", scale * -1) - } - } + f.write_str(&self.string_value()) } } @@ -186,28 +203,36 @@ impl Display for TimestampPrecision { /// `valid_values`: `` #[derive(Debug, Clone, PartialEq)] pub enum ValidValue { - Range(Range), - Element(Element), + NumberRange(NumberRange), + TimestampRange(TimestampRange), + Element(Value), } impl ValidValue { - pub fn from_ion_element(value: &Element, isl_version: IslVersion) -> IonSchemaResult { - if value.annotations().contains("range") { - Ok(ValidValue::Range(Range::from_ion_element( - value, - RangeType::NumberOrTimestamp, - isl_version, - )?)) - } else if value - .annotations() - .iter() - .any(|a| a != &Symbol::from("range")) - { - invalid_schema_error( - "Annotations are not allowed for valid_values constraint except `range` annotation", - ) + pub fn from_ion_element(element: &Element, isl_version: IslVersion) -> IonSchemaResult { + let annotation = element.annotations(); + if element.annotations().contains("range") { + isl_require!(annotation.len() == 1 => "Unexpected annotation(s) on valid values argument: {element}")?; + // Does it contain any timestamps + let has_timestamp = element.as_sequence().map_or(false, |s| { + s.elements().any(|it| it.ion_type() == IonType::Timestamp) + }); + let range = if has_timestamp { + ValidValue::TimestampRange(TimestampRange::from_ion_element(element, |e| { + e.as_timestamp() + .cloned() + .filter(|t| isl_version != IslVersion::V1_0 || t.offset().is_some()) + })?) + } else { + ValidValue::NumberRange(NumberRange::from_ion_element( + element, + Element::any_number_as_decimal, + )?) + }; + Ok(range) } else { - Ok(ValidValue::Element(value.to_owned())) + isl_require!(annotation.is_empty() => "Unexpected annotation(s) on valid values argument: {element}")?; + Ok(ValidValue::Element(element.value().to_owned())) } } } @@ -215,8 +240,9 @@ impl ValidValue { impl Display for ValidValue { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { match self { - ValidValue::Range(range) => write!(f, "{range}"), ValidValue::Element(element) => write!(f, "{element}"), + ValidValue::NumberRange(r) => write!(f, "{r}"), + ValidValue::TimestampRange(r) => write!(f, "{r}"), } } } @@ -224,13 +250,33 @@ impl Display for ValidValue { impl WriteToIsl for ValidValue { fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { match self { - ValidValue::Range(range) => range.write_to(writer)?, - ValidValue::Element(element) => writer.write_element(element)?, + // TODO: replace with write_value once https://github.com/amazon-ion/ion-rust/pull/579 is released + ValidValue::Element(value) => writer.write_element(&value.to_owned().into())?, + ValidValue::NumberRange(r) => r.write_to(writer)?, + ValidValue::TimestampRange(r) => r.write_to(writer)?, } Ok(()) } } +impl From for ValidValue { + fn from(number_range: NumberRange) -> Self { + ValidValue::NumberRange(number_range) + } +} + +impl From for ValidValue { + fn from(timestamp_range: TimestampRange) -> Self { + ValidValue::TimestampRange(timestamp_range) + } +} + +impl> From for ValidValue { + fn from(value: T) -> Self { + ValidValue::Element(value.into()) + } +} + /// Represent a timestamp offset /// Known timestamp offset value is stored in minutes as i32 value /// For example, "+07::00" wil be stored as `TimestampOffset::Known(420)` diff --git a/ion-schema/src/lib.rs b/ion-schema/src/lib.rs index 0bf4737..3af1bd7 100644 --- a/ion-schema/src/lib.rs +++ b/ion-schema/src/lib.rs @@ -30,6 +30,7 @@ macro_rules! try_to { pub mod authority; mod constraint; mod import; +pub(crate) mod ion_extension; mod ion_path; pub mod isl; mod nfa; @@ -233,8 +234,8 @@ impl UserReservedFields { && f.text() != Some("type") }) { return invalid_schema_error( - "User reserved fields can only have schema_header, schema_footer or type as the field names", - ); + "User reserved fields can only have schema_header, schema_footer or type as the field names", + ); } Ok(Self { schema_header_fields: UserReservedFields::field_names_from_ion_elements( diff --git a/ion-schema/src/result.rs b/ion-schema/src/result.rs index 96d3b2b..ebaadcc 100644 --- a/ion-schema/src/result.rs +++ b/ion-schema/src/result.rs @@ -91,3 +91,19 @@ pub fn unresolvable_schema_error_raw>(description: S) -> IonSchema description: description.as_ref().to_string(), } } + +/// A macro that checks some condition required to be valid ISL. +/// +/// If invalid, returns an InvalidSchemaErr with the given error message. +#[macro_export] +macro_rules! isl_require { + ($expression:expr => $fmt_string:literal $(, $($tt:tt)*)?) => { + if ($expression) { + Ok(()) + } else { + Err($crate::result::IonSchemaError::InvalidSchemaError { + description: format!($fmt_string), + }) + } + }; +} diff --git a/ion-schema/src/type_reference.rs b/ion-schema/src/type_reference.rs index 500ac0d..b6e932e 100644 --- a/ion-schema/src/type_reference.rs +++ b/ion-schema/src/type_reference.rs @@ -1,6 +1,6 @@ use crate::ion_path::IonPath; -use crate::isl::isl_range::Range; use crate::isl::isl_type_reference::NullabilityModifier; +use crate::isl::ranges::UsizeRange; use crate::result::ValidationResult; use crate::system::{TypeId, TypeStore}; use crate::types::TypeValidator; @@ -78,11 +78,11 @@ impl TypeValidator for TypeReference { #[derive(Debug, Clone, PartialEq)] pub struct VariablyOccurringTypeRef { type_ref: TypeReference, - occurs_range: Range, // represents the range provided by `occurs` field for given type reference + occurs_range: UsizeRange, // represents the range provided by `occurs` field for given type reference } impl VariablyOccurringTypeRef { - pub fn new(type_ref: TypeReference, occurs_range: Range) -> Self { + pub fn new(type_ref: TypeReference, occurs_range: UsizeRange) -> Self { Self { type_ref, occurs_range, @@ -93,7 +93,7 @@ impl VariablyOccurringTypeRef { self.type_ref } - pub fn occurs_range(&self) -> &Range { + pub fn occurs_range(&self) -> &UsizeRange { &self.occurs_range } } diff --git a/ion-schema/src/types.rs b/ion-schema/src/types.rs index 383868d..0a57b64 100644 --- a/ion-schema/src/types.rs +++ b/ion-schema/src/types.rs @@ -605,17 +605,15 @@ mod type_definition_tests { use super::*; use crate::constraint::Constraint; use crate::isl::isl_constraint::v_1_0::*; - use crate::isl::isl_range::Number; - use crate::isl::isl_range::NumberRange; - use crate::isl::isl_range::Range; use crate::isl::isl_type::v_1_0::*; use crate::isl::isl_type::IslType; use crate::isl::isl_type_reference::v_1_0::*; + use crate::isl::ranges::*; use crate::isl::util::Ieee754InterchangeFormat; + use crate::isl::util::TimestampPrecision; use crate::isl::*; use crate::system::PendingTypes; - use ion_rs::Decimal; - use ion_rs::Int; + use rstest::*; use std::collections::HashSet; @@ -696,14 +694,14 @@ mod type_definition_tests { /* For a schema with ordered_elements constraint as below: { ordered_elements: [ symbol, { type: int }, ] } */ - anonymous_type([ordered_elements([variably_occurring_type_ref(named_type_ref("symbol"), Range::required()), variably_occurring_type_ref(anonymous_type_ref([type_constraint(named_type_ref("int"))]), Range::required())])]), + anonymous_type([ordered_elements([variably_occurring_type_ref(named_type_ref("symbol"), UsizeRange::new_single_value(1)), variably_occurring_type_ref(anonymous_type_ref([type_constraint(named_type_ref("int"))]), UsizeRange::new_single_value(1))])]), TypeDefinitionKind::anonymous([Constraint::ordered_elements([5, 36]), Constraint::type_constraint(34)]) ), case::fields_constraint( /* For a schema with fields constraint as below: { fields: { name: string, id: int} } */ - anonymous_type([fields(vec![("name".to_owned(), variably_occurring_type_ref(named_type_ref("string"), Range::optional())), ("id".to_owned(), variably_occurring_type_ref(named_type_ref("int"), Range::optional()))].into_iter())]), + anonymous_type([fields(vec![("name".to_owned(), variably_occurring_type_ref(named_type_ref("string"), UsizeRange::zero_or_one())), ("id".to_owned(), variably_occurring_type_ref(named_type_ref("int"), UsizeRange::zero_or_one()))].into_iter())]), TypeDefinitionKind::anonymous([Constraint::fields(vec![("name".to_owned(), 4), ("id".to_owned(), 0)].into_iter()), Constraint::type_constraint(34)]) ), case::field_names_constraint( @@ -780,50 +778,29 @@ mod type_definition_tests { /* For a schema with scale constraint as below: { scale: 2 } */ - anonymous_type([scale(Int::I64(2).into())]), - TypeDefinitionKind::anonymous([Constraint::scale(Int::I64(2).into()), Constraint::type_constraint(34)]) + anonymous_type([scale(2.into())]), + TypeDefinitionKind::anonymous([Constraint::scale(2.into()), Constraint::type_constraint(34)]) ), case::exponent_constraint( /* For a schema with exponent constraint as below: { exponent: 2 } */ - isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::exponent(Int::I64(2).into())]), - TypeDefinitionKind::anonymous([Constraint::exponent(Int::I64(2).into()), Constraint::type_constraint(34)]) + isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::exponent(2.into())]), + TypeDefinitionKind::anonymous([Constraint::exponent(2.into()), Constraint::type_constraint(34)]) ), case::timestamp_precision_constraint( /* For a schema with timestamp_precision constraint as below: { timestamp_precision: month } */ - anonymous_type([timestamp_precision("month".try_into().unwrap())]), - TypeDefinitionKind::anonymous([Constraint::timestamp_precision("month".try_into().unwrap()), Constraint::type_constraint(34)]) + anonymous_type([timestamp_precision(TimestampPrecisionRange::new_single_value(TimestampPrecision::Month))]), + TypeDefinitionKind::anonymous([Constraint::timestamp_precision(TimestampPrecision::Month.into()), Constraint::type_constraint(34)]) ), case::valid_values_constraint( - /* For a schema with valid_values constraint as below: - { valid_values: [2, 3.5, 5e7, "hello", hi] } - */ - anonymous_type([valid_values_with_values(vec![2.into(), Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into()]).unwrap()]), - TypeDefinitionKind::anonymous([Constraint::valid_values_with_values(vec![2.into(), Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into()], IslVersion::V1_0).unwrap(), Constraint::type_constraint(34)]) - ), - case::valid_values_with_range_constraint( - /* For a schema with valid_values constraint as below: - { valid_values: range::[1, 5.5] } - */ - anonymous_type( - [valid_values_with_range( - NumberRange::new( - Number::from(&Int::I64(1)), - Number::from(&Decimal::new(55, -1)) - ).unwrap().into()) - ] - ), - TypeDefinitionKind::anonymous([ - Constraint::valid_values_with_range( - NumberRange::new( - Number::from(&Int::I64(1)), - Number::from(&Decimal::new(55, -1)) - ).unwrap().into()), - Constraint::type_constraint(34) - ]) + /* For a schema with valid_values constraint as below: + { valid_values: [2, 3.5, 5e7, "hello", hi, range::[2, 3]] } + */ + anonymous_type([valid_values(vec![2.into(), ion_rs::Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into(), NumberRange::new_inclusive(2.into(), 3.into()).unwrap().into()]).unwrap()]), + TypeDefinitionKind::anonymous([Constraint::valid_values(vec![2.into(), ion_rs::Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into(), NumberRange::new_inclusive(2.into(), 3.into()).unwrap().into()], IslVersion::V1_0).unwrap(), Constraint::type_constraint(34)]) ), case::utf8_byte_length_constraint( /* For a schema with utf8_byte_length constraint as below: