diff --git a/README.md b/README.md index 91e55c8..2cdebc5 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ schema_footer::{ ### Loading a schema and validating an Ion value ```rust -use ion_rs::element::Element; +use ion_rs::Element; use ion_schema::authority::{DocumentAuthority, FileSystemDocumentAuthority}; use ion_schema::result::{IonSchemaResult, ValidationResult}; use ion_schema::schema::Schema; diff --git a/ion-schema-tests b/ion-schema-tests index 416be3e..9a0d20b 160000 --- a/ion-schema-tests +++ b/ion-schema-tests @@ -1 +1 @@ -Subproject commit 416be3edca4e62fdf3a894a9ace1d82c1152aece +Subproject commit 9a0d20b0e7d172e1e75a1a73cd97d3ae31286024 diff --git a/ion-schema-tests-runner/Cargo.toml b/ion-schema-tests-runner/Cargo.toml index e167a96..c7069e5 100644 --- a/ion-schema-tests-runner/Cargo.toml +++ b/ion-schema-tests-runner/Cargo.toml @@ -9,7 +9,7 @@ proc-macro = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ion-rs = "0.18.1" +ion-rs = "1.0.0-rc.7" ion-schema = { path = "../ion-schema" } quote = "1.0.21" syn = "1.0.102" diff --git a/ion-schema-tests-runner/src/generator.rs b/ion-schema-tests-runner/src/generator.rs index 5200410..fa5d042 100644 --- a/ion-schema-tests-runner/src/generator.rs +++ b/ion-schema-tests-runner/src/generator.rs @@ -1,7 +1,8 @@ use crate::generator::util::*; use crate::model::{TestCaseDetails, TestCaseVec}; -use ion_rs::element::Element; +use ion_rs::Element; use ion_rs::IonType; +use ion_rs::Sequence; use ion_schema::isl::IslVersion; use proc_macro2::{Literal, TokenStream, TokenTree}; use quote::{format_ident, quote}; @@ -121,7 +122,7 @@ fn generate_test_cases_for_file(ctx: Context) -> TokenStream { // get the schema content from given schema file path let ion_content = fs::read(ctx.current_dir.as_path()) .unwrap_or_else(|e| panic!("Unable to read {path_string} – {e}")); - let schema_content = + let schema_content: Sequence = Element::read_all(ion_content).unwrap_or_else(|e| panic!("Error in {path_string} – {e:?}")); let isl_version = find_isl_version(&schema_content); @@ -173,7 +174,7 @@ fn generate_test_cases_for_file(ctx: Context) -> TokenStream { } /// find ISL version from schema content -fn find_isl_version(schema_content: &[Element]) -> IslVersion { +fn find_isl_version(schema_content: &Sequence) -> IslVersion { // ISL version marker regex let isl_version_marker: Regex = Regex::new(r"^\$ion_schema_\d.*$").unwrap(); @@ -266,7 +267,7 @@ fn generate_preamble(root_dir_path: &Path) -> TokenStream { let root_dir_token = TokenTree::from(Literal::string(root_dir_path.to_str().unwrap())); quote! { - use ion_rs::element::Sequence; + use ion_rs::Sequence; use std::hash::{Hash, Hasher}; /// Gets the root directory for the test suite. @@ -322,7 +323,7 @@ fn generate_preamble(root_dir_path: &Path) -> TokenStream { fn __assert_value_validity_for_type(value_ion: &str, schema_id: &str, type_id: &str, expect_valid: bool) -> Result<(), String> { let schema = __new_schema_system().load_schema(schema_id).unwrap(); let isl_type = schema.get_type(type_id).unwrap(); - let value: ion_rs::element::Element = ion_rs::element::Element::read_one(value_ion.as_bytes()).unwrap(); + let value: ion_rs::Element = ion_rs::Element::read_one(value_ion.as_bytes()).unwrap(); let prepared_value: ion_schema::IonSchemaElement = if value.annotations().contains("document") && value.ion_type() == ion_rs::IonType::SExp { let element_vec = value.as_sequence() .unwrap_or_else(|| unreachable!("We already confirmed that this is a s-expression.")) diff --git a/ion-schema-tests-runner/src/model.rs b/ion-schema-tests-runner/src/model.rs index de9ee2e..052455c 100644 --- a/ion-schema-tests-runner/src/model.rs +++ b/ion-schema-tests-runner/src/model.rs @@ -1,4 +1,4 @@ -use ion_rs::element::{Element, List, Struct}; +use ion_rs::*; use std::ops::Deref; // Represents a single test case. @@ -211,9 +211,9 @@ impl StructUtils for Struct { field_name, Element::from(self.clone()) ))?; - Ok(ion_rs::element::List(list.clone())) + Ok(ion_rs::List(list.clone())) } else { - Ok(ion_rs::element::List(Element::sequence_builder().build())) + Ok(ion_rs::List(Element::sequence_builder().build())) } } } diff --git a/ion-schema/Cargo.toml b/ion-schema/Cargo.toml index 8ba47e0..0fb3672 100644 --- a/ion-schema/Cargo.toml +++ b/ion-schema/Cargo.toml @@ -22,7 +22,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ion-rs = "0.18.1" +ion-rs = { version = "1.0.0-rc.7", features = ["experimental-reader-writer"] } thiserror = "1.0" num-bigint = "0.3" num-traits = "0.2" diff --git a/ion-schema/examples/schema.rs b/ion-schema/examples/schema.rs index f50964e..6457f40 100644 --- a/ion-schema/examples/schema.rs +++ b/ion-schema/examples/schema.rs @@ -1,9 +1,8 @@ #[macro_use] extern crate clap; use clap::{App, ArgMatches}; -use ion_rs::element::Element; -use ion_rs::IonWriter; -use ion_rs::{IonType, TextWriterBuilder}; +use ion_rs::WriteConfig; +use ion_rs::{Element, SequenceWriter, StructWriter, TextFormat, ValueWriter, Writer}; use ion_schema::authority::{DocumentAuthority, FileSystemDocumentAuthority}; use ion_schema::result::IonSchemaResult; use ion_schema::system::SchemaSystem; @@ -86,32 +85,37 @@ fn validate(command_args: &ArgMatches) -> IonSchemaResult<()> { let type_ref = schema.unwrap().get_type(schema_type).unwrap(); // create a text writer to make the output - let mut output = vec![]; - let mut writer = TextWriterBuilder::pretty().build(&mut output)?; + let output = vec![]; + let write_config = WriteConfig::::new(TextFormat::Pretty); + let mut writer = Writer::new(write_config, output)?; // validate owned_elements according to type_ref for owned_element in owned_elements { // create a validation report with validation result, value, schema and/or violation - writer.step_in(IonType::Struct)?; + let mut struct_writer = writer.struct_writer()?; let validation_result = type_ref.validate(&owned_element); - writer.set_field_name("result"); match validation_result { Ok(_) => { - writer.write_string("Valid")?; - writer.set_field_name("value"); - writer.write_string(format!("{owned_element}"))?; - writer.set_field_name("schema"); - writer.write_string(schema_id)?; + struct_writer.field_writer("result").write_string("Valid")?; + struct_writer + .field_writer("value") + .write_string(format!("{owned_element}"))?; + struct_writer + .field_writer("schema") + .write_string(schema_id)?; } Err(_) => { - writer.write_string("Invalid")?; - writer.set_field_name("violation"); - writer.write_string(format!("{:#?}", validation_result.unwrap_err()))?; + struct_writer + .field_writer("result") + .write_string("Invalid")?; + struct_writer + .field_writer("violation") + .write_string(format!("{:#?}", validation_result.unwrap_err()))?; } } - writer.step_out()?; + struct_writer.close()?; } - drop(writer); + let output = writer.close()?; println!("Validation report:"); println!("{}", from_utf8(&output).unwrap()); Ok(()) diff --git a/ion-schema/src/authority.rs b/ion-schema/src/authority.rs index aa1fc03..1e2da1d 100644 --- a/ion-schema/src/authority.rs +++ b/ion-schema/src/authority.rs @@ -61,7 +61,7 @@ //! ``` use crate::result::{unresolvable_schema_error_raw, IonSchemaResult}; -use ion_rs::element::Element; +use ion_rs::Element; use std::collections::HashMap; use std::fmt::Debug; use std::fs; @@ -108,7 +108,7 @@ impl DocumentAuthority for FileSystemDocumentAuthority { // if absolute_path exists for the given id then load schema with file contents let ion_content = fs::read(absolute_path)?; let schema_content = Element::read_all(ion_content)?; - Ok(schema_content) + Ok(schema_content.into_iter().collect()) } } @@ -139,6 +139,6 @@ impl DocumentAuthority for MapDocumentAuthority { )) })?; let schema_content = Element::read_all(ion_content.as_bytes())?; - Ok(schema_content) + Ok(schema_content.into_iter().collect()) } } diff --git a/ion-schema/src/constraint.rs b/ion-schema/src/constraint.rs index b28edfe..161bf3b 100644 --- a/ion-schema/src/constraint.rs +++ b/ion-schema/src/constraint.rs @@ -21,10 +21,10 @@ use crate::type_reference::{TypeReference, VariablyOccurringTypeRef}; 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::Element; use ion_rs::IonData; use ion_rs::IonType; +use ion_rs::Value; use num_traits::ToPrimitive; use regex::{Regex, RegexBuilder}; use std::collections::{HashMap, HashSet}; @@ -814,7 +814,7 @@ impl OrderedElementsConstraint { // * `min == 0`, that state will have a transition that advances to the next state automatically, making an occurrence of that `type_id` optional. // // Here is an example of how the built NFA would look like for an `ordered_elements` constraint: - // ``` + // ```ion // ordered_elements: [ // { type: int, occurs: optional }, // number, @@ -1042,7 +1042,7 @@ impl ConstraintValidator for FieldsConstraint { violations.push(Violation::new( "fields", ViolationCode::TypeMismatched, - &format!( + format!( "Expected {} of field {}: found {}", occurs_range, field_name, @@ -1116,7 +1116,8 @@ impl ConstraintValidator for FieldNamesConstraint { for (field_name, _) in ion_struct.iter() { ion_path.push(IonPathElement::Field(field_name.text().unwrap().to_owned())); - let schema_element: IonSchemaElement = (&Element::symbol(field_name)).into(); + let field_name_symbol_as_element = Element::symbol(field_name); + let schema_element: IonSchemaElement = (&field_name_symbol_as_element).into(); if let Err(violation) = self.type_reference @@ -1188,7 +1189,7 @@ impl ConstraintValidator for ContainsConstraint { return Err(Violation::new( "contains", ViolationCode::TypeMismatched, - &format!( + format!( "expected list/sexp/struct/document found {}", if element.is_null() { format!("{element}") @@ -1210,7 +1211,8 @@ impl ConstraintValidator for ContainsConstraint { // for each value in expected values if it does not exist in ion sequence // then add it to missing_values to keep track of missing values for expected_value in self.values.iter() { - if !values.contains(expected_value) { + let expected = expected_value.into(); + if !values.contains(&expected) { missing_values.push(expected_value); } } @@ -1547,36 +1549,31 @@ impl ConstraintValidator for AnnotationsConstraint2_0 { type_store: &TypeStore, ion_path: &mut IonPath, ) -> ValidationResult { - match value { - IonSchemaElement::SingleElement(element) => { - let schema_element: IonSchemaElement = (&element - .annotations() - .iter() - .map(Element::symbol) - .collect::>()) - .into(); - - self.type_ref - .validate(&schema_element, type_store, ion_path) - .map_err(|v| { - Violation::with_violations( - "annotations", - ViolationCode::AnnotationMismatched, - "one or more annotations don't satisfy annotations constraint", - ion_path, - vec![v], - ) - }) - } - IonSchemaElement::Document(document) => { - // document type can not have annotations - Err(Violation::new( - "annotations", - ViolationCode::AnnotationMismatched, - "annotations constraint is not applicable for document type", - ion_path, - )) - } + if let Some(element) = value.as_element() { + let annotations: Vec = + element.annotations().iter().map(Element::symbol).collect(); + let annotations_element: Element = ion_rs::Value::List(annotations.into()).into(); + let annotations_ion_schema_element = IonSchemaElement::from(&annotations_element); + + self.type_ref + .validate(&annotations_ion_schema_element, type_store, ion_path) + .map_err(|v| { + Violation::with_violations( + "annotations", + ViolationCode::AnnotationMismatched, + "one or more annotations don't satisfy annotations constraint", + ion_path, + vec![v], + ) + }) + } else { + // document type can not have annotations + Err(Violation::new( + "annotations", + ViolationCode::AnnotationMismatched, + "annotations constraint is not applicable for document type", + ion_path, + )) } } } @@ -1748,26 +1745,23 @@ impl ConstraintValidator for AnnotationsConstraint { ) -> ValidationResult { let violations: Vec = vec![]; - match value { - IonSchemaElement::SingleElement(element) => { - // validate annotations that have list-level `ordered` annotation - if self.is_ordered { - return self - .validate_ordered_annotations(element, type_store, violations, ion_path); - } - - // validate annotations that does not have list-level `ordered` annotation - self.validate_unordered_annotations(element, type_store, violations, ion_path) - } - IonSchemaElement::Document(document) => { - // document type can not have annotations - Err(Violation::new( - "annotations", - ViolationCode::AnnotationMismatched, - "annotations constraint is not applicable for document type", - ion_path, - )) + if let Some(element) = value.as_element() { + // validate annotations that have list-level `ordered` annotation + if self.is_ordered { + return self + .validate_ordered_annotations(element, type_store, violations, ion_path); } + + // validate annotations that does not have list-level `ordered` annotation + self.validate_unordered_annotations(element, type_store, violations, ion_path) + } else { + // document type can not have annotations + Err(Violation::new( + "annotations", + ViolationCode::AnnotationMismatched, + "annotations constraint is not applicable for document type", + ion_path, + )) } } } @@ -1951,7 +1945,7 @@ impl ConstraintValidator for TimestampPrecisionConstraint { // get isl timestamp precision as a range let precision_range: &TimestampPrecisionRange = self.timestamp_precision(); - let precision = &TimestampPrecision::from_timestamp(timestamp_value); + let precision = &TimestampPrecision::from_timestamp(×tamp_value); // return a Violation if the value didn't follow timestamp precision constraint if !precision_range.contains(precision) { return Err(Violation::new( @@ -2000,8 +1994,8 @@ impl ConstraintValidator for ValidValuesConstraint { type_store: &TypeStore, ion_path: &mut IonPath, ) -> ValidationResult { - match value { - IonSchemaElement::SingleElement(element) => { + match value.as_element() { + Some(element) => { for valid_value in &self.valid_values { let does_match = match valid_value { ValidValue::Element(valid_value) => { @@ -2034,7 +2028,7 @@ impl ConstraintValidator for ValidValuesConstraint { ion_path, )) } - IonSchemaElement::Document(document) => Err(Violation::new( + _ => Err(Violation::new( "valid_values", ViolationCode::InvalidValue, format!( diff --git a/ion-schema/src/ion_extension.rs b/ion-schema/src/ion_extension.rs index 5c5b1a5..1c7f149 100644 --- a/ion-schema/src/ion_extension.rs +++ b/ion-schema/src/ion_extension.rs @@ -1,6 +1,5 @@ -use ion_rs::element::{Element, Value}; -use ion_rs::external::bigdecimal::BigDecimal; -use ion_rs::{Decimal, Int}; +use ion_rs::Decimal; +use ion_rs::{Element, Value}; use num_traits::ToPrimitive; /// Trait for adding extensions to [`Element`] that are useful for implementing Ion Schema. @@ -19,25 +18,21 @@ pub(crate) trait ElementExtensions { 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(), + Value::Int(i) => i.as_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(), + Value::Int(i) => i.as_i128()?.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::Int(i) => Some((*i).into()), Value::Float(f) => (*f).try_into().ok(), - Value::Decimal(d) => Some(d.clone()), + Value::Decimal(d) => Some(*d), _ => None, } } diff --git a/ion-schema/src/ion_path.rs b/ion-schema/src/ion_path.rs index fef0f41..c0914d0 100644 --- a/ion-schema/src/ion_path.rs +++ b/ion-schema/src/ion_path.rs @@ -1,5 +1,5 @@ -use ion_rs::element::{Element, SExp, Sequence}; use ion_rs::Symbol; +use ion_rs::{Element, SExp, Sequence}; use std::fmt; use std::fmt::{Debug, Formatter}; diff --git a/ion-schema/src/isl/isl_constraint.rs b/ion-schema/src/isl/isl_constraint.rs index f427283..a958d6d 100644 --- a/ion-schema/src/isl/isl_constraint.rs +++ b/ion-schema/src/isl/isl_constraint.rs @@ -6,13 +6,11 @@ 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, Value}; -use ion_rs::types::IntAccess; -use ion_rs::{IonType, IonWriter, Symbol}; +use ion_rs::{Element, Value}; +use ion_rs::{IonResult, SequenceWriter, StructWriter, ValueWriter, WriteAsIon}; +use ion_rs::{IonType, Symbol}; use std::collections::HashMap; use std::convert::TryInto; @@ -27,7 +25,7 @@ pub mod v_1_0 { use crate::isl::util::{Annotation, TimestampOffset, ValidValue}; use crate::isl::IslVersion; use crate::result::IonSchemaResult; - use ion_rs::element::Element; + use ion_rs::Element; /// 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 @@ -219,7 +217,7 @@ pub mod v_2_0 { 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::Element; /// 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 @@ -729,6 +727,7 @@ impl IslConstraintValue { "ordered_elements constraint was a null instead of a list", ); } + isl_require!(value.annotations().is_empty() => "ordered_elements list may not be annotated")?; if value.ion_type() != IonType::List { return invalid_schema_error(format!( "ordered_elements constraint was a {:?} instead of a list", @@ -960,148 +959,92 @@ impl IslConstraintValue { Ok(fields_map) } + + pub(crate) fn field_name(&self) -> &str { + match self { + IslConstraintValue::AllOf(_) => "all_of", + IslConstraintValue::Annotations(_) => "annotations", + IslConstraintValue::AnyOf(_) => "any_of", + IslConstraintValue::ByteLength(_) => "byte_length", + IslConstraintValue::CodepointLength(_) => "codepoint_length", + IslConstraintValue::Contains(_) => "contains", + IslConstraintValue::ContentClosed => "content", + IslConstraintValue::ContainerLength(_) => "container_length", + IslConstraintValue::Element(_, _) => "element", + IslConstraintValue::Exponent(_) => "exponent", + IslConstraintValue::Fields(_, _) => "fields", + IslConstraintValue::FieldNames(_, _) => "field_names", + IslConstraintValue::Ieee754Float(_) => "ieee754_float", + IslConstraintValue::Not(_) => "not", + IslConstraintValue::OneOf(_) => "one_of", + IslConstraintValue::OrderedElements(_) => "ordered_elements", + IslConstraintValue::Precision(_) => "precision", + IslConstraintValue::Regex(_) => "regex", + IslConstraintValue::Scale(_) => "scale", + IslConstraintValue::TimestampOffset(_) => "timestamp_offset", + IslConstraintValue::TimestampPrecision(_) => "timestamp_precision", + IslConstraintValue::Type(_) => "type", + IslConstraintValue::Unknown(field_name, _) => field_name.as_str(), + IslConstraintValue::Utf8ByteLength(_) => "utf8_byte_length", + IslConstraintValue::ValidValues(_) => "valid_values", + } + } } -impl WriteToIsl for IslConstraintValue { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for IslConstraintValue { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match self { - IslConstraintValue::AllOf(type_refs) => { - writer.set_field_name("all_of"); - writer.step_in(IonType::List)?; - for type_ref in type_refs { - type_ref.write_to(writer)?; - } - writer.step_out()?; - } - IslConstraintValue::Annotations(annotations) => { - annotations.write_to(writer)?; - } - IslConstraintValue::AnyOf(type_refs) => { - writer.set_field_name("any_of"); - writer.step_in(IonType::List)?; - for type_ref in type_refs { - type_ref.write_to(writer)?; - } - writer.step_out()?; - } - IslConstraintValue::ByteLength(range) => { - writer.set_field_name("byte_length"); - range.write_to(writer)?; - } - IslConstraintValue::CodepointLength(range) => { - writer.set_field_name("codepoint_length"); - range.write_to(writer)?; - } + IslConstraintValue::AllOf(type_refs) => writer.write(type_refs), + IslConstraintValue::Annotations(annotations) => writer.write(annotations), + IslConstraintValue::AnyOf(type_refs) => writer.write(type_refs), + IslConstraintValue::ByteLength(range) => writer.write(range), + IslConstraintValue::CodepointLength(range) => writer.write(range), IslConstraintValue::Contains(elements) => { - writer.set_field_name("contains"); - writer.step_in(IonType::List)?; + let mut list_writer = writer.list_writer()?; for element in elements { - writer.write_element(element)?; + list_writer.write(element)?; } - writer.step_out()?; - } - IslConstraintValue::ContentClosed => { - writer.set_field_name("content"); - writer.write_symbol("closed")?; - } - IslConstraintValue::ContainerLength(range) => { - writer.set_field_name("container_length"); - range.write_to(writer)?; + list_writer.close() } + IslConstraintValue::ContentClosed => writer.write_symbol("closed"), + IslConstraintValue::ContainerLength(range) => writer.write(range), IslConstraintValue::Element(type_ref, is_distinct) => { - writer.set_field_name("element"); if *is_distinct { - writer.set_annotations(["distinct"]); + writer.with_annotations(["distinct"])?.write(type_ref) + } else { + writer.write(type_ref) } - type_ref.write_to(writer)?; - } - IslConstraintValue::Exponent(range) => { - writer.set_field_name("exponent"); - range.write_to(writer)?; } + IslConstraintValue::Exponent(range) => writer.write(range), IslConstraintValue::Fields(fields, content_closed) => { - writer.set_field_name("fields"); - if *content_closed { - writer.set_annotations(["closed"]); - } - writer.step_in(IonType::Struct)?; + let annotations: &[&'static str] = if *content_closed { &["closed"] } else { &[] }; + let mut struct_writer = writer.with_annotations(annotations)?.struct_writer()?; for (field_name, type_ref) in fields.iter() { - writer.set_field_name(field_name); - type_ref.write_to(writer)?; + struct_writer.write(field_name.as_str(), type_ref)?; } - writer.step_out()?; + struct_writer.close() } IslConstraintValue::FieldNames(type_ref, is_distinct) => { - writer.set_field_name("field_names"); if *is_distinct { - writer.set_annotations(["distinct"]); - } - type_ref.write_to(writer)?; - } - IslConstraintValue::Ieee754Float(format) => { - writer.set_field_name("ieee754_float"); - format.write_to(writer)?; - } - IslConstraintValue::Not(type_ref) => { - writer.set_field_name("not"); - type_ref.write_to(writer)?; - } - IslConstraintValue::OneOf(type_refs) => { - writer.set_field_name("one_of"); - writer.step_in(IonType::List)?; - for type_ref in type_refs { - type_ref.write_to(writer)?; - } - writer.step_out()?; - } - IslConstraintValue::OrderedElements(type_refs) => { - writer.set_field_name("ordered_elements"); - writer.step_in(IonType::List)?; - for type_ref in type_refs { - type_ref.write_to(writer)?; - } - writer.step_out()?; - } - IslConstraintValue::Precision(range) => { - writer.set_field_name("precision"); - range.write_to(writer)?; - } - IslConstraintValue::Regex(regex) => { - regex.write_to(writer)?; - } - IslConstraintValue::Scale(range) => { - writer.set_field_name("scale"); - range.write_to(writer)?; - } - IslConstraintValue::TimestampOffset(timestamp_offset) => { - timestamp_offset.write_to(writer)?; - } - IslConstraintValue::TimestampPrecision(range) => { - writer.set_field_name("timestamp_precision"); - range.write_to(writer)?; - } - IslConstraintValue::Type(type_ref) => { - writer.set_field_name("type"); - type_ref.write_to(writer)?; - } - IslConstraintValue::Unknown(field_name, value) => { - writer.set_field_name(field_name); - writer.write_element(value)?; - } - IslConstraintValue::Utf8ByteLength(range) => { - writer.set_field_name("utf8_byte_length"); - range.write_to(writer)?; - } - IslConstraintValue::ValidValues(valid_values) => { - writer.set_field_name("valid_values"); - writer.step_in(IonType::List)?; - for valid_value in &valid_values.valid_values { - valid_value.write_to(writer)?; + writer.with_annotations(["distinct"])?.write(type_ref) + } else { + writer.write(type_ref) } - writer.step_out()?; } + IslConstraintValue::Ieee754Float(format) => writer.write(format), + IslConstraintValue::Not(type_ref) => writer.write(type_ref), + IslConstraintValue::OneOf(type_refs) => writer.write(type_refs), + IslConstraintValue::OrderedElements(type_refs) => writer.write(type_refs), + IslConstraintValue::Precision(range) => writer.write(range), + IslConstraintValue::Regex(regex) => writer.write(regex), + IslConstraintValue::Scale(range) => writer.write(range), + IslConstraintValue::TimestampOffset(timestamp_offset) => writer.write(timestamp_offset), + IslConstraintValue::TimestampPrecision(range) => writer.write(range), + IslConstraintValue::Type(type_ref) => writer.write(type_ref), + IslConstraintValue::Unknown(field_name, value) => writer.write(value), + IslConstraintValue::Utf8ByteLength(range) => writer.write(range), + IslConstraintValue::ValidValues(valid_values) => writer.write(valid_values), } - Ok(()) } } @@ -1114,18 +1057,12 @@ pub enum IslAnnotationsConstraint { StandardAnnotations(IslTypeRef), } -impl WriteToIsl for IslAnnotationsConstraint { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_field_name("annotations"); +impl WriteAsIon for IslAnnotationsConstraint { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match self { - IslAnnotationsConstraint::SimpleAnnotations(simple_annotations) => { - simple_annotations.write_to(writer)?; - } - IslAnnotationsConstraint::StandardAnnotations(standard_annotations) => { - standard_annotations.write_to(writer)?; - } + IslAnnotationsConstraint::SimpleAnnotations(ann) => writer.write(ann), + IslAnnotationsConstraint::StandardAnnotations(ann) => writer.write(ann), } - Ok(()) } } @@ -1247,8 +1184,8 @@ impl IslSimpleAnnotationsConstraint { } } -impl WriteToIsl for IslSimpleAnnotationsConstraint { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for IslSimpleAnnotationsConstraint { + fn write_as_ion(&self, writer: V) -> IonResult<()> { let mut annotation_modifiers = vec![]; if self.is_ordered { annotation_modifiers.push("ordered"); @@ -1259,13 +1196,9 @@ impl WriteToIsl for IslSimpleAnnotationsConstraint { if self.is_required { annotation_modifiers.push("required"); } - writer.set_annotations(annotation_modifiers); - writer.step_in(IonType::List)?; - for annotation in &self.annotations { - annotation.write_to(writer)?; - } - writer.step_out()?; - Ok(()) + writer + .with_annotations(annotation_modifiers)? + .write(&self.annotations) } } @@ -1304,6 +1237,16 @@ impl IslValidValuesConstraint { } } +impl WriteAsIon for IslValidValuesConstraint { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut list_writer = writer.list_writer()?; + for vv in self.values() { + list_writer.write(vv)?; + } + list_writer.close() + } +} + /// Represents the [regex] constraint /// /// [regex]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#regex @@ -1336,9 +1279,8 @@ impl IslRegexConstraint { } } -impl WriteToIsl for IslRegexConstraint { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_field_name("regex"); +impl WriteAsIon for IslRegexConstraint { + fn write_as_ion(&self, writer: V) -> IonResult<()> { let mut regex_modifiers = vec![]; if self.case_insensitive { regex_modifiers.push("i"); @@ -1346,9 +1288,9 @@ impl WriteToIsl for IslRegexConstraint { if self.multi_line { regex_modifiers.push("m"); } - writer.set_annotations(regex_modifiers); - writer.write_string(&self.expression)?; - Ok(()) + writer + .with_annotations(regex_modifiers)? + .write_string(&self.expression) } } @@ -1370,14 +1312,12 @@ impl IslTimestampOffsetConstraint { } } -impl WriteToIsl for IslTimestampOffsetConstraint { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_field_name("timestamp_offset"); - writer.step_in(IonType::List)?; +impl WriteAsIon for IslTimestampOffsetConstraint { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut list_writer = writer.list_writer()?; for timestamp_offset in &self.valid_offsets { - timestamp_offset.write_to(writer)?; + list_writer.write(timestamp_offset)?; } - writer.step_out()?; - Ok(()) + list_writer.close() } } diff --git a/ion-schema/src/isl/isl_import.rs b/ion-schema/src/isl/isl_import.rs index 027989a..ac67f7c 100644 --- a/ion-schema/src/isl/isl_import.rs +++ b/ion-schema/src/isl/isl_import.rs @@ -1,7 +1,5 @@ -use crate::isl::WriteToIsl; use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult}; -use ion_rs::element::Element; -use ion_rs::{IonType, IonWriter}; +use ion_rs::{Element, IonResult, StructWriter, Symbol, ValueWriter, WriteAsIon}; /// Represents an [import] in an ISL schema. /// @@ -58,23 +56,20 @@ impl IslImport { } } -impl WriteToIsl for IslImport { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.step_in(IonType::Struct)?; +impl WriteAsIon for IslImport { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match self { IslImport::Schema(schema_import) => { - writer.set_field_name("id"); - writer.write_string(schema_import)?; + let mut struct_writer = writer.struct_writer()?; + struct_writer + .field_writer("id") + .write_string(schema_import)?; + struct_writer.close() } - IslImport::Type(type_import) => { - type_import.write_to(writer)?; - } - IslImport::TypeAlias(type_alias_import) => { - type_alias_import.write_to(writer)?; + IslImport::Type(type_import) | IslImport::TypeAlias(type_import) => { + writer.write(type_import) } } - writer.step_out()?; - Ok(()) } } @@ -110,16 +105,17 @@ impl IslImportType { } } -impl WriteToIsl for IslImportType { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_field_name("id"); - writer.write_symbol(&self.id)?; - writer.set_field_name("type"); - writer.write_symbol(&self.type_name)?; +impl WriteAsIon for IslImportType { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut struct_writer = writer.struct_writer()?; + + struct_writer.field_writer("id").write_string(&self.id)?; + struct_writer + .field_writer("type") + .write_symbol(self.type_name.as_str())?; if let Some(alias) = &self.alias { - writer.set_field_name("as"); - writer.write_symbol(alias)?; + struct_writer.write("as", Symbol::from(alias))?; } - Ok(()) + struct_writer.close() } } diff --git a/ion-schema/src/isl/isl_type.rs b/ion-schema/src/isl/isl_type.rs index c63e8a0..f9bb972 100644 --- a/ion-schema/src/isl/isl_type.rs +++ b/ion-schema/src/isl/isl_type.rs @@ -1,10 +1,8 @@ use crate::isl::isl_constraint::{IslConstraint, IslConstraintValue}; use crate::isl::isl_import::IslImportType; use crate::isl::IslVersion; -use crate::isl::WriteToIsl; use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult}; -use ion_rs::element::Element; -use ion_rs::{IonType, IonWriter}; +use ion_rs::{Element, IonResult, StructWriter, ValueWriter, WriteAsIon}; /// Provides public facing APIs for constructing ISL types programmatically for ISL 1.0 pub mod v_1_0 { @@ -12,7 +10,7 @@ pub mod v_1_0 { use crate::isl::isl_type::IslType; use crate::isl::IslVersion; use crate::result::IonSchemaResult; - use ion_rs::element::Element; + use ion_rs::Element; /// Creates a named [IslType] using the [IslConstraint] defined within it pub fn named_type, B: Into>>( @@ -50,7 +48,7 @@ pub mod v_2_0 { use crate::isl::isl_type::{v_1_0, IslType}; use crate::isl::IslVersion; use crate::result::IonSchemaResult; - use ion_rs::element::Element; + use ion_rs::Element; /// Creates a named [IslType] using the [IslConstraint] defined within it pub fn named_type, B: Into>>( @@ -101,8 +99,8 @@ impl IslType { } } - pub fn name(&self) -> &Option { - &self.name + pub fn name(&self) -> Option<&str> { + self.name.as_deref() } pub fn constraints(&self) -> &[IslConstraint] { @@ -217,23 +215,23 @@ impl IslType { } } -impl WriteToIsl for IslType { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_annotations(["type"]); - writer.step_in(IonType::Struct)?; - writer.set_field_name("name"); - let _ = self - .name - .as_ref() - .map(|name| writer.write_symbol(name)) - .ok_or(invalid_schema_error_raw( - "Top level type definitions must contain a `name` field", - ))?; +impl WriteAsIon for IslType { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut struct_writer = writer.with_annotations(["type"])?.struct_writer()?; + + if let Some(name) = self.name.as_ref() { + struct_writer + .field_writer("name") + .write_symbol(name.as_str())?; + } + for constraint in self.constraints() { - constraint.constraint_value.write_to(writer)?; + let constraint_name = constraint.constraint().field_name(); + struct_writer + .field_writer(constraint_name) + .write(constraint.constraint())?; } - writer.step_out()?; - Ok(()) + struct_writer.close() } } diff --git a/ion-schema/src/isl/isl_type_reference.rs b/ion-schema/src/isl/isl_type_reference.rs index c4eab76..8adbe7b 100644 --- a/ion-schema/src/isl/isl_type_reference.rs +++ b/ion-schema/src/isl/isl_type_reference.rs @@ -3,7 +3,6 @@ use crate::isl::isl_import::{IslImport, IslImportType}; use crate::isl::isl_type::IslType; 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, @@ -11,8 +10,8 @@ use crate::result::{ use crate::system::{PendingTypes, TypeId, TypeStore}; use crate::type_reference::{TypeReference, VariablyOccurringTypeRef}; use crate::types::TypeDefinitionImpl; -use ion_rs::element::{Element, Value}; -use ion_rs::{IonType, IonWriter}; +use ion_rs::IonType; +use ion_rs::{Element, IonResult, StructWriter, Value, ValueWriter, WriteAsIon}; /// Provides public facing APIs for constructing ISL type references programmatically for ISL 1.0 pub mod v_1_0 { @@ -91,18 +90,13 @@ pub enum NullabilityModifier { Nothing, // Represents that no modifiers were provided with the type reference } -impl WriteToIsl for NullabilityModifier { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl NullabilityModifier { + fn as_annotations(&self) -> &'static [&'static str] { match self { - NullabilityModifier::Nullable => { - writer.set_annotations(["nullable"]); - } - NullabilityModifier::NullOr => { - writer.set_annotations(["$null_or"]); - } - NullabilityModifier::Nothing => {} + NullabilityModifier::Nullable => &["nullable"], + NullabilityModifier::NullOr => &["$null_or"], + NullabilityModifier::Nothing => &[], } - Ok(()) } } @@ -335,29 +329,19 @@ impl IslTypeRef { } } -impl WriteToIsl for IslTypeRef { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for IslTypeRef { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match self { - IslTypeRef::Named(name, nullability_modifier) => { - nullability_modifier.write_to(writer)?; - writer.write_symbol(name)?; - } - IslTypeRef::TypeImport(type_import, nullability_modifier) => { - nullability_modifier.write_to(writer)?; - writer.step_in(IonType::Struct)?; - type_import.write_to(writer)?; - writer.step_out()?; - } - IslTypeRef::Anonymous(type_def, nullability_modifier) => { - nullability_modifier.write_to(writer)?; - writer.step_in(IonType::Struct)?; - for constraint in type_def.constraints() { - constraint.constraint_value.write_to(writer)?; - } - writer.step_out()?; - } + IslTypeRef::Named(name, nullability_modifier) => writer + .with_annotations(nullability_modifier.as_annotations())? + .write_symbol(name.as_str()), + IslTypeRef::TypeImport(type_import, nullability_modifier) => writer + .with_annotations(nullability_modifier.as_annotations())? + .write(type_import), + IslTypeRef::Anonymous(type_def, nullability_modifier) => writer + .with_annotations(nullability_modifier.as_annotations())? + .write(type_def), } - Ok(()) } } @@ -475,32 +459,27 @@ impl IslVariablyOccurringTypeRef { } } -impl WriteToIsl for IslVariablyOccurringTypeRef { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for IslVariablyOccurringTypeRef { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match &self.type_ref { - IslTypeRef::Named(name, nullability_modifier) => { - nullability_modifier.write_to(writer)?; - writer.write_symbol(name)?; - } - IslTypeRef::TypeImport(type_import, nullability_modifier) => { - nullability_modifier.write_to(writer)?; - writer.step_in(IonType::Struct)?; - type_import.write_to(writer)?; - writer.set_field_name("occurs"); - self.occurs.write_to(writer)?; - writer.step_out()?; - } + IslTypeRef::Named(name, nullability_modifier) => writer + .with_annotations(nullability_modifier.as_annotations())? + .write_symbol(name.as_str()), + IslTypeRef::TypeImport(type_import, nullability_modifier) => writer + .with_annotations(nullability_modifier.as_annotations())? + .write(type_import), IslTypeRef::Anonymous(type_def, nullability_modifier) => { - nullability_modifier.write_to(writer)?; - writer.step_in(IonType::Struct)?; + let mut struct_writer = writer + .with_annotations(nullability_modifier.as_annotations())? + .struct_writer()?; + for constraint in type_def.constraints() { - constraint.constraint_value.write_to(writer)?; + let constraint_value = constraint.constraint(); + struct_writer.write(constraint_value.field_name(), constraint_value)?; } - writer.set_field_name("occurs"); - self.occurs.write_to(writer)?; - writer.step_out()?; + struct_writer.write("occurs", &self.occurs)?; + struct_writer.close() } } - Ok(()) } } diff --git a/ion-schema/src/isl/mod.rs b/ion-schema/src/isl/mod.rs index 0c0fae0..6831f2c 100644 --- a/ion-schema/src/isl/mod.rs +++ b/ion-schema/src/isl/mod.rs @@ -71,8 +71,9 @@ //! //! ## Example of serializing a programmatically constructed schema into a schema file: //! ``` -//! use ion_rs::{IonWriter, TextWriterBuilder}; -//! use ion_schema::isl::{isl_type::v_1_0::*, isl_constraint::v_1_0::*, isl_type_reference::v_1_0::*, IslSchema, WriteToIsl}; +//! use ion_rs::{Writer, WriteConfig, TextFormat, SequenceWriter}; +//! use ion_rs::v1_0::Text; +//! use ion_schema::isl::{isl_type::v_1_0::*, isl_constraint::v_1_0::*, isl_type_reference::v_1_0::*, IslSchema}; //! use ion_schema::schema::Schema; //! use ion_schema::system::SchemaSystem; //! @@ -104,10 +105,10 @@ //! //! // initiate an Ion pretty text writer //! let mut buffer = Vec::new(); -//! let mut writer = TextWriterBuilder::pretty().build(&mut buffer).unwrap(); +//! let mut writer = Writer::new(Text, buffer).unwrap(); //! //! // write the previously constructed ISL model into a schema file using `write_to` -//! let write_schema_result = isl_schema.write_to(&mut writer); +//! let write_schema_result = writer.write_all(&isl_schema); //! assert!(write_schema_result.is_ok()); //! //! // The above written schema file looks like following: @@ -138,11 +139,10 @@ use crate::isl::isl_import::{IslImport, IslImportType}; use crate::isl::isl_type::IslType; -use crate::result::IonSchemaResult; use crate::UserReservedFields; -use ion_rs::element::writer::ElementWriter; -use ion_rs::element::Element; -use ion_rs::{IonType, IonWriter}; +use ion_rs::Element; +use ion_rs::{IonResult, SequenceWriter, StructWriter, ValueWriter, WriteAsIon}; +use std::collections::HashMap; use std::fmt::{Display, Formatter}; pub mod isl_constraint; @@ -160,6 +160,16 @@ pub enum IslVersion { V2_0, } +impl WriteAsIon for IslVersion { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let text = match self { + IslVersion::V1_0 => "$ion_schema_1_0", + IslVersion::V2_0 => "$ion_schema_2_0", + }; + writer.write_symbol(text) + } +} + impl Display for IslVersion { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( @@ -173,48 +183,219 @@ impl Display for IslVersion { } } -/// Provides a method to write an ISL model using an Ion writer -pub trait WriteToIsl { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()>; +/// Models the content that could be in a Schema document. +#[derive(Debug, Clone, PartialEq)] +pub enum SchemaContent { + Version(IslVersion), + Header(SchemaHeader), + Type(IslType), + Footer(SchemaFooter), + OpenContent(Element), +} +impl From for SchemaContent { + fn from(value: IslVersion) -> Self { + SchemaContent::Version(value) + } +} +impl From for SchemaContent { + fn from(value: SchemaHeader) -> Self { + SchemaContent::Header(value) + } +} +impl From for SchemaContent { + fn from(value: IslType) -> Self { + SchemaContent::Type(value) + } +} +impl From for SchemaContent { + fn from(value: SchemaFooter) -> Self { + SchemaContent::Footer(value) + } } -/// Provides an internal representation of an schema file -#[derive(Debug, Clone, PartialEq)] -pub struct IslSchema { - /// Represents an id for the given ISL model - id: String, - /// Represents the ISL version for given schema - version: IslVersion, +impl SchemaContent { + fn as_header(&self) -> Option<&SchemaHeader> { + if let SchemaContent::Header(schema_header) = self { + Some(schema_header) + } else { + None + } + } + fn as_isl_version(&self) -> Option<&IslVersion> { + if let SchemaContent::Version(value) = self { + Some(value) + } else { + None + } + } + fn as_type(&self) -> Option<&IslType> { + if let SchemaContent::Type(value) = self { + Some(value) + } else { + None + } + } + fn as_open_content(&self) -> Option<&Element> { + if let SchemaContent::OpenContent(value) = self { + Some(value) + } else { + None + } + } + fn as_footer(&self) -> Option<&SchemaFooter> { + if let SchemaContent::Footer(value) = self { + Some(value) + } else { + None + } + } + fn expect_header(&self) -> &SchemaHeader { + let SchemaContent::Header(value) = self else { + unreachable!("expected to find a Header, but found: {:?}", self) + }; + value + } + fn expect_isl_version(&self) -> &IslVersion { + let SchemaContent::Version(value) = self else { + unreachable!("expected to find an IslVersion, but found: {:?}", self) + }; + value + } + fn expect_type(&self) -> &IslType { + let SchemaContent::Type(value) = self else { + unreachable!("expected to find a Type, but found: {:?}", self) + }; + value + } + fn expect_open_content(&self) -> &Element { + let SchemaContent::OpenContent(value) = self else { + unreachable!("expected to find OpenContent, but found: {:?}", self) + }; + value + } + fn expect_footer(&self) -> &SchemaFooter { + let SchemaContent::Footer(value) = self else { + unreachable!("expected to find a Footer, but found: {:?}", self) + }; + value + } +} + +impl WriteAsIon for SchemaContent { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + match self { + SchemaContent::Version(value) => writer.write(value), + SchemaContent::Header(value) => writer.write(value), + SchemaContent::Type(value) => writer.write(value), + SchemaContent::Footer(value) => writer.write(value), + SchemaContent::OpenContent(value) => writer.write(value), + } + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +struct SchemaHeader { /// Represents the user defined reserved fields - /// For ISL 2.0 this contains the use reserved fields that are defined within schema header, - /// Otherwise, it is None. - user_reserved_fields: Option, + user_reserved_fields: UserReservedFields, /// Represents all the IslImports inside the schema file. /// For more information: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#imports imports: Vec, - /// Represents all the IslType defined in this schema file. - /// For more information: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#type-definitions - types: Vec, + /// User-defined (aka "open") content + user_content: Vec<(String, Element)>, +} + +impl WriteAsIon for SchemaHeader { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut struct_writer = writer + .with_annotations(["schema_header"])? + .struct_writer()?; + if !self.imports.is_empty() { + struct_writer.field_writer("imports").write(&self.imports)?; + } + if !self.user_reserved_fields.is_empty() { + struct_writer + .field_writer("user_reserved_fields") + .write(&self.user_reserved_fields)?; + } + if !self.user_content.is_empty() { + for (k, v) in &self.user_content { + struct_writer.write(k.as_str(), v)?; + } + } + struct_writer.close() + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +struct SchemaFooter { + /// User-defined (aka "open") content + user_content: Vec<(String, Element)>, +} + +impl WriteAsIon for SchemaFooter { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut struct_writer = writer + .with_annotations(["schema_footer"])? + .struct_writer()?; + if !self.user_content.is_empty() { + for (k, v) in &self.user_content { + struct_writer.write(k.as_str(), v)?; + } + } + struct_writer.close() + } +} + +/// Provides an internal representation of a schema file +#[derive(Debug, Clone, PartialEq)] +pub struct IslSchema { + /// Represents an id for the given ISL model + id: String, + /// Content of the schema document. + schema_content: Vec, + /// Position of the header within [schema_content]. + header_index: Option, + /// Position of the footer within [schema_content]. + footer_index: Option, + /// Position of all types defined in this schema file. + types_by_name: HashMap, + /// Represents all the inline IslImportTypes in this schema file. inline_imported_types: Vec, - /// Represents open content as `Element`s - /// Note: This doesn't preserve the information about where the open content is placed in the schema file. - /// e.g. This method doesn't differentiate between following schemas with open content: - /// ```ion - /// $foo - /// $bar - /// type::{ name: foo, codepoint_length: 1 } - /// ``` - /// - /// ```ion - /// type::{ name: foo, codepoint_length: 1 } - /// $foo - /// $bar - /// ``` - open_content: Vec, } impl IslSchema { + fn new_from_content_vec( + id: String, + schema_content: Vec, + inline_imported_types: Vec, + ) -> Self { + assert!(matches!(schema_content[0], SchemaContent::Version(_))); + let header_index = schema_content + .iter() + .position(|x| matches!(x, SchemaContent::Header(_))); + let footer_index = schema_content + .iter() + .position(|x| matches!(x, SchemaContent::Footer(_))); + // TODO: Ensure that ISL 1.0 schemas have footer iff header + let types_by_name: HashMap = schema_content + .iter() + .enumerate() + .filter(|&(_, x)| matches!(x, SchemaContent::Type(_))) + .map(|(i, t)| (t.expect_type().name().unwrap().to_string(), i)) + .collect(); + + Self { + id, + schema_content, + header_index, + footer_index, + types_by_name, + inline_imported_types, + } + } + + /// TODO: Replace with a builder pub(crate) fn new>( id: A, version: IslVersion, @@ -224,18 +405,27 @@ impl IslSchema { inline_imports: Vec, open_content: Vec, ) -> Self { - Self { - id: id.as_ref().to_owned(), - version, - user_reserved_fields, - imports, - types, - inline_imported_types: inline_imports, - open_content, + let mut schema_content: Vec = vec![]; + schema_content.push(version.into()); + schema_content.push( + SchemaHeader { + user_reserved_fields: user_reserved_fields.unwrap_or_default(), + imports, + user_content: vec![], + } + .into(), + ); + for t in types { + schema_content.push(t.into()); + } + for e in open_content { + schema_content.push(SchemaContent::OpenContent(e)); } + Self::new_from_content_vec(id.as_ref().to_string(), schema_content, inline_imports) } /// Creates an ISL schema using the [IslType]s, [IslImport]s, open content and schema id + /// TODO: Replace with a builder pub fn schema_v_1_0>( id: A, imports: Vec, @@ -255,6 +445,7 @@ impl IslSchema { } /// Creates an ISL schema using the [IslType]s, [IslImport]s, [UserReservedFields] open content and schema id + /// TODO: Replace with a builder pub fn schema_v_2_0>( id: A, user_reserved_fields: UserReservedFields, @@ -279,80 +470,87 @@ impl IslSchema { } pub fn version(&self) -> IslVersion { - self.version + *(self.schema_content[0].expect_isl_version()) + } + + fn header(&self) -> Option<&SchemaHeader> { + self.header_index + .map(|i| self.schema_content.get(i).unwrap()) + .map(SchemaContent::expect_header) } - pub fn imports(&self) -> &[IslImport] { - &self.imports + fn footer(&self) -> Option<&SchemaFooter> { + self.footer_index + .map(|i| self.schema_content.get(i).unwrap()) + .map(SchemaContent::expect_footer) } - pub fn types(&self) -> &[IslType] { - &self.types + const EMPTY_VEC: Vec = vec![]; + const EMPTY_VEC_REF: &'static Vec = &Self::EMPTY_VEC; + + pub fn imports(&self) -> impl Iterator { + self.header() + .map(|x| &x.imports) + .unwrap_or(Self::EMPTY_VEC_REF) + .iter() } - pub fn inline_imported_types(&self) -> &[IslImportType] { - &self.inline_imported_types + pub fn types(&self) -> impl Iterator { + self.schema_content.iter().filter_map(|x| x.as_type()) + } + + pub fn inline_imported_types(&self) -> impl Iterator { + self.inline_imported_types.iter() } /// Provides top level open content for given schema /// For open content defined within type definitions use IslType#open_content() - pub fn open_content(&self) -> &Vec { - &self.open_content + pub fn open_content(&self) -> impl Iterator { + self.schema_content + .iter() + .filter_map(|x| x.as_open_content()) } /// Provide user reserved field defined in the given schema for ISL 2.0, /// Otherwise returns None pub fn user_reserved_fields(&self) -> Option<&UserReservedFields> { - self.user_reserved_fields.as_ref() + self.header().map(|x| &x.user_reserved_fields) } +} - fn write_header(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.set_annotations(["schema_header"]); - writer.step_in(IonType::Struct)?; - if !self.imports.is_empty() { - writer.set_field_name("imports"); - writer.step_in(IonType::List)?; - for import in &self.imports { - import.write_to(writer)?; - } - writer.step_out()?; - } - if let Some(user_reserved_fields) = &self.user_reserved_fields { - user_reserved_fields.write_to(writer)?; +impl IslSchema { + fn write_as_ion(&self, writer: &mut W) -> IonResult<()> { + for item in &self.schema_content { + writer.write(item)?; } - writer.step_out()?; - Ok(()) } } -impl WriteToIsl for IslSchema { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - let version = self.version; - // write the version marker for given schema - match version { - IslVersion::V1_0 => { - writer.write_symbol("$ion_schema_1_0")?; - } - IslVersion::V2_0 => { - writer.write_symbol("$ion_schema_2_0")?; - } - } - self.write_header(writer)?; - for isl_type in &self.types { - isl_type.write_to(writer)?; - } - // write open content at the end of the schema - for value in &self.open_content { - writer.write_element(value)?; +impl<'a> IntoIterator for &'a IslSchema { + type Item = &'a SchemaContent; + type IntoIter = SchemaIterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + SchemaIterator { + content: &self.schema_content, + i: 0, } + } +} - // write footer for given schema - writer.set_annotations(["schema_footer"]); - writer.step_in(IonType::Struct)?; - writer.step_out()?; - writer.flush()?; - Ok(()) +pub struct SchemaIterator<'a> { + content: &'a Vec, + i: usize, +} + +impl<'a> Iterator for SchemaIterator<'a> { + type Item = &'a SchemaContent; + + fn next(&mut self) -> Option { + let next_item = self.content.get(self.i); + self.i += 1; + next_item } } @@ -373,11 +571,11 @@ mod isl_tests { use crate::isl::*; use crate::result::IonSchemaResult; use crate::system::SchemaSystem; - use ion_rs::element::Element; - use ion_rs::types::Decimal; + use ion_rs::v1_0; + use ion_rs::Decimal; use ion_rs::IonType; use ion_rs::Symbol; - use ion_rs::TextWriterBuilder; + use ion_rs::{Element, TextFormat, WriteConfig, Writer}; use rstest::*; use std::path::Path; use test_generator::test_resources; @@ -695,9 +893,9 @@ mod isl_tests { #[test_resources("ion-schema-tests/**/*.isl")] #[test_resources("ion-schema-schemas/**/*.isl")] - fn test_write_to_isl(file_name: &str) { + fn test_write_to_isl(file_name: &str) -> IonSchemaResult<()> { if is_skip_list_path(file_name) { - return; + return Ok(()); } // creates a schema system that will be sued to load schema files into a schema model @@ -707,26 +905,25 @@ mod isl_tests { ))]); // construct an ISL model from a schema file - let expected_schema_result = schema_system.load_isl_schema(file_name); - assert!(expected_schema_result.is_ok()); - let expected_schema = expected_schema_result.unwrap(); + let expected_schema = schema_system.load_isl_schema(file_name)?; // initiate an Ion pretty text writer - let mut buffer = Vec::new(); - let mut writer = TextWriterBuilder::pretty().build(&mut buffer).unwrap(); + let buffer = Vec::new(); + let config = WriteConfig::::new(TextFormat::Pretty); + let mut writer = Writer::new(config, buffer)?; // write the previously constructed ISL model into a schema file using `write_to` - let write_schema_result = expected_schema.write_to(&mut writer); + let write_schema_result = expected_schema.write_as_ion(&mut writer); assert!(write_schema_result.is_ok()); + let output = writer.close()?; + // create a new schema model from the schema file generated by the previous step - let actual_schema_result = - schema_system.new_isl_schema(writer.output().as_slice(), file_name); - assert!(actual_schema_result.is_ok()); - let actual_schema = actual_schema_result.unwrap(); + let actual_schema = schema_system.new_isl_schema(output.as_slice(), file_name)?; // compare the actual schema model that was generated from dynamically created schema file // with the expected schema model that was constructed from given schema file assert_eq!(actual_schema, expected_schema); + Ok(()) } } diff --git a/ion-schema/src/isl/ranges.rs b/ion-schema/src/isl/ranges.rs index 9925133..047c6ff 100644 --- a/ion-schema/src/isl/ranges.rs +++ b/ion-schema/src/isl/ranges.rs @@ -17,13 +17,13 @@ use crate::isl::ranges::base::RangeValidation; use crate::isl::util::TimestampPrecision; +use crate::IonSchemaResult; 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 ion_rs::{Element, IonResult, ValueWriter, WriteAsIon}; +use ion_rs::{IonType, Timestamp}; use num_traits::{CheckedAdd, One}; +use std::cmp::Ordering; use std::fmt::{Display, Formatter}; /// An end (upper or lower) of a [`Range`]. @@ -38,44 +38,62 @@ pub enum Limit { /// /// `Unbounded` is represented in Ion Schema Language as `min` or `max`, depending on the /// position in which it occurs. - Unbounded, + Min, + Max, /// 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(); +impl PartialEq for Limit { + fn eq(&self, other: &T) -> bool { match self { - Limit::Unbounded => true, - Limit::Exclusive(this) => this > other, - Limit::Inclusive(this) => this >= other, + Limit::Inclusive(x) => x == other, + _ => false, } } +} - /// 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(); +impl PartialOrd for Limit { + fn partial_cmp(&self, other: &T) -> Option { match self { - Limit::Unbounded => true, - Limit::Exclusive(this) => this < other, - Limit::Inclusive(this) => this <= other, + Limit::Min => Some(Ordering::Less), + Limit::Max => Some(Ordering::Greater), + Limit::Inclusive(x) => x.partial_cmp(other), + Limit::Exclusive(x) => { + // Exclusive limits can never be equal—only lesser or greater + match x.partial_cmp(other) { + Some(Ordering::Equal) => None, + order => order, + } + } } } } -impl Limit { - fn fmt_for_display(&self, f: &mut Formatter<'_>, unbounded_text: &str) -> std::fmt::Result { + +impl Display for Limit { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Limit::Unbounded => f.write_str(unbounded_text), + Limit::Min => "min".fmt(f), + Limit::Max => "max".fmt(f), Limit::Inclusive(value) => value.fmt(f), Limit::Exclusive(value) => write!(f, "exclusive::{value}"), } } } +impl WriteAsIon for Limit { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + match self { + Limit::Min => writer.write_symbol("min"), + Limit::Max => writer.write_symbol("max"), + Limit::Inclusive(t) => writer.write(t), + Limit::Exclusive(t) => writer.with_annotations(["exclusive"])?.write(t), + } + } +} + pub type UsizeRange = base::Range; impl RangeValidation for UsizeRange { fn is_empty(start: &Limit, end: &Limit) -> bool { @@ -98,14 +116,16 @@ impl UsizeRange { // 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::Min => 0, Limit::Inclusive(x) => *x, Limit::Exclusive(x) => x + 1, + Limit::Max => unreachable!(), }; let upper = match self.upper() { - Limit::Unbounded => usize::MAX, + Limit::Max => usize::MAX, Limit::Inclusive(x) => *x, Limit::Exclusive(x) => x - 1, + Limit::Min => unreachable!(), }; (lower, upper) } @@ -151,76 +171,6 @@ 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::*; @@ -283,8 +233,10 @@ mod base { /// 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) => "")?; + isl_require!(start != Limit::Min || end != Limit::Max => "range may not contain both 'min' and 'max'")?; + isl_require!(start != Limit::Max => "range may not be empty (start of range may not be 'max')")?; + isl_require!(end != Limit::Min => "range may not be empty (end of range cannot be 'min')")?; + isl_require!(!Self::is_empty(&start, &end) => "range may not be empty")?; Ok(Self { lower: start, upper: end, @@ -313,7 +265,8 @@ mod base { /// 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) + let other = value.clone().into(); + self.lower <= other && self.upper >= other } /// Reads a [`Range`] from an [`Element`] of Ion Schema Language. @@ -327,10 +280,8 @@ mod base { 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)?; + let lower_limit = Self::read_range_bound(element, seq.get(0).unwrap(), &value_fn)?; + let upper_limit = Self::read_range_bound(element, seq.get(1).unwrap(), &value_fn)?; Self::new(lower_limit, upper_limit) } else { @@ -346,27 +297,33 @@ mod base { 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) + let is_exclusive = boundary_element.annotations().contains("exclusive"); + isl_require!(boundary_element.annotations().len() == if is_exclusive {1} else {0} => "invalid annotation(s) on range boundary {element}")?; + + match boundary_element.as_symbol().map(|s| s.text()) { + Some(Some("min")) => { + isl_require!(!is_exclusive => "'min' may not be exclusive")?; + Ok(Limit::Min) } - }; - Ok(limit) + Some(Some("max")) => { + isl_require!(!is_exclusive => "'max' may not be exclusive")?; + Ok(Limit::Max) + } + _ => { + let limit_value: T = value_fn(boundary_element).ok_or_else(|| { + invalid_schema_error_raw(format!( + "invalid value for range boundary: {element}" + )) + })?; + if is_exclusive { + Ok(Limit::Exclusive(limit_value)) + } else { + Ok(Limit::Inclusive(limit_value)) + } + } + } } } @@ -379,50 +336,22 @@ mod base { } } - impl + Clone + PartialEq> WriteToIsl for &Range { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + impl Display for Range { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 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(()) - } + Limit::Inclusive(value) if self.lower == self.upper => value.fmt(f), + _ => f.write_fmt(format_args!("range::[{},{}]", self.lower, self.upper)), } } } - impl Display for Range { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + impl WriteAsIon for Range { + fn write_as_ion(&self, writer: V) -> IonResult<()> { 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("]") - } + Limit::Inclusive(value) if self.lower == self.upper => writer.write(value), + _ => writer + .with_annotations(["range"])? + .write([&self.lower, &self.upper]), } } } @@ -436,8 +365,7 @@ mod 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 ion_rs::Element; use rstest::*; #[rstest( @@ -458,7 +386,7 @@ mod tests { } #[rstest( - case::range_with_both_limits_unbounded(Range::new(Limit::Unbounded, Limit::Unbounded)), + case::range_with_both_limits_unbounded(Range::new(Limit::Min, Limit::Max)), case::range_with_lower_bound_greater_than_upper_bound(Range::new( Limit::Inclusive(3), Limit::Inclusive(1) @@ -479,12 +407,12 @@ mod tests { #[rstest( case::lower_is_unbounded( - Range::new(Limit::Unbounded, Limit::Inclusive(5)), + Range::new(Limit::Min, Limit::Inclusive(5)), vec![-128, 0, 5], vec![6, 127], ), case::upper_is_unbounded( - Range::new(Limit::Inclusive(5), Limit::Unbounded), + Range::new(Limit::Inclusive(5), Limit::Max), vec![5, 6, 127], vec![-128, 0, 4], ), @@ -504,20 +432,27 @@ mod tests { #[case] valid_values: Vec, #[case] invalid_values: Vec, ) { + let range = range.unwrap(); for valid_value in valid_values { - let range_contains_result = range.as_ref().unwrap().contains(&valid_value); - assert!(range_contains_result) + let range_contains_result = range.contains(&valid_value); + assert!( + range_contains_result, + "Value {valid_value} was not in {range}" + ) } for invalid_value in invalid_values { - let range_contains_result = range.as_ref().unwrap().contains(&invalid_value); - assert!(!range_contains_result) + let range_contains_result = range.contains(&invalid_value); + assert!( + !range_contains_result, + "Value {invalid_value} was unexpectedly in {range}" + ) } } #[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_is_unbounded("range::[min,1]", Range::new(Limit::Min, Limit::Inclusive(1)).unwrap()), + case::upper_is_unbounded("range::[1,max]", Range::new(Limit::Inclusive(1), Limit::Max).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()), diff --git a/ion-schema/src/isl/util.rs b/ion-schema/src/isl/util.rs index 82bb15b..1985750 100644 --- a/ion-schema/src/isl/util.rs +++ b/ion-schema/src/isl/util.rs @@ -1,12 +1,11 @@ use crate::ion_extension::ElementExtensions; use crate::isl::ranges::{NumberRange, TimestampRange}; -use crate::isl::{IslVersion, WriteToIsl}; +use crate::isl::IslVersion; use crate::isl_require; use crate::result::{invalid_schema_error, IonSchemaError, IonSchemaResult}; -use ion_rs::element::writer::ElementWriter; -use ion_rs::element::{Element, Value}; -use ion_rs::types::Precision; -use ion_rs::{IonType, IonWriter, Timestamp}; +use ion_rs::TimestampPrecision as Precision; +use ion_rs::{Element, IonResult, Value, ValueWriter, WriteAsIon}; +use ion_rs::{IonType, Timestamp}; use num_traits::abs; use std::cmp::Ordering; use std::fmt; @@ -57,17 +56,21 @@ impl Annotation { } } -impl WriteToIsl for Annotation { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for Annotation { + fn write_as_ion(&self, writer: V) -> IonResult<()> { if self.isl_version == IslVersion::V1_0 { if self.is_required { - writer.set_annotations(["required"]); + writer + .with_annotations(["required"])? + .write_symbol(self.value.as_str()) } else { - writer.set_annotations(["optional"]); + writer + .with_annotations(["optional"])? + .write_symbol(self.value.as_str()) } + } else { + writer.write_symbol(self.value.as_str()) } - writer.write_symbol(&self.value)?; - Ok(()) } } @@ -167,10 +170,9 @@ impl PartialOrd for TimestampPrecision { } } -impl WriteToIsl for TimestampPrecision { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - writer.write_symbol(self.string_value())?; - Ok(()) +impl WriteAsIon for TimestampPrecision { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + writer.write_symbol(self.string_value().as_str()) } } @@ -213,7 +215,6 @@ impl ValidValue { 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 { @@ -240,15 +241,13 @@ impl Display for ValidValue { } } -impl WriteToIsl for ValidValue { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for ValidValue { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match self { - // 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)?, + ValidValue::Element(value) => writer.write(value), + ValidValue::NumberRange(r) => writer.write(r), + ValidValue::TimestampRange(r) => writer.write(r), } - Ok(()) } } @@ -342,18 +341,17 @@ impl Display for TimestampOffset { } } -impl WriteToIsl for TimestampOffset { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for TimestampOffset { + fn write_as_ion(&self, writer: V) -> IonResult<()> { match &self { TimestampOffset::Known(offset) => { let sign = if offset < &0 { "-" } else { "+" }; let hours = abs(*offset) / 60; let minutes = abs(*offset) - hours * 60; - writer.write_string(format!("{sign}{hours:02}:{minutes:02}"))?; + writer.write_string(format!("{sign}{hours:02}:{minutes:02}")) } - TimestampOffset::Unknown => writer.write_string("-00:00")?, + TimestampOffset::Unknown => writer.write_string("-00:00"), } - Ok(()) } } @@ -394,13 +392,12 @@ impl Display for Ieee754InterchangeFormat { } } -impl WriteToIsl for Ieee754InterchangeFormat { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { +impl WriteAsIon for Ieee754InterchangeFormat { + fn write_as_ion(&self, writer: V) -> IonResult<()> { writer.write_symbol(match self { Ieee754InterchangeFormat::Binary16 => "binary16", Ieee754InterchangeFormat::Binary32 => "binary32", Ieee754InterchangeFormat::Binary64 => "binary64", - })?; - Ok(()) + }) } } diff --git a/ion-schema/src/lib.rs b/ion-schema/src/lib.rs index f4e35b2..ecd415a 100644 --- a/ion-schema/src/lib.rs +++ b/ion-schema/src/lib.rs @@ -1,19 +1,18 @@ // TODO: remove the following line once we have a basic implementation ready #![allow(dead_code, unused_variables)] -use crate::external::ion_rs::IonType; use crate::ion_path::IonPath; use crate::isl::isl_constraint::IslConstraintValue; use crate::isl::isl_type::IslType; -use crate::isl::WriteToIsl; use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult}; use crate::violation::{Violation, ViolationCode}; -use ion_rs::element::{Element, Struct}; -use ion_rs::{IonWriter, Symbol}; +use ion_rs::Symbol; +use ion_rs::{Element, IonResult, IonType, Struct, StructWriter, ValueWriter, WriteAsIon}; use regex::Regex; use std::fmt::{Display, Formatter}; use std::sync::OnceLock; -/// A [`try`]-like macro to workaround the [`Option`]/[`Result`] nested APIs. + +/// A `try`-like macro to work around the [`Option`]/[`Result`] nested APIs. /// These API require checking the type and then calling the appropriate getter function /// (which returns a None if you got it wrong). This macro turns the `None` into /// an `IonSchemaError` which cannot be currently done with `?`. @@ -101,7 +100,7 @@ const ISL_2_0_KEYWORDS: [&str; 28] = [ /// In order to create an `IonSchemaElement`: /// /// ``` -/// use ion_rs::element::Element; +/// use ion_rs::Element; /// use ion_schema::IonSchemaElement; /// /// // create an IonSchemaElement from an Element @@ -215,7 +214,10 @@ impl From<&Vec> for IonSchemaElement { // helper function to be used by schema tests fn load(text: &str) -> Vec { - Element::read_all(text.as_bytes()).expect("parsing failed unexpectedly") + Element::read_all(text.as_bytes()) + .expect("parsing failed unexpectedly") + .into_iter() + .collect() } #[derive(Debug, Clone, Default, PartialEq)] @@ -226,6 +228,12 @@ pub struct UserReservedFields { } impl UserReservedFields { + pub(crate) fn is_empty(&self) -> bool { + self.type_fields.is_empty() + && self.schema_header_fields.is_empty() + && self.schema_footer_fields.is_empty() + } + /// Parse use reserved fields inside a [Struct] pub(crate) fn from_ion_elements(user_reserved_fields: &Struct) -> IonSchemaResult { if user_reserved_fields.fields().any(|(f, v)| { @@ -359,38 +367,18 @@ impl UserReservedFields { } } -impl WriteToIsl for UserReservedFields { - fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { - // this function assumes that we are already inside a schema header struct - // writes `user_reserved_fields` in the schema header - writer.set_field_name("user_reserved_fields"); - writer.step_in(IonType::Struct)?; - - // writes user reserved fields for `schema_header` - writer.set_field_name("schema_header"); - writer.step_in(IonType::List)?; - for value in &self.schema_header_fields { - writer.write_symbol(value)?; - } - writer.step_out()?; - - // writes user reserved fields for `type` - writer.set_field_name("type"); - writer.step_in(IonType::List)?; - for value in &self.type_fields { - writer.write_symbol(value)?; - } - writer.step_out()?; - - // writes user reserved fields for `schema_footer` - writer.set_field_name("schema_footer"); - writer.step_in(IonType::List)?; - for value in &self.schema_footer_fields { - writer.write_symbol(value)?; - } - writer.step_out()?; - - writer.step_out()?; - Ok(()) +impl WriteAsIon for UserReservedFields { + fn write_as_ion(&self, writer: V) -> IonResult<()> { + let mut struct_writer = writer.struct_writer()?; + struct_writer + .field_writer("schema_header") + .write(&self.schema_header_fields)?; + struct_writer + .field_writer("type") + .write(&self.type_fields)?; + struct_writer + .field_writer("schema_footer") + .write(&self.schema_footer_fields)?; + struct_writer.close() } } diff --git a/ion-schema/src/nfa.rs b/ion-schema/src/nfa.rs index f206077..4869bad 100644 --- a/ion-schema/src/nfa.rs +++ b/ion-schema/src/nfa.rs @@ -3,7 +3,7 @@ use crate::system::TypeStore; use crate::type_reference::TypeReference; use crate::types::TypeValidator; use crate::IonSchemaElement; -use ion_rs::element::Element; +use ion_rs::Element; use std::collections::{HashMap, HashSet}; use std::iter::Peekable; use std::slice::Iter; diff --git a/ion-schema/src/result.rs b/ion-schema/src/result.rs index ebaadcc..24622ba 100644 --- a/ion-schema/src/result.rs +++ b/ion-schema/src/result.rs @@ -4,7 +4,7 @@ //! It is an enum with the variants, Ok(T), representing success and containing a value,and Err(E), representing an [`IonSchemaError`]. use crate::violation::Violation; -use ion_rs::result::IonError; +use ion_rs::IonError; use std::io; use thiserror::Error; diff --git a/ion-schema/src/schema.rs b/ion-schema/src/schema.rs index b4f6522..4170e44 100644 --- a/ion-schema/src/schema.rs +++ b/ion-schema/src/schema.rs @@ -128,13 +128,16 @@ mod schema_tests { use super::*; use crate::authority::MapDocumentAuthority; use crate::system::{Resolver, SchemaSystem}; - use ion_rs::element::Element; + use ion_rs::Element; use rstest::*; use std::sync::Arc; // helper function to be used by schema tests fn load(text: &str) -> Vec { - Element::read_all(text.as_bytes()).expect("parsing failed unexpectedly") + Element::read_all(text.as_bytes()) + .expect("parsing failed unexpectedly") + .into_iter() + .collect() } // helper function to be used by validation tests @@ -1332,7 +1335,7 @@ mod schema_tests { for valid_value in valid_values.iter() { // there is only a single type in each schema defined above hence validate with that type let validation_result = type_ref.validate(valid_value); - assert!(validation_result.is_ok()); + validation_result.unwrap(); } // check for violations due to invalid values for invalid_value in invalid_values.iter() { diff --git a/ion-schema/src/system.rs b/ion-schema/src/system.rs index 533d1ec..18fdf62 100644 --- a/ion-schema/src/system.rs +++ b/ion-schema/src/system.rs @@ -30,8 +30,8 @@ use crate::result::{ use crate::schema::Schema; use crate::types::{BuiltInTypeDefinition, Nullability, TypeDefinitionImpl, TypeDefinitionKind}; use crate::{is_isl_version_marker, is_reserved_word, UserReservedFields}; -use ion_rs::element::{Annotations, Element}; use ion_rs::IonType; +use ion_rs::{Annotations, Element}; use std::collections::{HashMap, HashSet}; use std::io::ErrorKind; use std::sync::Arc; @@ -734,12 +734,8 @@ impl Resolver { // get all isl type names from given isl types // this will be used to resolve type references which might not have yet resolved while loading a type definition - let isl_type_names: HashSet<&str> = HashSet::from_iter( - isl_types - .iter() - .filter(|t| t.name().is_some()) - .map(|t| t.name().as_ref().unwrap().as_str()), - ); + let isl_type_names: HashSet<&str> = + HashSet::from_iter(isl_types.iter().filter_map(|t| t.name())); for isl_type in &isl_types { // convert [IslType] into [TypeDefinitionKind] @@ -1011,12 +1007,8 @@ impl Resolver { // get all isl type names that are defined within the schema // this will be used to resolve type references which might not have yet resolved while loading a type definition - let isl_type_names: HashSet<&str> = HashSet::from_iter( - isl_types - .iter() - .filter(|t| t.name().is_some()) - .map(|t| t.name().as_ref().unwrap().as_str()), - ); + let isl_type_names: HashSet<&str> = + HashSet::from_iter(isl.types().filter_map(|t| t.name())); // Resolve all ISL types and constraints for isl_type in isl_types { @@ -2038,7 +2030,7 @@ mod schema_system_tests { let mut schema_system = SchemaSystem::new(vec![Box::new(MapDocumentAuthority::new(map_authority))]); let schema = schema_system.load_isl_schema("sample.isl")?; - let expected_open_content = Element::read_all( + let expected_open_content: Vec<_> = Element::read_all( r#" open_content_1::{ unknown_constraint: "this is an open content struct" @@ -2049,11 +2041,14 @@ mod schema_system_tests { } "# .as_bytes(), - )?; + )? + .into_iter() + .collect(); // verify the open content that is retrieved from the ISL model is same as the expected open content - assert_eq!(&schema.open_content().len(), &2); - assert_eq!(schema.open_content(), &expected_open_content); + let open_content: Vec<_> = schema.open_content().map(|x| x.to_owned()).collect(); + assert_eq!(open_content.len(), 2); + assert_eq!(open_content, expected_open_content); Ok(()) } diff --git a/ion-schema/src/type_reference.rs b/ion-schema/src/type_reference.rs index b6e932e..c0ba8ed 100644 --- a/ion-schema/src/type_reference.rs +++ b/ion-schema/src/type_reference.rs @@ -48,8 +48,8 @@ impl TypeValidator for TypeReference { use crate::isl::isl_type_reference::NullabilityModifier::*; let type_def = type_store.get_type_by_id(self.type_id()).unwrap(); match self.type_modifier { - Nullable => match value { - IonSchemaElement::SingleElement(element) => { + Nullable => { + if let Some(element) = value.as_element() { if element.is_null() && (element.ion_type() == IonType::Null || type_def @@ -58,8 +58,7 @@ impl TypeValidator for TypeReference { return Ok(()); } } - IonSchemaElement::Document(_) => {} - }, + } NullOr => { if let IonSchemaElement::SingleElement(element) = value { if element.ion_type() == IonType::Null { diff --git a/ion-schema/src/types.rs b/ion-schema/src/types.rs index 75ace16..dffdaac 100644 --- a/ion-schema/src/types.rs +++ b/ion-schema/src/types.rs @@ -7,7 +7,7 @@ use crate::result::{invalid_schema_error, IonSchemaResult, ValidationResult}; use crate::system::{PendingTypes, TypeId, TypeStore}; use crate::violation::{Violation, ViolationCode}; use crate::IonSchemaElement; -use ion_rs::element::Element; +use ion_rs::Element; use ion_rs::IonType; use ion_rs::Symbol; use std::fmt::{Display, Formatter}; @@ -54,16 +54,16 @@ impl TypeDefinition { /// Provides the validation for the given value based on this schema type /// ``` - /// use ion_rs::element::Element; + /// use ion_rs::Element; /// use ion_schema::IonSchemaElement; /// use ion_schema::authority::{FileSystemDocumentAuthority, DocumentAuthority}; /// use ion_schema::system::SchemaSystem; /// use ion_schema::result::IonSchemaResult; /// use std::path::Path; + /// use ion_schema::authority::MapDocumentAuthority; /// /// fn main() -> IonSchemaResult<()> { /// // create an IonSchemaElement from an Element - /// use ion_schema::authority::MapDocumentAuthority; /// let owned_element: Element = 4.into(); /// let document: Vec = vec![4.into(), "hello".to_string().into(), true.into()]; /// @@ -131,7 +131,7 @@ impl BuiltInTypeDefinition { let mut constraints = vec![]; // parses an isl_type to a TypeDefinition - let type_name = isl_type.name(); + let type_name = isl_type.name().map(|x| x.to_owned()); // convert IslConstraint to Constraint for isl_constraint in isl_type.constraints() { @@ -175,8 +175,8 @@ impl TypeValidator for BuiltInTypeDefinition { match &self { BuiltInTypeDefinition::Atomic(ion_type, is_nullable) => { // atomic types doesn't include document type - match value { - IonSchemaElement::SingleElement(element) => { + match value.as_element() { + Some(element) => { if *is_nullable == Nullability::NotNullable && element.is_null() { return Err(Violation::new( "type_constraint", @@ -200,7 +200,7 @@ impl TypeValidator for BuiltInTypeDefinition { Ok(()) } - IonSchemaElement::Document(document) => Err(Violation::new( + _ => Err(Violation::new( "type_constraint", ViolationCode::TypeMismatched, format!("expected type {ion_type:?}, found document"), @@ -382,7 +382,7 @@ pub(crate) struct TypeDefinitionImpl { constraints: Vec, // `is_deferred_type_def` indicates if this is a deferred type def which will be resolved later // e.g. - // ``` + // ```ion // type:: { // name: foo, // type: bar, @@ -471,7 +471,7 @@ impl TypeDefinitionImpl { } // add this unresolved type to context for type_id - let type_id = pending_types.add_type(type_store, type_name.to_owned()); + let type_id = pending_types.add_type(type_store, type_name.map(|x| x.to_owned())); // convert IslConstraint to Constraint let mut found_type_constraint = false; @@ -495,7 +495,7 @@ impl TypeDefinitionImpl { if !found_type_constraint && isl_version == IslVersion::V1_0 { // set the isl type name for any error that is returned while parsing its constraints let isl_type_name = match type_name.to_owned() { - Some(name) => name, + Some(name) => name.to_owned(), None => match isl_struct { None => "".to_owned(), Some(isl_type_struct) => format!("{isl_type_struct}"), @@ -523,7 +523,7 @@ impl TypeDefinitionImpl { } let type_def = TypeDefinitionImpl::new( - type_name.to_owned(), + type_name.map(|x| x.to_owned()), constraints, isl_type.isl_type_struct.to_owned(), ); diff --git a/ion-schema/src/violation.rs b/ion-schema/src/violation.rs index aa5c1f1..19ede23 100644 --- a/ion-schema/src/violation.rs +++ b/ion-schema/src/violation.rs @@ -89,7 +89,11 @@ impl Violation { // TODO: Implement Violation with proper indentation for the nested tree of violations impl fmt::Display for Violation { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "A validation error occurred: {}", self.message) + write!(f, "A validation error occurred: {}", self.message)?; + for v in &self.violations { + write!(f, " {v}")?; + } + Ok(()) } } diff --git a/ion-schema/tests/reexport_external.rs b/ion-schema/tests/reexport_external.rs deleted file mode 100644 index 770a229..0000000 --- a/ion-schema/tests/reexport_external.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[cfg(test)] -mod reexport_tests { - use ion_schema::authority::MapDocumentAuthority; - use ion_schema::isl::isl_constraint::v_1_0::*; - use ion_schema::isl::isl_type::v_1_0::*; - use ion_schema::isl::isl_type::IslType; - use ion_schema::isl::isl_type_reference::v_1_0::*; - use ion_schema::result::IonSchemaResult; - use ion_schema::system::SchemaSystem; - - /// This test shows how the ion_schema integration with ion_rs can be used - /// through a reexport. This means that consumers of ion_schema can use this - /// integration without having to specify the exact dependency version. - #[test] - fn ion_rs_is_reexported() -> IonSchemaResult<()> { - // map with (id, ion content) - let map_authority = [( - "sample.isl", - r#" - schema_header::{} - - type::{ - name: my_type, - type: int, - } - - schema_footer::{} - "#, - )]; - let mut schema_system = - SchemaSystem::new(vec![Box::new(MapDocumentAuthority::new(map_authority))]); - let schema = schema_system.load_isl_schema("sample.isl")?; - let actual_isl_type: &IslType = &schema.types()[0]; - - let expected_isl_type = &named_type("my_type", [type_constraint(named_type_ref("int"))]); - assert_eq!(actual_isl_type, expected_isl_type); - Ok(()) - } -} diff --git a/wasm-schema-sandbox/src/lib.rs b/wasm-schema-sandbox/src/lib.rs index 24ffb7d..fff3317 100644 --- a/wasm-schema-sandbox/src/lib.rs +++ b/wasm-schema-sandbox/src/lib.rs @@ -1,7 +1,7 @@ use ion_schema::authority::{DocumentAuthority, MapDocumentAuthority}; -use ion_schema::external::ion_rs::element::Element; use ion_schema::external::ion_rs::IonResult; +use ion_schema::external::ion_rs::{Element, Sequence}; use ion_schema::result::IonSchemaResult; use ion_schema::schema::Schema; use ion_schema::system::SchemaSystem; @@ -24,7 +24,7 @@ macro_rules! log { } } -fn load_all(text: &str) -> IonResult> { +fn load_all(text: &str) -> IonResult { Element::read_all(text.as_bytes()) } @@ -174,8 +174,10 @@ pub fn validate( let values_result = load_all(ion); let value = match values_result { - Ok(v) if is_document => IonSchemaElement::Document(v), - Ok(v) => IonSchemaElement::SingleElement(v[0].to_owned()), + Ok(v) if is_document => { + IonSchemaElement::Document(v.iter().map(|it| it.to_owned()).collect()) + } + Ok(v) => IonSchemaElement::SingleElement(v.get(0).unwrap().to_owned()), Err(_) => { return SchemaValidationResult::new( false, @@ -214,7 +216,7 @@ pub fn validate( let result: SchemaValidationResult = SchemaValidationResult::new( result.is_ok(), violations_result, - format!("{value}"), + ion.to_string(), false, "".to_string(), );