use crate::isl::isl_import::{IslImport, IslImportType};
use crate::isl::isl_type::IslType;
use crate::UserReservedFields;
use ion_rs::Element;
use ion_rs::{IonResult, SequenceWriter, StructWriter, ValueWriter, WriteAsIon};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
pub mod isl_constraint;
pub mod isl_import;
pub mod isl_type;
pub mod isl_type_reference;
pub mod ranges;
pub mod util;
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum IslVersion {
V1_0,
V2_0,
}
impl WriteAsIon for IslVersion {
fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
let text = match self {
IslVersion::V1_0 => "$ion_schema_1_0",
IslVersion::V2_0 => "$ion_schema_2_0",
};
writer.write_symbol(text)
}
}
impl Display for IslVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
IslVersion::V1_0 => "ISL 1.0",
IslVersion::V2_0 => "ISL 2.0",
}
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SchemaContent {
Version(IslVersion),
Header(SchemaHeader),
Type(IslType),
Footer(SchemaFooter),
OpenContent(Element),
}
impl From<IslVersion> for SchemaContent {
fn from(value: IslVersion) -> Self {
SchemaContent::Version(value)
}
}
impl From<SchemaHeader> for SchemaContent {
fn from(value: SchemaHeader) -> Self {
SchemaContent::Header(value)
}
}
impl From<IslType> for SchemaContent {
fn from(value: IslType) -> Self {
SchemaContent::Type(value)
}
}
impl From<SchemaFooter> for SchemaContent {
fn from(value: SchemaFooter) -> Self {
SchemaContent::Footer(value)
}
}
impl SchemaContent {
fn as_header(&self) -> Option<&SchemaHeader> {
if let SchemaContent::Header(schema_header) = self {
Some(schema_header)
} else {
None
}
}
fn as_isl_version(&self) -> Option<&IslVersion> {
if let SchemaContent::Version(value) = self {
Some(value)
} else {
None
}
}
fn as_type(&self) -> Option<&IslType> {
if let SchemaContent::Type(value) = self {
Some(value)
} else {
None
}
}
fn as_open_content(&self) -> Option<&Element> {
if let SchemaContent::OpenContent(value) = self {
Some(value)
} else {
None
}
}
fn as_footer(&self) -> Option<&SchemaFooter> {
if let SchemaContent::Footer(value) = self {
Some(value)
} else {
None
}
}
fn expect_header(&self) -> &SchemaHeader {
let SchemaContent::Header(value) = self else {
unreachable!("expected to find a Header, but found: {:?}", self)
};
value
}
fn expect_isl_version(&self) -> &IslVersion {
let SchemaContent::Version(value) = self else {
unreachable!("expected to find an IslVersion, but found: {:?}", self)
};
value
}
fn expect_type(&self) -> &IslType {
let SchemaContent::Type(value) = self else {
unreachable!("expected to find a Type, but found: {:?}", self)
};
value
}
fn expect_open_content(&self) -> &Element {
let SchemaContent::OpenContent(value) = self else {
unreachable!("expected to find OpenContent, but found: {:?}", self)
};
value
}
fn expect_footer(&self) -> &SchemaFooter {
let SchemaContent::Footer(value) = self else {
unreachable!("expected to find a Footer, but found: {:?}", self)
};
value
}
}
impl WriteAsIon for SchemaContent {
fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
match self {
SchemaContent::Version(value) => writer.write(value),
SchemaContent::Header(value) => writer.write(value),
SchemaContent::Type(value) => writer.write(value),
SchemaContent::Footer(value) => writer.write(value),
SchemaContent::OpenContent(value) => writer.write(value),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SchemaHeader {
user_reserved_fields: UserReservedFields,
imports: Vec<IslImport>,
user_content: Vec<(String, Element)>,
}
impl WriteAsIon for SchemaHeader {
fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
let mut struct_writer = writer
.with_annotations(["schema_header"])?
.struct_writer()?;
if !self.imports.is_empty() {
struct_writer.field_writer("imports").write(&self.imports)?;
}
if !self.user_reserved_fields.is_empty() {
struct_writer
.field_writer("user_reserved_fields")
.write(&self.user_reserved_fields)?;
}
if !self.user_content.is_empty() {
for (k, v) in &self.user_content {
struct_writer.write(k.as_str(), v)?;
}
}
struct_writer.close()
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SchemaFooter {
user_content: Vec<(String, Element)>,
}
impl WriteAsIon for SchemaFooter {
fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
let mut struct_writer = writer
.with_annotations(["schema_footer"])?
.struct_writer()?;
if !self.user_content.is_empty() {
for (k, v) in &self.user_content {
struct_writer.write(k.as_str(), v)?;
}
}
struct_writer.close()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IslSchema {
id: String,
schema_content: Vec<SchemaContent>,
header_index: Option<usize>,
footer_index: Option<usize>,
types_by_name: HashMap<String, usize>,
inline_imported_types: Vec<IslImportType>,
}
impl IslSchema {
fn new_from_content_vec(
id: String,
schema_content: Vec<SchemaContent>,
inline_imported_types: Vec<IslImportType>,
) -> Self {
assert!(matches!(schema_content[0], SchemaContent::Version(_)));
let header_index = schema_content
.iter()
.position(|x| matches!(x, SchemaContent::Header(_)));
let footer_index = schema_content
.iter()
.position(|x| matches!(x, SchemaContent::Footer(_)));
let types_by_name: HashMap<String, usize> = schema_content
.iter()
.enumerate()
.filter(|&(_, x)| matches!(x, SchemaContent::Type(_)))
.map(|(i, t)| (t.expect_type().name().unwrap().to_string(), i))
.collect();
Self {
id,
schema_content,
header_index,
footer_index,
types_by_name,
inline_imported_types,
}
}
pub(crate) fn new<A: AsRef<str>>(
id: A,
version: IslVersion,
user_reserved_fields: Option<UserReservedFields>,
imports: Vec<IslImport>,
types: Vec<IslType>,
inline_imports: Vec<IslImportType>,
open_content: Vec<Element>,
) -> Self {
let mut schema_content: Vec<SchemaContent> = vec![];
schema_content.push(version.into());
schema_content.push(
SchemaHeader {
user_reserved_fields: user_reserved_fields.unwrap_or_default(),
imports,
user_content: vec![],
}
.into(),
);
for t in types {
schema_content.push(t.into());
}
for e in open_content {
schema_content.push(SchemaContent::OpenContent(e));
}
Self::new_from_content_vec(id.as_ref().to_string(), schema_content, inline_imports)
}
pub fn schema_v_1_0<A: AsRef<str>>(
id: A,
imports: Vec<IslImport>,
types: Vec<IslType>,
inline_imports: Vec<IslImportType>,
open_content: Vec<Element>,
) -> IslSchema {
IslSchema::new(
id.as_ref(),
IslVersion::V1_0,
None,
imports,
types,
inline_imports,
open_content,
)
}
pub fn schema_v_2_0<A: AsRef<str>>(
id: A,
user_reserved_fields: UserReservedFields,
imports: Vec<IslImport>,
types: Vec<IslType>,
inline_imports: Vec<IslImportType>,
open_content: Vec<Element>,
) -> IslSchema {
IslSchema::new(
id.as_ref(),
IslVersion::V2_0,
Some(user_reserved_fields),
imports,
types,
inline_imports,
open_content,
)
}
pub fn id(&self) -> String {
self.id.to_owned()
}
pub fn version(&self) -> IslVersion {
*(self.schema_content[0].expect_isl_version())
}
fn header(&self) -> Option<&SchemaHeader> {
self.header_index
.map(|i| self.schema_content.get(i).unwrap())
.map(SchemaContent::expect_header)
}
fn footer(&self) -> Option<&SchemaFooter> {
self.footer_index
.map(|i| self.schema_content.get(i).unwrap())
.map(SchemaContent::expect_footer)
}
const EMPTY_VEC: Vec<IslImport> = vec![];
const EMPTY_VEC_REF: &'static Vec<IslImport> = &Self::EMPTY_VEC;
pub fn imports(&self) -> impl Iterator<Item = &IslImport> {
self.header()
.map(|x| &x.imports)
.unwrap_or(Self::EMPTY_VEC_REF)
.iter()
}
pub fn types(&self) -> impl Iterator<Item = &IslType> {
self.schema_content.iter().filter_map(|x| x.as_type())
}
pub fn inline_imported_types(&self) -> impl Iterator<Item = &IslImportType> {
self.inline_imported_types.iter()
}
pub fn open_content(&self) -> impl Iterator<Item = &Element> {
self.schema_content
.iter()
.filter_map(|x| x.as_open_content())
}
pub fn user_reserved_fields(&self) -> Option<&UserReservedFields> {
self.header().map(|x| &x.user_reserved_fields)
}
}
impl IslSchema {
fn write_as_ion<W: SequenceWriter>(&self, writer: &mut W) -> IonResult<()> {
for item in &self.schema_content {
writer.write(item)?;
}
Ok(())
}
}
impl<'a> IntoIterator for &'a IslSchema {
type Item = &'a SchemaContent;
type IntoIter = SchemaIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
SchemaIterator {
content: &self.schema_content,
i: 0,
}
}
}
pub struct SchemaIterator<'a> {
content: &'a Vec<SchemaContent>,
i: usize,
}
impl<'a> Iterator for SchemaIterator<'a> {
type Item = &'a SchemaContent;
fn next(&mut self) -> Option<Self::Item> {
let next_item = self.content.get(self.i);
self.i += 1;
next_item
}
}
#[cfg(test)]
mod isl_tests {
use crate::authority::FileSystemDocumentAuthority;
use crate::ion_extension::ElementExtensions;
use crate::isl::isl_constraint::v_1_0::*;
use crate::isl::isl_type::v_1_0::load_isl_type as load_isl_type_def;
use crate::isl::isl_type::v_1_0::*;
use crate::isl::isl_type::v_2_0::load_isl_type as load_isl_type_def_v2_0;
use crate::isl::isl_type::IslType;
use crate::isl::isl_type_reference::v_1_0::*;
use crate::isl::ranges::*;
use crate::isl::util::Ieee754InterchangeFormat;
use crate::isl::util::TimestampPrecision;
use crate::isl::util::ValidValue;
use crate::isl::*;
use crate::result::IonSchemaResult;
use crate::system::SchemaSystem;
use ion_rs::v1_0;
use ion_rs::Decimal;
use ion_rs::IonType;
use ion_rs::Symbol;
use ion_rs::{Element, TextFormat, WriteConfig, Writer};
use rstest::*;
use std::path::Path;
use test_generator::test_resources;
fn load_isl_type(text: &str) -> IslType {
load_isl_type_def(text.as_bytes()).unwrap()
}
fn load_isl_type_v2_0(text: &str) -> IslType {
load_isl_type_def_v2_0(text.as_bytes()).unwrap()
}
#[test]
fn test_open_content_for_type_def() -> IonSchemaResult<()> {
let type_def = load_isl_type(
r#"
// type definition with open content
type:: {
name: my_int,
type: int,
unknown_constraint: "this is an open content field value"
}
"#,
);
assert_eq!(
type_def.open_content(),
vec![(
"unknown_constraint".to_owned(),
Element::read_one(r#""this is an open content field value""#.as_bytes())?
)]
);
Ok(())
}
#[rstest(
isl_type1,isl_type2,
case::type_constraint_with_anonymous_type(
load_isl_type(r#" // For a schema with single anonymous type
{type: any}
"#),
anonymous_type([type_constraint(named_type_ref("any"))])
),
case::type_constraint_with_nullable_annotation(
load_isl_type(r#" // For a schema with `nullable` annotation`
{type: nullable::int}
"#),
anonymous_type([type_constraint(nullable_built_in_type_ref(IonType::Int))])
),
case::type_constraint_with_null_or_annotation(
load_isl_type_v2_0(r#" // For a schema with `$null_or` annotation`
{type: $null_or::int}
"#),
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"))])
),
case::type_constraint_with_named_type(
load_isl_type(r#" // For a schema with named type
type:: { name: my_int, type: int }
"#),
named_type("my_int", [type_constraint(named_type_ref("int"))])
),
case::type_constraint_with_named_nullable_type(
load_isl_type(r#" // For a schema with named type
type:: { name: my_nullable_int, type: $int }
"#),
named_type("my_nullable_int", [type_constraint(named_type_ref("$int"))])
),
case::type_constraint_with_self_reference_type(
load_isl_type(r#" // For a schema with self reference type
type:: { name: my_int, type: my_int }
"#),
named_type("my_int", [type_constraint(named_type_ref("my_int"))])
),
case::type_constraint_with_nested_self_reference_type(
load_isl_type(r#" // For a schema with nested self reference type
type:: { name: my_int, type: { type: my_int } }
"#),
named_type("my_int", [type_constraint(anonymous_type_ref([type_constraint(named_type_ref("my_int"))]))])
),
case::type_constraint_with_nested_type(
load_isl_type(r#" // For a schema with nested types
type:: { name: my_int, type: { type: int } }
"#),
named_type("my_int", [type_constraint(anonymous_type_ref([type_constraint(named_type_ref("int"))]))])
),
case::type_constraint_with_nested_multiple_types(
load_isl_type(r#" // For a schema with nested multiple types
type:: { name: my_int, type: { type: int }, type: { type: my_int } }
"#),
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"))]))])
),
case::all_of_constraint(
load_isl_type(r#" // For a schema with all_of type as below:
{ all_of: [{ type: int }] }
"#),
anonymous_type([all_of([anonymous_type_ref([type_constraint(named_type_ref("int"))])])])
),
case::any_of_constraint(
load_isl_type(r#" // For a schema with any_of constraint as below:
{ any_of: [{ type: int }, { type: decimal }] }
"#),
anonymous_type([any_of([anonymous_type_ref([type_constraint(named_type_ref("int"))]), anonymous_type_ref([type_constraint(named_type_ref("decimal"))])])])
),
case::one_of_constraint(
load_isl_type(r#" // For a schema with one_of constraint as below:
{ one_of: [{ type: int }, { type: decimal }] }
"#),
anonymous_type([one_of([anonymous_type_ref([type_constraint(named_type_ref("int"))]), anonymous_type_ref([type_constraint(named_type_ref("decimal"))])])])
),
case::not_constraint(
load_isl_type(r#" // For a schema with not constraint as below:
{ not: { type: int } }
"#),
anonymous_type([not(anonymous_type_ref([type_constraint(named_type_ref("int"))]))])
),
case::ordered_elements_constraint(
load_isl_type(r#" // For a schema with ordered_elements constraint as below:
{ ordered_elements: [ symbol, { type: int }, ] }
"#),
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))])])
),
case::closed_fields_constraint(
load_isl_type_v2_0(r#" // For a schema with fields constraint as below:
{ fields: closed::{ name: string, id: int} }
"#),
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)]),
),
case::fields_constraint(
load_isl_type(r#" // For a schema with fields constraint as below:
{ fields: { name: string, id: int} }
"#),
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())]),
),
case::field_names_constraint(
load_isl_type_v2_0(r#" // For a schema with field_names constraint as below:
{ field_names: distinct::symbol }
"#),
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)]),
),
case::contains_constraint(
load_isl_type(r#" // For a schema with contains constraint as below:
{ contains: [true, 1, "hello"] }
"#),
anonymous_type([contains([true.into(), 1.into(), "hello".to_owned().into()])])
),
case::container_length_constraint(
load_isl_type(r#" // For a schema with container_length constraint as below:
{ container_length: 3 }
"#),
anonymous_type([container_length(3.into())])
),
case::byte_length_constraint(
load_isl_type(r#" // For a schema with byte_length constraint as below:
{ byte_length: 3 }
"#),
anonymous_type([byte_length(3.into())])
),
case::codepoint_length_constraint(
load_isl_type(r#" // For a schema with codepoint_length constraint as below:
{ codepoint_length: 3 }
"#),
anonymous_type([codepoint_length(3.into())])
),
case::element_constraint(
load_isl_type(r#" // For a schema with element constraint as below:
{ element: int }
"#),
anonymous_type([element(named_type_ref("int"))])
),
case::distinct_element_constraint(
load_isl_type_v2_0(r#" // For a schema with distinct element constraint as below:
{ element: distinct::int }
"#),
isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::element(named_type_ref("int"), true)])
),
case::annotations_constraint(
load_isl_type(r#" // For a schema with annotations constraint as below:
{ annotations: closed::[red, blue, green] }
"#),
anonymous_type([annotations(vec!["closed"], vec![Symbol::from("red").into(), Symbol::from("blue").into(), Symbol::from("green").into()])])
),
case::standard_syantx_annotations_constraint(
load_isl_type_v2_0(r#" // For a schema with annotations constraint as below:
{ annotations: { container_length: 1 } }
"#),
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())]))])
),
case::precision_constraint(
load_isl_type(r#" // For a schema with precision constraint as below:
{ precision: 2 }
"#),
anonymous_type([precision(2.into())])
),
case::scale_constraint(
load_isl_type(r#" // For a schema with scale constraint as below:
{ scale: 2 }
"#),
anonymous_type([scale(2.into())])
),
case::exponent_constraint(
load_isl_type_v2_0(r#" // For a schema with exponent constraint as below:
{ exponent: 2 }
"#),
isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::exponent(2.into())])
),
case::timestamp_precision_constraint(
load_isl_type(r#" // For a schema with timestamp_precision constraint as below:
{ timestamp_precision: year }
"#),
anonymous_type([timestamp_precision(TimestampPrecisionRange::new_single_value(TimestampPrecision::Year))])
),
case::valid_values_constraint(
load_isl_type(r#" // For a schema with valid_values constraint as below:
{ valid_values: [2, 3.5, 5e7, "hello", hi] }
"#),
anonymous_type([valid_values(vec![2.into(), Decimal::new(35, -1).into(), 5e7.into(), "hello".to_owned().into(), Symbol::from("hi").into()]).unwrap()])
),
case::valid_values_with_range_constraint(
load_isl_type(r#" // For a schema with valid_values constraint as below:
{ valid_values: range::[1, 5.5] }
"#),
anonymous_type(
[valid_values(vec![
ValidValue::NumberRange(
NumberRange::new_inclusive(
1.into(),
Decimal::new(55, -1)
).unwrap()
)
]).unwrap()]
)
),
case::utf8_byte_length_constraint(
load_isl_type(r#" // For a schema with utf8_byte_length constraint as below:
{ utf8_byte_length: 3 }
"#),
anonymous_type([utf8_byte_length(3.into())])
),
case::regex_constraint(
load_isl_type(r#" // For a schema with regex constraint as below:
{ regex: "[abc]" }
"#),
anonymous_type(
[
regex(
false, false, "[abc]".to_string()
)
]
)
),
case::timestamp_offset_constraint(
load_isl_type(r#" // For a schema with timestamp_offset constraint as below:
{ timestamp_offset: ["+00:00"] }
"#),
anonymous_type(
[timestamp_offset(vec!["+00:00".try_into().unwrap()])]
)
),
case::ieee754_float_constraint(
load_isl_type_v2_0(r#" // For a schema with ieee754_float constraint as below:
{ ieee754_float: binary16 }
"#),
isl_type::v_2_0::anonymous_type([isl_constraint::v_2_0::ieee754_float(Ieee754InterchangeFormat::Binary16)])
),
)]
fn owned_struct_to_isl_type(isl_type1: IslType, isl_type2: IslType) {
assert_eq!(isl_type1, isl_type2);
}
fn load_timestamp_precision_range(text: &str) -> IonSchemaResult<TimestampPrecisionRange> {
TimestampPrecisionRange::from_ion_element(
&Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"),
|e| {
let symbol_text = e.as_symbol().and_then(Symbol::text)?;
TimestampPrecision::try_from(symbol_text).ok()
},
)
}
fn load_number_range(text: &str) -> IonSchemaResult<NumberRange> {
NumberRange::from_ion_element(
&Element::read_one(text.as_bytes()).expect("parsing failed unexpectedly"),
Element::any_number_as_decimal,
)
}
fn elements<T: Into<Element> + std::clone::Clone>(values: &[T]) -> Vec<Element> {
values.iter().cloned().map(|v| v.into()).collect()
}
const SKIP_LIST: [&str; 5] = [
"ion-schema-schemas/json/json.isl", "ion-schema-tests/ion_schema_1_0/nullable.isl", "ion-schema-tests/ion_schema_1_0/schema/import/import_inline.isl",
"ion-schema-tests/ion_schema_2_0/imports/tree/inline_import_a.isl",
"ion-schema-tests/ion_schema_2_0/imports/tree/inline_import_c.isl",
];
fn is_skip_list_path(file_name: &str) -> bool {
SKIP_LIST
.iter()
.map(|p| p.replace('/', std::path::MAIN_SEPARATOR_STR))
.any(|p| p == file_name)
}
#[test_resources("ion-schema-tests/**/*.isl")]
#[test_resources("ion-schema-schemas/**/*.isl")]
fn test_write_to_isl(file_name: &str) -> IonSchemaResult<()> {
if is_skip_list_path(file_name) {
return Ok(());
}
let mut schema_system =
SchemaSystem::new(vec![Box::new(FileSystemDocumentAuthority::new(
Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(),
))]);
let expected_schema = schema_system.load_isl_schema(file_name)?;
let buffer = Vec::new();
let config = WriteConfig::<v1_0::Text>::new(TextFormat::Pretty);
let mut writer = Writer::new(config, buffer)?;
let write_schema_result = expected_schema.write_as_ion(&mut writer);
assert!(write_schema_result.is_ok());
let output = writer.close()?;
let actual_schema = schema_system.new_isl_schema(output.as_slice(), file_name)?;
assert_eq!(actual_schema, expected_schema);
Ok(())
}
}