Skip to main content

ion_schema/isl/
mod.rs

1//! Provides a way to construct ISL types/constraints programmatically.
2//!
3//! This module consists of three submodules that help constructing ISL types/constraint:
4//!
5//! * `isl_type` module represents a schema type [IslType] which converts given ion content in the schema file
6//!   into an ISL types(not-yet-resolved types). It stores `IslConstraint`s defined within the given type.
7//! * `isl_import` module represents a schema import [IslImport] which converts given ion content in the schema file
8//!   into an ISL import. It stores schema id, an optional type that needs to be imported and an optional alias to that type.
9//! * `isl_constraint` module represents schema constraints [IslConstraint]
10//!   which converts given ion content in the schema file into an ISL constraint(not-yet-resolved constraints).
11//! * `isl_type_reference` module provides a schema type reference.
12//!   The type reference grammar is defined in the [Ion Schema Specification]
13//!
14//! [IslConstraint]: isl_constraint::IslConstraint
15//! [Ion Schema Specification]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#grammar
16//!
17//! ## Example usage of `isl` module to create an `IslSchema` using `IslType`:
18//! ```
19//! use ion_schema::isl::isl_constraint::v_1_0::{all_of, type_constraint};
20//! use ion_schema::isl::isl_type::v_1_0::named_type;
21//! use ion_schema::isl::isl_type_reference::v_1_0::{anonymous_type_ref, named_type_ref};
22//! use ion_schema::isl::IslSchema;
23//! use ion_schema::system::SchemaSystem;
24//!
25//! // below code represents an ISL type:
26//! // type:: {
27//! //      name:my_type_name,
28//! //      type: int,
29//! //      all_of: [
30//! //          { type: bool }
31//! //      ]
32//! //  }
33//! let isl_type = named_type(
34//!     // represents the `name` of the defined type
35//!     "my_type_name".to_owned(),
36//!     vec![
37//!         // represents the `type: int` constraint
38//!         type_constraint(
39//!             named_type_ref("int")
40//!         ),
41//!         // represents `all_of` with anonymous type `{ type: bool }` constraint
42//!         all_of(
43//!             vec![
44//!                 anonymous_type_ref(
45//!                     vec![
46//!                         type_constraint(
47//!                             named_type_ref("bool")
48//!                         )
49//!                     ]
50//!                 )
51//!             ]
52//!         )
53//!     ]
54//! );
55//!
56//! // create an ISL schema using above IslType
57//! let isl_schema = IslSchema::schema_v_1_0("my_schema", vec![], vec![isl_type.to_owned()], vec![], vec![]);
58//!
59//! // create a schema with resolved type references from given IslSchema using SchemaSystem
60//! let mut schema_system = SchemaSystem::new(vec![]); // no authorities added
61//! let schema = schema_system.load_schema_from_isl_schema_v1_0(isl_schema);
62//!
63//! assert!(schema.is_ok());
64//!
65//! // verify if the generated schema contains the correct type
66//! assert!(schema.unwrap().get_type("my_type_name").is_some())
67//! ```
68//!
69//!  ## Example of serializing a programmatically constructed schema into a schema file:
70//! ```
71//! use ion_rs::{Writer, WriteConfig, TextFormat, SequenceWriter};
72//! use ion_rs::v1_0::Text;
73//! use ion_schema::isl::{isl_type::v_1_0::*, isl_constraint::v_1_0::*, isl_type_reference::v_1_0::*, IslSchema};
74//! use ion_schema::schema::Schema;
75//! use ion_schema::system::SchemaSystem;
76//!
77//! let isl_type = named_type(
78//!     // represents the `name` of the defined type
79//!     "my_type_name".to_owned(),
80//!     vec![
81//!         // represents the `type: int` constraint
82//!         type_constraint(
83//!             named_type_ref("int")
84//!         ),
85//!         // represents `all_of` with anonymous type `{ type: bool }` constraint
86//!         all_of(
87//!             vec![
88//!                 anonymous_type_ref(
89//!                     vec![
90//!                         type_constraint(
91//!                             named_type_ref("bool")
92//!                         )
93//!                     ]
94//!                 )
95//!             ]
96//!         )
97//!     ]
98//! );
99//!
100//! // create an ISL schema using above IslType
101//! let isl_schema = IslSchema::schema_v_1_0("my_schema", vec![], vec![isl_type.to_owned()], vec![], vec![]);
102//!
103//! // initiate an Ion pretty text writer
104//! let mut buffer = Vec::new();
105//! let mut writer = Writer::new(Text, buffer).unwrap();
106//!
107//! // write the previously constructed ISL model into a schema file using `write_to`
108//! let write_schema_result = writer.write_all(&isl_schema);
109//! assert!(write_schema_result.is_ok());
110//!
111//! // The above written schema file looks like following:
112//! // $ion_schema_1_0
113//! // schema_header::{}
114//! // type::{
115//! //    name: my_type_name,
116//! //   type: int,
117//! //   all_of: [
118//! //       {
119//! //           type: bool
120//! //       }
121//! //   ]
122//! // }
123//! // schema_footer::{}
124//! ```
125//! Note that all the above functions to construct a type, constraint and type reference comes from `v_1_0` module which represents functions for [ISL 1.0](https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec).
126//! In order to programmatically construct [ISL 2.0](https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec) types, constraints and type references use `v_2_0` module.
127
128// The given schema is loaded with a two phase approach:
129// 1. Phase 1: Constructing an internal representation of ISL types/constraints from given schema file.
130//             This phase creates all [IslType],  [IslConstraint], [IslTypeRef] structs from the ion content in schema file.
131// 2. Phase 2: Constructing resolved types/constraints from internal representation of ISL types/constraints(not-yet-resolved types/constraints).
132//             This is done by loading all types into [Schema] as below:
133//                 a. Convert all [IslType] → [TypeDefinition], [IslConstraint] → [Constraint], [IslTypeRef] → [TypeDefinition]
134//                 b. While doing (a) store all [TypeDefinition] in the [TypeStore](which could help
135//                    returning resolved types in a schema) and store generated [TypeId] in the constraint.
136
137use crate::isl::isl_import::{IslImport, IslImportType};
138use crate::isl::isl_type::IslType;
139use crate::UserReservedFields;
140use ion_rs::Element;
141use ion_rs::{IonResult, SequenceWriter, StructWriter, ValueWriter, WriteAsIon};
142use std::collections::HashMap;
143use std::fmt::{Display, Formatter};
144
145pub mod isl_constraint;
146pub mod isl_import;
147pub mod isl_type;
148pub mod isl_type_reference;
149pub mod ranges;
150pub mod util;
151
152/// Represents Ion Schema Language Versions
153/// Currently it support v1.0 and v2.0
154#[derive(Debug, Clone, PartialEq, Eq, Copy)]
155pub enum IslVersion {
156    V1_0,
157    V2_0,
158}
159
160impl WriteAsIon for IslVersion {
161    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
162        let text = match self {
163            IslVersion::V1_0 => "$ion_schema_1_0",
164            IslVersion::V2_0 => "$ion_schema_2_0",
165        };
166        writer.write_symbol(text)
167    }
168}
169
170impl Display for IslVersion {
171    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
172        write!(
173            f,
174            "{}",
175            match self {
176                IslVersion::V1_0 => "ISL 1.0",
177                IslVersion::V2_0 => "ISL 2.0",
178            }
179        )
180    }
181}
182
183/// Models the content that could be in a Schema document.
184#[derive(Debug, Clone, PartialEq)]
185pub enum SchemaContent {
186    Version(IslVersion),
187    Header(SchemaHeader),
188    Type(IslType),
189    Footer(SchemaFooter),
190    OpenContent(Element),
191}
192impl From<IslVersion> for SchemaContent {
193    fn from(value: IslVersion) -> Self {
194        SchemaContent::Version(value)
195    }
196}
197impl From<SchemaHeader> for SchemaContent {
198    fn from(value: SchemaHeader) -> Self {
199        SchemaContent::Header(value)
200    }
201}
202impl From<IslType> for SchemaContent {
203    fn from(value: IslType) -> Self {
204        SchemaContent::Type(value)
205    }
206}
207impl From<SchemaFooter> for SchemaContent {
208    fn from(value: SchemaFooter) -> Self {
209        SchemaContent::Footer(value)
210    }
211}
212
213impl SchemaContent {
214    fn as_header(&self) -> Option<&SchemaHeader> {
215        if let SchemaContent::Header(schema_header) = self {
216            Some(schema_header)
217        } else {
218            None
219        }
220    }
221    fn as_isl_version(&self) -> Option<&IslVersion> {
222        if let SchemaContent::Version(value) = self {
223            Some(value)
224        } else {
225            None
226        }
227    }
228    fn as_type(&self) -> Option<&IslType> {
229        if let SchemaContent::Type(value) = self {
230            Some(value)
231        } else {
232            None
233        }
234    }
235    fn as_open_content(&self) -> Option<&Element> {
236        if let SchemaContent::OpenContent(value) = self {
237            Some(value)
238        } else {
239            None
240        }
241    }
242    fn as_footer(&self) -> Option<&SchemaFooter> {
243        if let SchemaContent::Footer(value) = self {
244            Some(value)
245        } else {
246            None
247        }
248    }
249    fn expect_header(&self) -> &SchemaHeader {
250        let SchemaContent::Header(value) = self else {
251            unreachable!("expected to find a Header, but found: {:?}", self)
252        };
253        value
254    }
255    fn expect_isl_version(&self) -> &IslVersion {
256        let SchemaContent::Version(value) = self else {
257            unreachable!("expected to find an IslVersion, but found: {:?}", self)
258        };
259        value
260    }
261    fn expect_type(&self) -> &IslType {
262        let SchemaContent::Type(value) = self else {
263            unreachable!("expected to find a Type, but found: {:?}", self)
264        };
265        value
266    }
267    fn expect_open_content(&self) -> &Element {
268        let SchemaContent::OpenContent(value) = self else {
269            unreachable!("expected to find OpenContent, but found: {:?}", self)
270        };
271        value
272    }
273    fn expect_footer(&self) -> &SchemaFooter {
274        let SchemaContent::Footer(value) = self else {
275            unreachable!("expected to find a Footer, but found: {:?}", self)
276        };
277        value
278    }
279}
280
281impl WriteAsIon for SchemaContent {
282    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
283        match self {
284            SchemaContent::Version(value) => writer.write(value),
285            SchemaContent::Header(value) => writer.write(value),
286            SchemaContent::Type(value) => writer.write(value),
287            SchemaContent::Footer(value) => writer.write(value),
288            SchemaContent::OpenContent(value) => writer.write(value),
289        }
290    }
291}
292
293#[derive(Debug, Clone, PartialEq, Default)]
294pub struct SchemaHeader {
295    /// Represents the user defined reserved fields
296    user_reserved_fields: UserReservedFields,
297    /// Represents all the IslImports inside the schema file.
298    /// For more information: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#imports
299    imports: Vec<IslImport>,
300    /// User-defined (aka "open") content
301    user_content: Vec<(String, Element)>,
302}
303
304impl WriteAsIon for SchemaHeader {
305    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
306        let mut struct_writer = writer
307            .with_annotations(["schema_header"])?
308            .struct_writer()?;
309        if !self.imports.is_empty() {
310            struct_writer.field_writer("imports").write(&self.imports)?;
311        }
312        if !self.user_reserved_fields.is_empty() {
313            struct_writer
314                .field_writer("user_reserved_fields")
315                .write(&self.user_reserved_fields)?;
316        }
317        if !self.user_content.is_empty() {
318            for (k, v) in &self.user_content {
319                struct_writer.write(k.as_str(), v)?;
320            }
321        }
322        struct_writer.close()
323    }
324}
325
326#[derive(Debug, Clone, PartialEq, Default)]
327pub struct SchemaFooter {
328    /// User-defined (aka "open") content
329    user_content: Vec<(String, Element)>,
330}
331
332impl WriteAsIon for SchemaFooter {
333    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
334        let mut struct_writer = writer
335            .with_annotations(["schema_footer"])?
336            .struct_writer()?;
337        if !self.user_content.is_empty() {
338            for (k, v) in &self.user_content {
339                struct_writer.write(k.as_str(), v)?;
340            }
341        }
342        struct_writer.close()
343    }
344}
345
346/// Provides an internal representation of a schema file
347#[derive(Debug, Clone, PartialEq)]
348pub struct IslSchema {
349    /// Represents an id for the given ISL model
350    id: String,
351    /// Content of the schema document.
352    schema_content: Vec<SchemaContent>,
353    /// Position of the header within [schema_content].
354    header_index: Option<usize>,
355    /// Position of the footer within [schema_content].
356    footer_index: Option<usize>,
357    /// Position of all types defined in this schema file.
358    types_by_name: HashMap<String, usize>,
359
360    /// Represents all the inline IslImportTypes in this schema file.
361    inline_imported_types: Vec<IslImportType>,
362}
363
364impl IslSchema {
365    fn new_from_content_vec(
366        id: String,
367        schema_content: Vec<SchemaContent>,
368        inline_imported_types: Vec<IslImportType>,
369    ) -> Self {
370        assert!(matches!(schema_content[0], SchemaContent::Version(_)));
371        let header_index = schema_content
372            .iter()
373            .position(|x| matches!(x, SchemaContent::Header(_)));
374        let footer_index = schema_content
375            .iter()
376            .position(|x| matches!(x, SchemaContent::Footer(_)));
377        // TODO: Ensure that ISL 1.0 schemas have footer iff header
378        let types_by_name: HashMap<String, usize> = schema_content
379            .iter()
380            .enumerate()
381            .filter(|&(_, x)| matches!(x, SchemaContent::Type(_)))
382            .map(|(i, t)| (t.expect_type().name().unwrap().to_string(), i))
383            .collect();
384
385        Self {
386            id,
387            schema_content,
388            header_index,
389            footer_index,
390            types_by_name,
391            inline_imported_types,
392        }
393    }
394
395    /// TODO: Replace with a builder
396    pub(crate) fn new<A: AsRef<str>>(
397        id: A,
398        version: IslVersion,
399        user_reserved_fields: Option<UserReservedFields>,
400        imports: Vec<IslImport>,
401        types: Vec<IslType>,
402        inline_imports: Vec<IslImportType>,
403        open_content: Vec<Element>,
404    ) -> Self {
405        let mut schema_content: Vec<SchemaContent> = vec![];
406        schema_content.push(version.into());
407        schema_content.push(
408            SchemaHeader {
409                user_reserved_fields: user_reserved_fields.unwrap_or_default(),
410                imports,
411                user_content: vec![],
412            }
413            .into(),
414        );
415        for t in types {
416            schema_content.push(t.into());
417        }
418        for e in open_content {
419            schema_content.push(SchemaContent::OpenContent(e));
420        }
421        Self::new_from_content_vec(id.as_ref().to_string(), schema_content, inline_imports)
422    }
423
424    /// Creates an ISL schema using the [IslType]s, [IslImport]s, open content and schema id
425    /// TODO: Replace with a builder
426    pub fn schema_v_1_0<A: AsRef<str>>(
427        id: A,
428        imports: Vec<IslImport>,
429        types: Vec<IslType>,
430        inline_imports: Vec<IslImportType>,
431        open_content: Vec<Element>,
432    ) -> IslSchema {
433        IslSchema::new(
434            id.as_ref(),
435            IslVersion::V1_0,
436            None,
437            imports,
438            types,
439            inline_imports,
440            open_content,
441        )
442    }
443
444    /// Creates an ISL schema using the [IslType]s, [IslImport]s, [UserReservedFields] open content and schema id
445    /// TODO: Replace with a builder
446    pub fn schema_v_2_0<A: AsRef<str>>(
447        id: A,
448        user_reserved_fields: UserReservedFields,
449        imports: Vec<IslImport>,
450        types: Vec<IslType>,
451        inline_imports: Vec<IslImportType>,
452        open_content: Vec<Element>,
453    ) -> IslSchema {
454        IslSchema::new(
455            id.as_ref(),
456            IslVersion::V2_0,
457            Some(user_reserved_fields),
458            imports,
459            types,
460            inline_imports,
461            open_content,
462        )
463    }
464
465    pub fn id(&self) -> String {
466        self.id.to_owned()
467    }
468
469    pub fn version(&self) -> IslVersion {
470        *(self.schema_content[0].expect_isl_version())
471    }
472
473    fn header(&self) -> Option<&SchemaHeader> {
474        self.header_index
475            .map(|i| self.schema_content.get(i).unwrap())
476            .map(SchemaContent::expect_header)
477    }
478
479    fn footer(&self) -> Option<&SchemaFooter> {
480        self.footer_index
481            .map(|i| self.schema_content.get(i).unwrap())
482            .map(SchemaContent::expect_footer)
483    }
484
485    const EMPTY_VEC: Vec<IslImport> = vec![];
486    const EMPTY_VEC_REF: &'static Vec<IslImport> = &Self::EMPTY_VEC;
487
488    pub fn imports(&self) -> impl Iterator<Item = &IslImport> {
489        self.header()
490            .map(|x| &x.imports)
491            .unwrap_or(Self::EMPTY_VEC_REF)
492            .iter()
493    }
494
495    pub fn types(&self) -> impl Iterator<Item = &IslType> {
496        self.schema_content.iter().filter_map(|x| x.as_type())
497    }
498
499    pub fn inline_imported_types(&self) -> impl Iterator<Item = &IslImportType> {
500        self.inline_imported_types.iter()
501    }
502
503    /// Provides top level open content for given schema
504    /// For open content defined within type definitions use IslType#open_content()
505    pub fn open_content(&self) -> impl Iterator<Item = &Element> {
506        self.schema_content
507            .iter()
508            .filter_map(|x| x.as_open_content())
509    }
510
511    /// Provide user reserved field defined in the given schema for ISL 2.0,
512    /// Otherwise returns None
513    pub fn user_reserved_fields(&self) -> Option<&UserReservedFields> {
514        self.header().map(|x| &x.user_reserved_fields)
515    }
516}
517
518impl IslSchema {
519    fn write_as_ion<W: SequenceWriter>(&self, writer: &mut W) -> IonResult<()> {
520        for item in &self.schema_content {
521            writer.write(item)?;
522        }
523        Ok(())
524    }
525}
526
527impl<'a> IntoIterator for &'a IslSchema {
528    type Item = &'a SchemaContent;
529    type IntoIter = SchemaIterator<'a>;
530
531    fn into_iter(self) -> Self::IntoIter {
532        SchemaIterator {
533            content: &self.schema_content,
534            i: 0,
535        }
536    }
537}
538
539pub struct SchemaIterator<'a> {
540    content: &'a Vec<SchemaContent>,
541    i: usize,
542}
543
544impl<'a> Iterator for SchemaIterator<'a> {
545    type Item = &'a SchemaContent;
546
547    fn next(&mut self) -> Option<Self::Item> {
548        let next_item = self.content.get(self.i);
549        self.i += 1;
550        next_item
551    }
552}
553
554#[cfg(test)]
555mod isl_tests {
556    use crate::authority::FileSystemDocumentAuthority;
557    use crate::ion_extension::ElementExtensions;
558    use crate::isl::isl_constraint::v_1_0::*;
559    use crate::isl::isl_type::v_1_0::load_isl_type as load_isl_type_def;
560    use crate::isl::isl_type::v_1_0::*;
561    use crate::isl::isl_type::v_2_0::load_isl_type as load_isl_type_def_v2_0;
562    use crate::isl::isl_type::IslType;
563    use crate::isl::isl_type_reference::v_1_0::*;
564    use crate::isl::ranges::*;
565    use crate::isl::util::Ieee754InterchangeFormat;
566    use crate::isl::util::TimestampPrecision;
567    use crate::isl::util::ValidValue;
568    use crate::isl::*;
569    use crate::result::IonSchemaResult;
570    use crate::system::SchemaSystem;
571    use ion_rs::v1_0;
572    use ion_rs::Decimal;
573    use ion_rs::IonType;
574    use ion_rs::Symbol;
575    use ion_rs::{Element, TextFormat, WriteConfig, Writer};
576    use rstest::*;
577    use std::path::Path;
578    use test_generator::test_resources;
579
580    // helper function to create AnonymousIslType for isl tests using ISL 1.0
581    fn load_isl_type(text: &str) -> IslType {
582        load_isl_type_def(text.as_bytes()).unwrap()
583    }
584
585    // helper function to create AnonymousIslType for isl tests using ISL 2.0
586    fn load_isl_type_v2_0(text: &str) -> IslType {
587        load_isl_type_def_v2_0(text.as_bytes()).unwrap()
588    }
589
590    #[test]
591    fn test_open_content_for_type_def() -> IonSchemaResult<()> {
592        let type_def = load_isl_type(
593            r#" 
594                // type definition with open content
595                type:: { 
596                    name: my_int, 
597                    type: int,
598                    unknown_constraint: "this is an open content field value"
599                }
600            "#,
601        );
602
603        assert_eq!(
604            type_def.open_content(),
605            vec![(
606                "unknown_constraint".to_owned(),
607                Element::read_one(r#""this is an open content field value""#.as_bytes())?
608            )]
609        );
610        Ok(())
611    }
612
613    #[rstest(
614    isl_type1,isl_type2,
615    case::type_constraint_with_anonymous_type(
616        load_isl_type(r#" // For a schema with single anonymous type
617                {type: any}
618            "#),
619        anonymous_type([type_constraint(named_type_ref("any"))])
620    ),
621    case::type_constraint_with_nullable_annotation(
622        load_isl_type(r#" // For a schema with `nullable` annotation`
623                {type: nullable::int}
624            "#),
625        anonymous_type([type_constraint(nullable_built_in_type_ref(IonType::Int))])
626    ),
627    case::type_constraint_with_null_or_annotation(
628        load_isl_type_v2_0(r#" // For a schema with `$null_or` annotation`
629                    {type: $null_or::int}
630                "#),
631        isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::type_constraint(isl_type_reference::v_2_0::null_or_named_type_ref("int"))])
632    ),
633    case::type_constraint_with_named_type(
634        load_isl_type(r#" // For a schema with named type
635                type:: { name: my_int, type: int }
636            "#),
637        named_type("my_int", [type_constraint(named_type_ref("int"))])
638    ),
639    case::type_constraint_with_named_nullable_type(
640        load_isl_type(r#" // For a schema with named type
641                type:: { name: my_nullable_int, type: $int }
642            "#),
643        named_type("my_nullable_int", [type_constraint(named_type_ref("$int"))])
644    ),
645    case::type_constraint_with_self_reference_type(
646        load_isl_type(r#" // For a schema with self reference type
647                type:: { name: my_int, type: my_int }
648            "#),
649        named_type("my_int", [type_constraint(named_type_ref("my_int"))])
650    ),
651    case::type_constraint_with_nested_self_reference_type(
652        load_isl_type(r#" // For a schema with nested self reference type
653                type:: { name: my_int, type: { type: my_int } }
654            "#),
655        named_type("my_int", [type_constraint(anonymous_type_ref([type_constraint(named_type_ref("my_int"))]))])
656    ),
657    case::type_constraint_with_nested_type(
658        load_isl_type(r#" // For a schema with nested types
659                type:: { name: my_int, type: { type: int } }
660            "#),
661        named_type("my_int", [type_constraint(anonymous_type_ref([type_constraint(named_type_ref("int"))]))])
662    ),
663    case::type_constraint_with_nested_multiple_types(
664        load_isl_type(r#" // For a schema with nested multiple types
665                type:: { name: my_int, type: { type: int }, type: { type: my_int } }
666            "#),
667        named_type("my_int", [type_constraint(anonymous_type_ref([type_constraint(named_type_ref("int"))])), type_constraint(anonymous_type_ref([type_constraint(named_type_ref("my_int"))]))])
668    ),
669    case::all_of_constraint(
670        load_isl_type(r#" // For a schema with all_of type as below:
671                { all_of: [{ type: int }] }
672            "#),
673        anonymous_type([all_of([anonymous_type_ref([type_constraint(named_type_ref("int"))])])])
674    ),
675    case::any_of_constraint(
676        load_isl_type(r#" // For a schema with any_of constraint as below:
677                    { any_of: [{ type: int }, { type: decimal }] }
678                "#),
679        anonymous_type([any_of([anonymous_type_ref([type_constraint(named_type_ref("int"))]), anonymous_type_ref([type_constraint(named_type_ref("decimal"))])])])
680    ),
681    case::one_of_constraint(
682        load_isl_type(r#" // For a schema with one_of constraint as below:
683                    { one_of: [{ type: int }, { type: decimal }] }
684                "#),
685        anonymous_type([one_of([anonymous_type_ref([type_constraint(named_type_ref("int"))]), anonymous_type_ref([type_constraint(named_type_ref("decimal"))])])])
686    ),
687    case::not_constraint(
688        load_isl_type(r#" // For a schema with not constraint as below:
689                    { not: { type: int } }
690                "#),
691        anonymous_type([not(anonymous_type_ref([type_constraint(named_type_ref("int"))]))])
692    ),
693    case::ordered_elements_constraint(
694        load_isl_type(r#" // For a schema with ordered_elements constraint as below:
695                    { ordered_elements: [  symbol, { type: int },  ] }
696                "#),
697        anonymous_type([ordered_elements([variably_occurring_type_ref(named_type_ref("symbol"), UsizeRange::new_single_value(1)), variably_occurring_type_ref(anonymous_type_ref([type_constraint(named_type_ref("int"))]), UsizeRange::new_single_value(1))])])
698    ),
699    case::closed_fields_constraint(
700        load_isl_type_v2_0(r#" // For a schema with fields constraint as below:
701                    { fields: closed::{ name: string, id: int} }
702                "#),
703        anonymous_type([isl_constraint::v_2_0::fields(vec![("name".to_owned(), variably_occurring_type_ref(named_type_ref("string"), UsizeRange::zero_or_one())), ("id".to_owned(), variably_occurring_type_ref(named_type_ref("int"), UsizeRange::zero_or_one()))].into_iter(), true)]),
704    ),
705        case::fields_constraint(
706            load_isl_type(r#" // For a schema with fields constraint as below:
707                    { fields: { name: string, id: int} }
708                "#),
709            anonymous_type([fields(vec![("name".to_owned(), variably_occurring_type_ref(named_type_ref("string"), UsizeRange::zero_or_one())), ("id".to_owned(), variably_occurring_type_ref(named_type_ref("int"), UsizeRange::zero_or_one()))].into_iter())]),
710        ),
711    case::field_names_constraint(
712        load_isl_type_v2_0(r#" // For a schema with field_names constraint as below:
713                        { field_names: distinct::symbol }
714                    "#),
715        isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::field_names(isl_type_reference::v_2_0::named_type_ref("symbol"), true)]),
716    ),
717    case::contains_constraint(
718        load_isl_type(r#" // For a schema with contains constraint as below:
719                    { contains: [true, 1, "hello"] }
720                "#),
721        anonymous_type([contains([true.into(), 1.into(), "hello".to_owned().into()])])
722    ),
723    case::container_length_constraint(
724        load_isl_type(r#" // For a schema with container_length constraint as below:
725                    { container_length: 3 }
726                "#),
727        anonymous_type([container_length(3.into())])
728    ),
729    case::byte_length_constraint(
730        load_isl_type(r#" // For a schema with byte_length constraint as below:
731                    { byte_length: 3 }
732                "#),
733        anonymous_type([byte_length(3.into())])
734    ),
735    case::codepoint_length_constraint(
736        load_isl_type(r#" // For a schema with codepoint_length constraint as below:
737                    { codepoint_length: 3 }
738                "#),
739        anonymous_type([codepoint_length(3.into())])
740    ),
741    case::element_constraint(
742        load_isl_type(r#" // For a schema with element constraint as below:
743                    { element: int }
744                "#),
745        anonymous_type([element(named_type_ref("int"))])
746    ),
747    case::distinct_element_constraint(
748        load_isl_type_v2_0(r#" // For a schema with distinct element constraint as below:
749                        { element: distinct::int }
750                    "#),
751    isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::element(named_type_ref("int"), true)])
752    ),
753    case::annotations_constraint(
754        load_isl_type(r#" // For a schema with annotations constraint as below:
755                        { annotations: closed::[red, blue, green] }
756                    "#),
757        anonymous_type([annotations(vec!["closed"], vec![Symbol::from("red").into(), Symbol::from("blue").into(), Symbol::from("green").into()])])
758    ),
759    case::standard_syantx_annotations_constraint(
760        load_isl_type_v2_0(r#" // For a schema with annotations constraint as below:
761                            { annotations: { container_length: 1 } }
762                        "#),
763    isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::annotations(isl_type_reference::v_2_0::anonymous_type_ref([isl_constraint::v_2_0::container_length(1.into())]))])
764    ),
765    case::precision_constraint(
766        load_isl_type(r#" // For a schema with precision constraint as below:
767                        { precision: 2 }
768                    "#),
769        anonymous_type([precision(2.into())])
770    ),
771    case::scale_constraint(
772        load_isl_type(r#" // For a schema with scale constraint as below:
773                        { scale: 2 }
774                    "#),
775        anonymous_type([scale(2.into())])
776    ),
777    case::exponent_constraint(
778        load_isl_type_v2_0(r#" // For a schema with exponent constraint as below:
779                        { exponent: 2 }
780                    "#),
781        isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::exponent(2.into())])
782    ),
783    case::timestamp_precision_constraint(
784        load_isl_type(r#" // For a schema with timestamp_precision constraint as below:
785                            { timestamp_precision: year }
786                        "#),
787        anonymous_type([timestamp_precision(TimestampPrecisionRange::new_single_value(TimestampPrecision::Year))])
788    ),
789    case::valid_values_constraint(
790        load_isl_type(r#" // For a schema with valid_values constraint as below:
791                        { valid_values: [2, 3.5, 5e7, "hello", hi] }
792                    "#),
793        anonymous_type([valid_values(vec![2.into(), Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into()]).unwrap()])
794    ),
795    case::valid_values_with_range_constraint(
796        load_isl_type(r#" // For a schema with valid_values constraint as below:
797                        { valid_values: range::[1, 5.5] }
798                    "#),
799        anonymous_type(
800                [valid_values(vec![
801                    ValidValue::NumberRange(
802                        NumberRange::new_inclusive(
803                            1.into(),
804                            Decimal::new(55, -1)
805                        ).unwrap()
806                    )
807                ]).unwrap()]
808            )
809        ),
810    case::utf8_byte_length_constraint(
811        load_isl_type(r#" // For a schema with utf8_byte_length constraint as below:
812                        { utf8_byte_length: 3 }
813                    "#),
814        anonymous_type([utf8_byte_length(3.into())])
815    ),
816    case::regex_constraint(
817        load_isl_type(r#" // For a schema with regex constraint as below:
818                            { regex: "[abc]" }
819                        "#),
820        anonymous_type(
821            [
822                regex(
823                    false, // case insensitive
824                    false, // multiline
825                    "[abc]".to_string()
826                )
827            ]
828        )
829    ),
830    case::timestamp_offset_constraint(
831        load_isl_type(r#" // For a schema with timestamp_offset constraint as below:
832                            { timestamp_offset: ["+00:00"] }
833                        "#),
834        anonymous_type(
835            [timestamp_offset(vec!["+00:00".try_into().unwrap()])]
836        )
837    ),
838    case::ieee754_float_constraint(
839        load_isl_type_v2_0(r#" // For a schema with ieee754_float constraint as below:
840                            { ieee754_float: binary16 }
841                        "#),
842        isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::ieee754_float(Ieee754InterchangeFormat::Binary16)])
843    ),
844    )]
845    fn owned_struct_to_isl_type(isl_type1: IslType, isl_type2: IslType) {
846        // assert if both the IslType are same in terms of constraints and name
847        assert_eq!(isl_type1, isl_type2);
848    }
849
850    // helper function to create a timestamp precision range
851    fn load_timestamp_precision_range(text: &str) -> IonSchemaResult<TimestampPrecisionRange> {
852        TimestampPrecisionRange::from_ion_element(
853            &Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"),
854            |e| {
855                let symbol_text = e.as_symbol().and_then(Symbol::text)?;
856                TimestampPrecision::try_from(symbol_text).ok()
857            },
858        )
859    }
860
861    // helper function to create a number range
862    fn load_number_range(text: &str) -> IonSchemaResult<NumberRange> {
863        NumberRange::from_ion_element(
864            &Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"),
865            Element::any_number_as_decimal,
866        )
867    }
868
869    // helper function to return Elements for range `contains` tests
870    fn elements<T: Into<Element> + std::clone::Clone>(values: &[T]) -> Vec<Element> {
871        values.iter().cloned().map(|v| v.into()).collect()
872    }
873
874    const SKIP_LIST: [&str; 5] = [
875        "ion-schema-schemas/json/json.isl", // the file contains `nan` which fails on equivalence for two schemas
876        "ion-schema-tests/ion_schema_1_0/nullable.isl", // Needs `nullable` annotation related fixes
877        // following skip list files are related to order of types in the schema file
878        // https://github.com/amazon-ion/ion-schema-rust/issues/184
879        "ion-schema-tests/ion_schema_1_0/schema/import/import_inline.isl",
880        "ion-schema-tests/ion_schema_2_0/imports/tree/inline_import_a.isl",
881        "ion-schema-tests/ion_schema_2_0/imports/tree/inline_import_c.isl",
882    ];
883
884    fn is_skip_list_path(file_name: &str) -> bool {
885        SKIP_LIST
886            .iter()
887            .map(|p| p.replace('/', std::path::MAIN_SEPARATOR_STR))
888            .any(|p| p == file_name)
889    }
890
891    #[test_resources("ion-schema-tests/**/*.isl")]
892    #[test_resources("ion-schema-schemas/**/*.isl")]
893    fn test_write_to_isl(file_name: &str) -> IonSchemaResult<()> {
894        if is_skip_list_path(file_name) {
895            return Ok(());
896        }
897
898        // creates a schema system that will be sued to load schema files into a schema model
899        let mut schema_system =
900            SchemaSystem::new(vec![Box::new(FileSystemDocumentAuthority::new(
901                Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(),
902            ))]);
903
904        // construct an ISL model from a schema file
905        let expected_schema = schema_system.load_isl_schema(file_name)?;
906
907        // initiate an Ion pretty text writer
908        let buffer = Vec::new();
909        let config = WriteConfig::<v1_0::Text>::new(TextFormat::Pretty);
910        let mut writer = Writer::new(config, buffer)?;
911
912        // write the previously constructed ISL model into a schema file using `write_to`
913        let write_schema_result = expected_schema.write_as_ion(&mut writer);
914        assert!(write_schema_result.is_ok());
915
916        let output = writer.close()?;
917
918        // create a new schema model from the schema file generated by the previous step
919        let actual_schema = schema_system.new_isl_schema(output.as_slice(), file_name)?;
920
921        // compare the actual schema model that was generated from dynamically created schema file
922        // with the expected schema model that was constructed from given schema file
923        assert_eq!(actual_schema, expected_schema);
924        Ok(())
925    }
926}