ion_schema/isl/
isl_type.rs

1use crate::isl::isl_constraint::{IslConstraint, IslConstraintValue};
2use crate::isl::isl_import::IslImportType;
3use crate::isl::IslVersion;
4use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult};
5use ion_rs::{Element, IonResult, StructWriter, ValueWriter, WriteAsIon};
6
7/// Provides public facing APIs for constructing ISL types programmatically for ISL 1.0
8pub mod v_1_0 {
9    use crate::isl::isl_constraint::{IslConstraint, IslConstraintValue};
10    use crate::isl::isl_type::IslType;
11    use crate::isl::IslVersion;
12    use crate::result::IonSchemaResult;
13    use ion_rs::Element;
14
15    /// Creates a named [IslType] using the [IslConstraint] defined within it
16    pub fn named_type<A: Into<String>, B: Into<Vec<IslConstraint>>>(
17        name: A,
18        constraints: B,
19    ) -> IslType {
20        let constraints = constraints.into();
21        IslType::new(Some(name.into()), constraints, None)
22    }
23
24    /// Creates an anonymous [IslType] using the [IslConstraint] defined within it
25    pub fn anonymous_type<A: Into<Vec<IslConstraint>>>(constraints: A) -> IslType {
26        let constraints = constraints.into();
27        let isl_constraints: Vec<IslConstraintValue> = constraints
28            .iter()
29            .map(|c| c.constraint_value.to_owned())
30            .collect();
31        IslType::new(None, constraints, None)
32    }
33
34    /// Creates an [IslType] using the bytes that represent an ISL type definition.
35    /// Returns an error if the given bytes representation is syntactically incorrect as per [Ion Schema specification's grammar].
36    ///
37    /// _Note: This method allows loading both named and anonymous type definition._
38    ///
39    /// [Ion Schema specification's grammar]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#grammar
40    pub fn load_isl_type(bytes: &[u8]) -> IonSchemaResult<IslType> {
41        IslType::from_owned_element(IslVersion::V1_0, &Element::read_one(bytes)?, &mut vec![])
42    }
43}
44
45/// Provides public facing APIs for constructing ISL types programmatically for ISL 2.0
46pub mod v_2_0 {
47    use crate::isl::isl_constraint::IslConstraint;
48    use crate::isl::isl_type::{v_1_0, IslType};
49    use crate::isl::IslVersion;
50    use crate::result::IonSchemaResult;
51    use ion_rs::Element;
52
53    /// Creates a named [IslType] using the [IslConstraint] defined within it
54    pub fn named_type<A: Into<String>, B: Into<Vec<IslConstraint>>>(
55        name: A,
56        constraints: B,
57    ) -> IslType {
58        v_1_0::named_type(name, constraints)
59    }
60
61    /// Creates an anonymous [IslType] using the [IslConstraint] defined within it
62    pub fn anonymous_type<A: Into<Vec<IslConstraint>>>(constraints: A) -> IslType {
63        v_1_0::anonymous_type(constraints)
64    }
65
66    /// Loads an [IslType] using the bytes that represent an ISL type definition.
67    /// Returns an error if the given bytes representation is syntactically incorrect as per [Ion Schema specification's grammar].
68    ///
69    /// _Note: This method allows loading both named and anonymous definition._
70    ///
71    /// [Ion Schema specification's grammar]: https://amazon-ion.github.io/ion-schema/docs/isl-1-0/spec#grammar
72    pub fn load_isl_type(bytes: &[u8]) -> IonSchemaResult<IslType> {
73        IslType::from_owned_element(IslVersion::V2_0, &Element::read_one(bytes)?, &mut vec![])
74    }
75}
76
77/// Represents both named and anonymous [IslType]s and can be converted to a resolved type definition
78/// Named ISL type grammar: `type:: { name: <NAME>, <CONSTRAINT>...}`
79/// Anonymous ISL type grammar: `{ <CONSTRAINT>... }`
80#[derive(Debug, Clone)]
81pub struct IslType {
82    name: Option<String>,
83    constraints: Vec<IslConstraint>,
84    // Represents the ISL type struct in string format for anonymous type definition
85    // For named type definition & programmatically created type definition, this will be `None`
86    pub(crate) isl_type_struct: Option<Element>,
87}
88
89impl IslType {
90    pub(crate) fn new(
91        name: Option<String>,
92        constraints: Vec<IslConstraint>,
93        isl_type_struct: Option<Element>,
94    ) -> Self {
95        Self {
96            name,
97            constraints,
98            isl_type_struct,
99        }
100    }
101
102    pub fn name(&self) -> Option<&str> {
103        self.name.as_deref()
104    }
105
106    pub fn constraints(&self) -> &[IslConstraint] {
107        &self.constraints
108    }
109
110    pub fn open_content(&self) -> Vec<(String, Element)> {
111        let mut open_content = vec![];
112        for constraint in &self.constraints {
113            if let IslConstraintValue::Unknown(constraint_name, element) =
114                &constraint.constraint_value
115            {
116                open_content.push((constraint_name.to_owned(), element.to_owned()))
117            }
118        }
119        open_content
120    }
121
122    pub(crate) fn is_open_content_allowed(&self) -> bool {
123        let mut open_content = true;
124        if self.constraints.contains(&IslConstraint::new(
125            IslVersion::V1_0,
126            IslConstraintValue::ContentClosed,
127        )) {
128            open_content = false;
129        }
130        open_content
131    }
132
133    /// Parse constraints inside an [Element] to an [IslType]
134    pub(crate) fn from_owned_element(
135        isl_version: IslVersion,
136        ion: &Element,
137        inline_imported_types: &mut Vec<IslImportType>, // stores the inline_imports that are discovered while loading this ISL type
138    ) -> IonSchemaResult<Self> {
139        let mut constraints = vec![];
140        let contains_annotations = ion.annotations().contains("type");
141
142        let ion_struct = try_to!(ion.as_struct());
143
144        // parses the name of the type specified by schema
145        if ion_struct.get_all("name").count() > 1 {
146            return Err(invalid_schema_error_raw(
147                "type definition must only contain a single field that represents name of the type",
148            ));
149        }
150        let type_name: Option<String> = match ion_struct.get("name") {
151            Some(name_element) => match name_element.as_symbol() {
152                Some(name_symbol) => match name_symbol.text() {
153                    None => {
154                        return Err(invalid_schema_error_raw(
155                            "type names must be a symbol with defined text",
156                        ))
157                    }
158                    Some(name) => {
159                        if !name_element.annotations().is_empty() {
160                            return Err(invalid_schema_error_raw(
161                                "type names must be a non null and unannotated symbol with defined text",
162                            ));
163                        }
164                        Some(name.to_owned())
165                    }
166                },
167                None => {
168                    return Err(invalid_schema_error_raw(
169                        "type names must be a symbol with defined text",
170                    ))
171                }
172            },
173            None => None, // If there is no name field then it is an anonymous type
174        };
175
176        if !contains_annotations && type_name.is_some() {
177            // For named types if it does not have the `type::` annotation return an error
178            return Err(invalid_schema_error_raw(
179                "Top level types must have `type::` annotation in their definition",
180            ));
181        }
182
183        // set the isl type name for any error that is returned while parsing its constraints
184        let isl_type_name = match type_name.to_owned() {
185            Some(name) => name,
186            None => format!("{ion_struct:?}"),
187        };
188
189        // parses all the constraints inside a Type
190        for (field_name, value) in ion_struct.iter() {
191            let constraint_name = match field_name.text() {
192                Some("name") => continue, // if the field_name is "name" then it's the type name not a constraint
193                Some("occurs") => continue, // if the field_name is "occurs" then skip it as it is handled elsewhere
194                Some(name) => name,
195                None => {
196                    return Err(invalid_schema_error_raw(
197                        "A type name symbol token does not have any text",
198                    ))
199                }
200            };
201
202            let constraint = IslConstraint::new(
203                isl_version,
204                IslConstraintValue::from_ion_element(
205                    isl_version,
206                    constraint_name,
207                    value,
208                    &isl_type_name,
209                    inline_imported_types,
210                )?,
211            );
212            constraints.push(constraint);
213        }
214        Ok(IslType::new(type_name, constraints, Some(ion.to_owned())))
215    }
216}
217
218impl WriteAsIon for IslType {
219    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
220        let mut struct_writer = writer.with_annotations(["type"])?.struct_writer()?;
221
222        if let Some(name) = self.name.as_ref() {
223            struct_writer
224                .field_writer("name")
225                .write_symbol(name.as_str())?;
226        }
227
228        for constraint in self.constraints() {
229            let constraint_name = constraint.constraint().field_name();
230            struct_writer
231                .field_writer(constraint_name)
232                .write(constraint.constraint())?;
233        }
234        struct_writer.close()
235    }
236}
237
238// OwnedStruct doesn't preserve field order hence the PartialEq won't work properly for unordered constraints
239// Related issue: https://github.com/amazon-ion/ion-rust/issues/200
240impl PartialEq for IslType {
241    fn eq(&self, other: &Self) -> bool {
242        self.constraints.len() == other.constraints.len()
243            && self.name == other.name
244            && self.constraints.iter().all(|constraint| {
245                other
246                    .constraints
247                    .iter()
248                    .any(|other_constraint| constraint == other_constraint)
249            })
250    }
251}