ion_schema/
lib.rs

1// TODO: remove the following line once we have a basic implementation ready
2#![allow(dead_code, unused_variables)]
3
4use crate::isl::isl_constraint::IslConstraintValue;
5use crate::isl::isl_type::IslType;
6use crate::result::{invalid_schema_error, invalid_schema_error_raw, IonSchemaResult};
7use ion_rs::{Element, IonResult, Struct, StructWriter, Symbol, ValueWriter, WriteAsIon};
8use regex::Regex;
9use std::sync::OnceLock;
10
11/// A `try`-like macro to work around the [`Option`]/[`Result`] nested APIs.
12/// These API require checking the type and then calling the appropriate getter function
13/// (which returns a None if you got it wrong). This macro turns the `None` into
14/// an `IonSchemaError` which cannot be currently done with `?`.
15macro_rules! try_to {
16    ($getter:expr) => {
17        match $getter {
18            Some(value) => value,
19            None => invalid_schema_error(format!("Missing a value: {}", stringify!($getter)))?,
20        }
21    };
22}
23
24// TODO: consider changing some of these modules to public if required
25pub mod authority;
26mod constraint;
27mod import;
28pub(crate) mod ion_extension;
29mod ion_path;
30mod ion_schema_element;
31pub mod isl;
32mod ordered_elements_nfa;
33pub mod result;
34pub mod schema;
35pub mod system;
36mod type_reference;
37pub mod types;
38pub mod violation;
39
40pub use ion_schema_element::*;
41
42static ISL_VERSION_MARKER_REGEX: OnceLock<Regex> = OnceLock::new();
43static RESERVED_WORD_REGEX: OnceLock<Regex> = OnceLock::new();
44
45/// Checks if a value is an ISL version marker.
46fn is_isl_version_marker(text: &str) -> bool {
47    ISL_VERSION_MARKER_REGEX
48        .get_or_init(|| Regex::new(r"^\$ion_schema_\d.*$").unwrap())
49        .is_match(text)
50}
51
52/// Checks is a value is reserved keyword ISL version maker.
53fn is_reserved_word(text: &str) -> bool {
54    RESERVED_WORD_REGEX
55        .get_or_init(|| Regex::new(r"^(\$ion_schema(_.*)?|[a-z][a-z0-9]*(_[a-z0-9]+)*)$").unwrap())
56        .is_match(text)
57}
58
59const ISL_2_0_KEYWORDS: [&str; 28] = [
60    "all_of",
61    "annotations",
62    "any_of",
63    "as",
64    "byte_length",
65    "codepoint_length",
66    "container_length",
67    "contains",
68    "element",
69    "exponent",
70    "field_names",
71    "fields",
72    "id",
73    "imports",
74    "name",
75    "not",
76    "occurs",
77    "one_of",
78    "ordered_elements",
79    "precision",
80    "regex",
81    "schema_footer",
82    "schema_header",
83    "timestamp_precision",
84    "type",
85    "user_reserved_fields",
86    "utf8_byte_length",
87    "valid_values",
88];
89
90// helper function to be used by schema tests
91fn load(text: &str) -> Vec<Element> {
92    Element::read_all(text.as_bytes())
93        .expect("parsing failed unexpectedly")
94        .into_iter()
95        .collect()
96}
97
98// TODO: Move this to a more sensible location
99#[derive(Debug, Clone, Default, PartialEq)]
100pub struct UserReservedFields {
101    schema_header_fields: Vec<String>,
102    schema_footer_fields: Vec<String>,
103    type_fields: Vec<String>,
104}
105
106impl UserReservedFields {
107    pub(crate) fn is_empty(&self) -> bool {
108        self.type_fields.is_empty()
109            && self.schema_header_fields.is_empty()
110            && self.schema_footer_fields.is_empty()
111    }
112
113    /// Parse use reserved fields inside a [Struct]
114    pub(crate) fn from_ion_elements(user_reserved_fields: &Struct) -> IonSchemaResult<Self> {
115        if user_reserved_fields.fields().any(|(f, v)| {
116            f.text() != Some("schema_header")
117                && f.text() != Some("schema_footer")
118                && f.text() != Some("type")
119        }) {
120            return invalid_schema_error(
121                "User reserved fields can only have schema_header, schema_footer or type as the field names",
122            );
123        }
124        Ok(Self {
125            schema_header_fields: UserReservedFields::field_names_from_ion_elements(
126                "schema_header",
127                user_reserved_fields,
128            )?,
129            schema_footer_fields: UserReservedFields::field_names_from_ion_elements(
130                "schema_footer",
131                user_reserved_fields,
132            )?,
133            type_fields: UserReservedFields::field_names_from_ion_elements(
134                "type",
135                user_reserved_fields,
136            )?,
137        })
138    }
139
140    fn field_names_from_ion_elements(
141        user_reserved_fields_type: &str,
142        user_reserved_fields: &Struct,
143    ) -> IonSchemaResult<Vec<String>> {
144        let user_reserved_elements: Vec<&Element> = user_reserved_fields
145            .get(user_reserved_fields_type)
146            .and_then(|it| it.as_sequence().map(|s| s.elements().collect()))
147            .ok_or(invalid_schema_error_raw(
148                "User reserved fields mut be non null",
149            ))?;
150
151        let user_reserved_fields = user_reserved_elements
152            .iter()
153            .filter(|e| e.annotations().is_empty() && !e.is_null())
154            .map(|e| e.as_text().map(|s| s.to_owned()))
155            .collect::<Option<Vec<String>>>()
156            .unwrap_or(vec![]);
157
158        if user_reserved_fields.len() != user_reserved_elements.len() {
159            return invalid_schema_error("User reserved fields mut be unannotated");
160        }
161
162        if user_reserved_fields
163            .iter()
164            .any(|f| is_reserved_word(f) || ISL_2_0_KEYWORDS.binary_search(&f.as_str()).is_ok())
165        {
166            return invalid_schema_error(
167                "ISl 2.0 keywords may not be declared as user reserved fields",
168            );
169        }
170
171        Ok(user_reserved_fields)
172    }
173
174    pub(crate) fn validate_field_names_in_header(
175        &self,
176        schema_header: &Struct,
177    ) -> IonSchemaResult<()> {
178        let unexpected_fields: Vec<(&Symbol, &Element)> = schema_header
179            .fields()
180            .filter(|(f, v)| {
181                !self
182                    .schema_header_fields
183                    .contains(&f.text().unwrap().to_owned())
184                    && f.text().unwrap() != "user_reserved_fields"
185                    && f.text().unwrap() != "imports"
186            })
187            .collect();
188
189        if !unexpected_fields.is_empty() {
190            // for unexpected fields return invalid schema error
191            return invalid_schema_error(format!(
192                "schema header contains unexpected fields: {unexpected_fields:?}"
193            ));
194        }
195
196        Ok(())
197    }
198
199    pub(crate) fn validate_field_names_in_type(&self, isl_type: &IslType) -> IonSchemaResult<()> {
200        let unexpected_fields: &Vec<&String> = &isl_type
201            .constraints()
202            .iter()
203            .filter(|c| matches!(c.constraint_value, IslConstraintValue::Unknown(_, _)))
204            .map(|c| match &c.constraint_value {
205                IslConstraintValue::Unknown(f, v) => f,
206                _ => {
207                    unreachable!("we have already filtered all other constraints")
208                }
209            })
210            .filter(|f| !self.type_fields.contains(f))
211            .collect();
212
213        if !unexpected_fields.is_empty() {
214            // for unexpected fields return invalid schema error
215            return invalid_schema_error(format!(
216                "schema type contains unexpected fields: {unexpected_fields:?}"
217            ));
218        }
219
220        Ok(())
221    }
222
223    pub(crate) fn validate_field_names_in_footer(
224        &self,
225        schema_footer: &Struct,
226    ) -> IonSchemaResult<()> {
227        let unexpected_fields: Vec<(&Symbol, &Element)> = schema_footer
228            .fields()
229            .filter(|(f, v)| {
230                !self
231                    .schema_footer_fields
232                    .contains(&f.text().unwrap().to_owned())
233            })
234            .collect();
235
236        if !unexpected_fields.is_empty() {
237            // for unexpected fields return invalid schema error
238            return invalid_schema_error(format!(
239                "schema footer contains unexpected fields: {unexpected_fields:?}"
240            ));
241        }
242        Ok(())
243    }
244}
245
246impl WriteAsIon for UserReservedFields {
247    fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
248        let mut struct_writer = writer.struct_writer()?;
249        struct_writer
250            .field_writer("schema_header")
251            .write(&self.schema_header_fields)?;
252        struct_writer
253            .field_writer("type")
254            .write(&self.type_fields)?;
255        struct_writer
256            .field_writer("schema_footer")
257            .write(&self.schema_footer_fields)?;
258        struct_writer.close()
259    }
260}