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