diff --git a/ion-schema/src/isl/isl_constraint.rs b/ion-schema/src/isl/isl_constraint.rs index 862221f..4937a29 100644 --- a/ion-schema/src/isl/isl_constraint.rs +++ b/ion-schema/src/isl/isl_constraint.rs @@ -4,9 +4,11 @@ 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::IslVersion; +use crate::isl::WriteToIsl; use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult}; +use ion_rs::element::writer::ElementWriter; use ion_rs::element::Element; -use ion_rs::IonType; +use ion_rs::{IonType, IonWriter}; use std::collections::HashMap; use std::convert::TryInto; @@ -1055,6 +1057,149 @@ impl IslConstraintImpl { } } +impl WriteToIsl for IslConstraintImpl { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + match self { + IslConstraintImpl::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()?; + } + IslConstraintImpl::Annotations(annotations) => { + annotations.write_to(writer)?; + } + IslConstraintImpl::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()?; + } + IslConstraintImpl::ByteLength(range) => { + writer.set_field_name("byte_length"); + range.write_to(writer)?; + } + IslConstraintImpl::CodepointLength(range) => { + writer.set_field_name("codepoint_length"); + range.write_to(writer)?; + } + IslConstraintImpl::Contains(elements) => { + writer.set_field_name("contains"); + writer.step_in(IonType::List)?; + for element in elements { + writer.write_element(element)?; + } + writer.step_out()?; + } + IslConstraintImpl::ContentClosed => { + writer.set_field_name("content"); + writer.write_symbol("closed")?; + } + IslConstraintImpl::ContainerLength(range) => { + writer.set_field_name("container_length"); + range.write_to(writer)?; + } + IslConstraintImpl::Element(type_ref, is_distinct) => { + writer.set_field_name("element"); + if *is_distinct { + writer.set_annotations(["distinct"]); + } + type_ref.write_to(writer)?; + } + IslConstraintImpl::Exponent(range) => { + writer.set_field_name("exponent"); + range.write_to(writer)?; + } + IslConstraintImpl::Fields(fields, content_closed) => { + writer.set_field_name("fields"); + if *content_closed { + writer.set_annotations(["closed"]); + } + writer.step_in(IonType::Struct)?; + for (field_name, type_ref) in fields.iter() { + writer.set_field_name(field_name); + type_ref.write_to(writer)?; + } + writer.step_out()?; + } + IslConstraintImpl::FieldNames(type_ref, is_distinct) => { + writer.set_field_name("field_names"); + if *is_distinct { + writer.set_annotations(["distinct"]); + } + type_ref.write_to(writer)?; + } + IslConstraintImpl::Ieee754Float(format) => { + writer.set_field_name("ieee754_float"); + format.write_to(writer)?; + } + IslConstraintImpl::Not(type_ref) => { + writer.set_field_name("not"); + type_ref.write_to(writer)?; + } + IslConstraintImpl::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()?; + } + IslConstraintImpl::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()?; + } + IslConstraintImpl::Precision(range) => { + writer.set_field_name("precision"); + range.write_to(writer)?; + } + IslConstraintImpl::Regex(regex) => { + regex.write_to(writer)?; + } + IslConstraintImpl::Scale(range) => { + writer.set_field_name("scale"); + range.write_to(writer)?; + } + IslConstraintImpl::TimestampOffset(timestamp_offset) => { + timestamp_offset.write_to(writer)?; + } + IslConstraintImpl::TimestampPrecision(range) => { + writer.set_field_name("timestamp_precision"); + range.write_to(writer)?; + } + IslConstraintImpl::Type(type_ref) => { + writer.set_field_name("type"); + type_ref.write_to(writer)?; + } + IslConstraintImpl::Unknown(field_name, value) => { + writer.set_field_name(field_name); + writer.write_element(value)?; + } + IslConstraintImpl::Utf8ByteLength(range) => { + writer.set_field_name("utf8_byte_length"); + range.write_to(writer)?; + } + IslConstraintImpl::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.step_out()?; + } + } + Ok(()) + } +} + /// Represents the [annotations] constraint /// /// [annotations]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#annotations @@ -1064,6 +1209,21 @@ pub(crate) enum IslAnnotationsConstraint { StandardAnnotations(IslTypeRefImpl), } +impl WriteToIsl for IslAnnotationsConstraint { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + writer.set_field_name("annotations"); + match self { + IslAnnotationsConstraint::SimpleAnnotations(simple_annotations) => { + simple_annotations.write_to(writer)?; + } + IslAnnotationsConstraint::StandardAnnotations(standard_annotations) => { + standard_annotations.write_to(writer)?; + } + } + Ok(()) + } +} + /// Represents the [simple syntax] for annotations constraint /// ```ion /// Grammar: ::= annotations: ... [ ... ] @@ -1180,6 +1340,28 @@ impl IslSimpleAnnotationsConstraint { } } +impl WriteToIsl for IslSimpleAnnotationsConstraint { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + let mut annotation_modifiers = vec![]; + if self.is_ordered { + annotation_modifiers.push("ordered"); + } + if self.is_closed { + annotation_modifiers.push("closed"); + } + 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(()) + } +} + /// Represents the [valid_values] constraint /// /// [valid_values]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#annotations @@ -1273,6 +1455,22 @@ impl IslRegexConstraint { } } +impl WriteToIsl for IslRegexConstraint { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + writer.set_field_name("regex"); + let mut regex_modifiers = vec![]; + if self.case_insensitive { + regex_modifiers.push("i"); + } + if self.multi_line { + regex_modifiers.push("m"); + } + writer.set_annotations(regex_modifiers); + writer.write_string(&self.expression)?; + Ok(()) + } +} + /// Represents the [timestamp_offset] constraint /// /// [timestamp_offset]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#timestamp_offset @@ -1290,3 +1488,15 @@ impl IslTimestampOffsetConstraint { &self.valid_offsets } } + +impl WriteToIsl for IslTimestampOffsetConstraint { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + writer.set_field_name("timestamp_offset"); + writer.step_in(IonType::List)?; + for timestamp_offset in &self.valid_offsets { + timestamp_offset.write_to(writer)?; + } + writer.step_out()?; + Ok(()) + } +} diff --git a/ion-schema/src/isl/isl_import.rs b/ion-schema/src/isl/isl_import.rs index 3c21161..027989a 100644 --- a/ion-schema/src/isl/isl_import.rs +++ b/ion-schema/src/isl/isl_import.rs @@ -1,5 +1,7 @@ +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}; /// Represents an [import] in an ISL schema. /// @@ -56,6 +58,26 @@ impl IslImport { } } +impl WriteToIsl for IslImport { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + writer.step_in(IonType::Struct)?; + match self { + IslImport::Schema(schema_import) => { + writer.set_field_name("id"); + writer.write_string(schema_import)?; + } + IslImport::Type(type_import) => { + type_import.write_to(writer)?; + } + IslImport::TypeAlias(type_alias_import) => { + type_alias_import.write_to(writer)?; + } + } + writer.step_out()?; + Ok(()) + } +} + /// Represents typed and type aliased [IslImport]s /// Typed import grammar: `{ id: , type: }` /// Type aliased import grammar: `{ id: , type: , as: }` @@ -87,3 +109,17 @@ impl IslImportType { &self.alias } } + +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)?; + if let Some(alias) = &self.alias { + writer.set_field_name("as"); + writer.write_symbol(alias)?; + } + Ok(()) + } +} diff --git a/ion-schema/src/isl/isl_range.rs b/ion-schema/src/isl/isl_range.rs index 48cf983..e040955 100644 --- a/ion-schema/src/isl/isl_range.rs +++ b/ion-schema/src/isl/isl_range.rs @@ -1,14 +1,16 @@ 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::integer::IntAccess; -use ion_rs::{element, Decimal, Int, IonType, Timestamp}; +use ion_rs::{element, Decimal, Int, IonType, IonWriter, Timestamp}; use std::cmp::Ordering; use std::fmt::{Display, Formatter}; use std::prelude::rust_2021::TryInto; @@ -418,6 +420,26 @@ impl Display for Range { } } +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)] @@ -736,6 +758,16 @@ impl Display for RangeImpl { } } +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 { @@ -944,6 +976,23 @@ impl Display for RangeBoundaryValue { } } +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 { diff --git a/ion-schema/src/isl/isl_type.rs b/ion-schema/src/isl/isl_type.rs index 9a7f365..343dded 100644 --- a/ion-schema/src/isl/isl_type.rs +++ b/ion-schema/src/isl/isl_type.rs @@ -1,8 +1,10 @@ use crate::isl::isl_constraint::{IslConstraint, IslConstraintImpl}; 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}; /// Provides public facing APIs for constructing ISL types programmatically for ISL 1.0 pub mod v_1_0 { @@ -217,6 +219,26 @@ impl IslTypeImpl { } } +impl WriteToIsl for IslTypeImpl { + 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", + ))?; + for constraint in self.constraints() { + constraint.write_to(writer)?; + } + writer.step_out()?; + Ok(()) + } +} + // OwnedStruct doesn't preserve field order hence the PartialEq won't work properly for unordered constraints // Related issue: https://github.com/amazon-ion/ion-rust/issues/200 impl PartialEq for IslTypeImpl { diff --git a/ion-schema/src/isl/isl_type_reference.rs b/ion-schema/src/isl/isl_type_reference.rs index 56bab75..8cd094a 100644 --- a/ion-schema/src/isl/isl_type_reference.rs +++ b/ion-schema/src/isl/isl_type_reference.rs @@ -2,6 +2,7 @@ use crate::isl::isl_import::{IslImport, IslImportType}; use crate::isl::isl_range::{Range, RangeType}; use crate::isl::isl_type::IslTypeImpl; use crate::isl::IslVersion; +use crate::isl::WriteToIsl; use crate::result::{ invalid_schema_error, invalid_schema_error_raw, unresolvable_schema_error, IonSchemaResult, }; @@ -9,7 +10,7 @@ use crate::system::{PendingTypes, TypeId, TypeStore}; use crate::type_reference::{TypeReference, VariablyOccurringTypeRef}; use crate::types::TypeDefinitionImpl; use ion_rs::element::Element; -use ion_rs::IonType; +use ion_rs::{IonType, IonWriter}; /// Provides public facing APIs for constructing ISL type references programmatically for ISL 1.0 pub mod v_1_0 { @@ -116,6 +117,21 @@ 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<()> { + match self { + NullabilityModifier::Nullable => { + writer.set_annotations(["nullable"]); + } + NullabilityModifier::NullOr => { + writer.set_annotations(["$null_or"]); + } + NullabilityModifier::Nothing => {} + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq)] pub(crate) enum IslTypeRefImpl { /// Represents a reference to a named type (including aliases and built-in types) @@ -332,6 +348,32 @@ impl IslTypeRefImpl { } } +impl WriteToIsl for IslTypeRefImpl { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + match self { + IslTypeRefImpl::Named(name, nullability_modifier) => { + nullability_modifier.write_to(writer)?; + writer.write_symbol(name)?; + } + IslTypeRefImpl::TypeImport(type_import, nullability_modifier) => { + nullability_modifier.write_to(writer)?; + writer.step_in(IonType::Struct)?; + type_import.write_to(writer)?; + writer.step_out()?; + } + IslTypeRefImpl::Anonymous(type_def, nullability_modifier) => { + nullability_modifier.write_to(writer)?; + writer.step_in(IonType::Struct)?; + for constraint in type_def.constraints() { + constraint.write_to(writer)?; + } + writer.step_out()?; + } + } + Ok(()) + } +} + /// Represents a [variably occurring type reference] that will be used by `ordered_elements` and `fields` constraints. /// /// ```ion @@ -460,3 +502,33 @@ impl IslVariablyOccurringTypeRef { Ok(VariablyOccurringTypeRef::new(type_ref, self.occurs())) } } + +impl WriteToIsl for IslVariablyOccurringTypeRef { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + match &self.type_ref { + IslTypeRefImpl::Named(name, nullability_modifier) => { + nullability_modifier.write_to(writer)?; + writer.write_symbol(name)?; + } + IslTypeRefImpl::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()?; + } + IslTypeRefImpl::Anonymous(type_def, nullability_modifier) => { + nullability_modifier.write_to(writer)?; + writer.step_in(IonType::Struct)?; + for constraint in type_def.constraints() { + constraint.write_to(writer)?; + } + writer.set_field_name("occurs"); + self.occurs.write_to(writer)?; + writer.step_out()?; + } + } + Ok(()) + } +} diff --git a/ion-schema/src/isl/mod.rs b/ion-schema/src/isl/mod.rs index f31a557..ec1e75a 100644 --- a/ion-schema/src/isl/mod.rs +++ b/ion-schema/src/isl/mod.rs @@ -81,8 +81,11 @@ 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 std::fmt::{Display, Formatter}; pub mod isl_constraint; @@ -113,6 +116,11 @@ 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<()>; +} + /// Provides an internal representation of an schema file #[derive(Debug, Clone, PartialEq)] pub struct IslSchema { @@ -194,6 +202,55 @@ impl IslSchema { pub fn user_reserved_fields(&self) -> Option<&UserReservedFields> { self.schema.user_reserved_fields.as_ref() } + + fn write_header(&self, writer: &mut W) -> IonSchemaResult<()> { + writer.set_annotations(["schema_header"]); + writer.step_in(IonType::Struct)?; + if !self.schema.imports.is_empty() { + writer.set_field_name("imports"); + writer.step_in(IonType::List)?; + for import in &self.schema.imports { + import.write_to(writer)?; + } + writer.step_out()?; + } + if let Some(user_reserved_fields) = &self.schema.user_reserved_fields { + user_reserved_fields.write_to(writer)?; + } + writer.step_out()?; + + Ok(()) + } +} + +impl WriteToIsl for IslSchema { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + let version = self.schema.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.schema.types { + isl_type.type_definition.write_to(writer)?; + } + // write open content at the end of the schema + for value in &self.schema.open_content { + writer.write_element(value)?; + } + + // write footer for given schema + writer.set_annotations(["schema_footer"]); + writer.step_in(IonType::Struct)?; + writer.step_out()?; + writer.flush()?; + Ok(()) + } } #[derive(Debug, Clone, PartialEq)] @@ -255,6 +312,7 @@ impl IslSchemaImpl { #[cfg(test)] mod isl_tests { + use crate::authority::FileSystemDocumentAuthority; use crate::isl::isl_constraint::v_1_0::*; use crate::isl::isl_constraint::IslConstraint; use crate::isl::isl_range::DecimalRange; @@ -273,14 +331,17 @@ mod isl_tests { 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::integer::Int as IntegerValue; use ion_rs::types::timestamp::Timestamp; - use ion_rs::IonType; use ion_rs::Symbol; + use ion_rs::{IonType, TextWriterBuilder}; use rstest::*; use std::io::Write; + use std::path::{Path, MAIN_SEPARATOR}; + use test_generator::test_resources; // helper function to create NamedIslType for isl tests using ISL 1.0 fn load_named_type(text: &str) -> IslType { @@ -945,4 +1006,58 @@ mod isl_tests { 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 + // following skip list files are related to order of types in the schema file + // https://github.com/amazon-ion/ion-schema-rust/issues/184 + "ion-schema-tests/ion_schema_1_0/schema/import/import_inline.isl", + "ion-schema-tests/ion_schema_2_0/imports/tree/inline_import_a.isl", + "ion-schema-tests/ion_schema_2_0/imports/tree/inline_import_c.isl", + ]; + + fn is_skip_list_path(file_name: &str) -> bool { + SKIP_LIST + .iter() + .map(|p| p.replace('/', &MAIN_SEPARATOR.to_string())) + .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) { + return; + } + + // creates a schema system that will be sued to load schema files into a schema model + let mut schema_system = + SchemaSystem::new(vec![Box::new(FileSystemDocumentAuthority::new( + Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(), + ))]); + + // 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(); + + // initiate an Ion pretty text writer + let mut buffer = Vec::new(); + let mut writer = TextWriterBuilder::pretty().build(&mut buffer).unwrap(); + + // write the previously constructed ISL model into a schema file using `write_to` + let write_schema_result = expected_schema.write_to(&mut writer); + assert!(write_schema_result.is_ok()); + + // 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(); + + // 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); + } } diff --git a/ion-schema/src/isl/util.rs b/ion-schema/src/isl/util.rs index d2eaac0..f3ae72a 100644 --- a/ion-schema/src/isl/util.rs +++ b/ion-schema/src/isl/util.rs @@ -1,9 +1,10 @@ use crate::isl::isl_range::{Range, RangeType}; -use crate::isl::IslVersion; +use crate::isl::{IslVersion, WriteToIsl}; use crate::result::{invalid_schema_error, IonSchemaError, IonSchemaResult}; +use ion_rs::element::writer::ElementWriter; use ion_rs::element::Element; use ion_rs::types::timestamp::Precision; -use ion_rs::{Symbol, Timestamp}; +use ion_rs::{IonWriter, Symbol, Timestamp}; use num_traits::abs; use std::cmp::Ordering; use std::fmt; @@ -54,6 +55,20 @@ impl Annotation { } } +impl WriteToIsl for Annotation { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + if self.isl_version == IslVersion::V1_0 { + if self.is_required { + writer.set_annotations(["required"]); + } else { + writer.set_annotations(["optional"]); + } + } + writer.write_symbol(&self.value)?; + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum TimestampPrecision { Year, @@ -206,6 +221,16 @@ 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)?, + } + Ok(()) + } +} + /// 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)` @@ -278,6 +303,21 @@ impl Display for TimestampOffset { } } +impl WriteToIsl for TimestampOffset { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + 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}"))?; + } + TimestampOffset::Unknown => writer.write_string("-00:00")?, + } + Ok(()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Ieee754InterchangeFormat { Binary16, @@ -314,3 +354,14 @@ impl Display for Ieee754InterchangeFormat { ) } } + +impl WriteToIsl for Ieee754InterchangeFormat { + fn write_to(&self, writer: &mut W) -> IonSchemaResult<()> { + 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 7b05091..0bf4737 100644 --- a/ion-schema/src/lib.rs +++ b/ion-schema/src/lib.rs @@ -5,10 +5,11 @@ use crate::external::ion_rs::IonType; use crate::ion_path::IonPath; use crate::isl::isl_constraint::IslConstraintImpl; use crate::isl::isl_type::IslTypeImpl; +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::Symbol; +use ion_rs::{IonWriter, Symbol}; use regex::Regex; use std::fmt::{Display, Formatter}; use std::sync::OnceLock; @@ -359,3 +360,39 @@ impl UserReservedFields { Ok(()) } } + +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(()) + } +}